适合当如缓存场景:

  1. 即时性、数据一致性要求不高的。
  2. 访问量大且更新频率不高的数据(读多,写少)

承担Persistence工作。

读模式缓存使用流程:

凡是放入Cache中的Data,我们应该指定过期时间,使其可以再系统即使没有主动更新Data也能自动触发Data加载进Cache的流程。避免业务崩溃导致的Data永久不一致Problem。

解决Distributed Cache中本地Cache导致Data不一致Problem,可以Usageredis中间件解决。

springboot整合redis

1、引入springboot整合的start:pomFile引入Dependency

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

引入Dependency之后就会有RedisAutoConfiguration,里面可以看的到redis的Configuration FileRedis.Properties.

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

Redis.PropertiesFile里面包含了redis的Configuration信息:

@ConfigurationProperties(
    prefix = "spring.redis"
)
public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String username;
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private Duration connectTimeout;
    private String clientName;
    private RedisProperties.ClientType clientType;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();

2、redisConfiguration,可以根据上面的Redis.PropertiesType,再applicatioin.yml中Configuration相关属性。

spring:
  redis:
    host: 127.0.0.1
    port: 6379

上面一个最简单的redisConfiguration就完成了。

3、Usageredis

由上面的RedisAutoConfiguration自动Configuration类,可以看到自动装配了RedisTemplate<Object, Object>、StringRedisTemplate。

RedisTemplate<Object, Object>:一般是字符串的Key,和序列化后的字符串value。

由于UsageStringType的key、value较多,所以还提供了StringRedisTemplate。

public class StringRedisTemplate extends RedisTemplate<String, String> {
    public StringRedisTemplate() {
        this.setKeySerializer(RedisSerializer.string());
        this.setValueSerializer(RedisSerializer.string());
        this.setHashKeySerializer(RedisSerializer.string());
        this.setHashValueSerializer(RedisSerializer.string());
    }

    public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
        this();
        this.setConnectionFactory(connectionFactory);
        this.afterPropertiesSet();
    }

    protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
        return new DefaultStringRedisConnection(connection);
    }
}

StringRedisTemplate也是继承了RedisTemplate<String, String>,都是字符串的key,value.

this.setKeySerializer(RedisSerializer.string()); this.setValueSerializer(RedisSerializer.string()); this.setHashKeySerializer(RedisSerializer.string()); this.setHashValueSerializer(RedisSerializer.string());

上面的key、value都是用RedisSerializer.string()Type序列化。

UsageConfiguration好的StringRedisTemplate。

StringRedisTemplate的value由多种不同Type的值。

保存Get值:

    @Test
    public void testStringRedisTemplate(){
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        opsForValue.set("hello", "world"+ UUID.randomUUID().toString());
        String hello = opsForValue.get("hello");
        System.out.println("之前保存值为:"+hello);
    }

输出结果:

之前保存值为:world1e9f395e-67b8-4077-9912-c690c7da0f06

redisDatabase的值:

注意保存Get时,key必须是一样的。

value一般是序列化后的json字符串,因为json字符串是跨语言跨平台的。vlaue存复杂对象时,序列化可以用alibaba的fastjson。

Map<String,List<UserEntity>> data:要存入redis的复杂数据,通过序列化后存在redis。
String  s = JSON.toJSONString(data);
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set("mydata",s);

同样Get的jsonData也需要反序列化后才能Usage:比如转化成一个复杂Data格式

String jsonStr= ops.get("mydata");
Map<String,List<UserEntity>> result = JSON.parseObject(jsonStr, new TypeReference<Map<String,List<UserEntity>>>() {});

注意:

springboot2.0后默认Usagelettuce作为操作redis的客户端,Usagenetty进行网络通信。但是lettuce的bug导致netty容易堆外内存溢出。netty如果没有指定堆外内存,默认Usage-Xmx300m。可以通过-Dio.netty.maxDirectMemorySettings堆外内存大小,但是始终会出现堆外内存溢出。

    private static void incrementMemoryCounter(int capacity) {
        if (DIRECT_MEMORY_COUNTER != null) {
            long newUsedMemory = DIRECT_MEMORY_COUNTER.addAndGet((long)capacity);
            if (newUsedMemory > DIRECT_MEMORY_LIMIT) {
                DIRECT_MEMORY_COUNTER.addAndGet((long)(-capacity));
                throw new OutOfDirectMemoryError("failed to allocate " + capacity + " byte(s) of direct memory (used: " + (newUsedMemory - (long)capacity) + ", max: " + DIRECT_MEMORY_LIMIT + ')');
            }
        }

    }

Solution:不能只用-Dio.netty.maxDirectMemory去调大内存。

1、升级netty客户端。

2、切换Usagejedis

如何使用jedis

redis默认Usage的是lettuce,因此需要排除掉lettuce,引入jedis。

        <!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion><!--排除lettuce-->
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--引入jedis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

lettuce、jedis都是操作redis的底层客户端。RedisTemplate是对lettuce、jedis对redis操作的再次封装,以后操作都可以用RedisTemplate进行操作redis。

通过redis的自动装配可以看到是引入了lettuce,jedis的。

@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {

通过jedis可以看到。

class JedisConnectionConfiguration extends RedisConnectionConfiguration {
    JedisConnectionConfiguration(RedisProperties properties, ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration, ObjectProvider<RedisClusterConfiguration> clusterConfiguration) {
        super(properties, sentinelConfiguration, clusterConfiguration);
    }

    @Bean
    JedisConnectionFactory redisConnectionFactory(ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
        return this.createJedisConnectionFactory(builderCustomizers);
    }

无论是lettuce、jedis最后都会注入JedisConnectionFactory,就是redis要用的。

高并发下缓存失效问题-缓存穿透:

Cache穿透:

指查询一个一定不存的Data,由于Cache是不命中,将去查询Database,但是Database也没有此记录,我们将这此查询的null写入Cache,这将导致这个不存在的Data每次请求都要到Storage层查询,失去了Cache的意义。

风险:利用不存在的Data进行攻击,,Database瞬时压力增大,最终导致崩溃。

Solution:null结果也Cache到redis,并加入短暂的过期时间。

高并发下缓存失效-缓存雪崩

Cache Avalanche:指在我们SettingsCache时key采用了相同的过期时间,导致Cache在某一时间同时失效,请求全部转发到DB,DB瞬时压力过大雪崩。

Solution:原有的失效时间Basic上增加一个随机值,比如11-5分分钟随机,这样每一个Cache的过期时间重复概率就会降低,很难引发集体失效。

高并发下缓存失效问题-缓存击穿

Cache穿透:

对于一些Settings了过期时间的key,如果这些key可能会在某些时间点被超高Concurrency地访问,是一种费用“热点”的Data。

如果这个key在大量请求同时进来前正好失效,那么所有对这个key的Data查询都落到DB上,我们称为Cache Penetration。

Solution:

加Lock

大量Concurrency只让一个去查,其他人等待,查到以后释放Lock,其它人Get到Lock,先差Cache,就会有Data了,而不用去DB查询。

Summary:

1、解决Cache穿透:空结果也Cache。

2、解决Cache Avalanche:Settings过期时间(随机值)。

3、解决Cache Penetration:加Lock。

例如:

ops.set("RecordData",null, 1, TimeUnit.DAYS);   //null值也缓存
ops.set("RecordData",JSON.toJSONString(recordEntity), 1, TimeUnit.DAYS);

解Lock:

1、通过synchronized/Lock本地Lock。

2、Distributed LockSolutionRedisson。

但是本地Locksynchronized/Lock不能解决Distributed带来的击穿Problem。因此Distributed还得用Redisson进行加Lock解决。

redissonDistributed Lock,可参考另一篇redissonDistributed Lock。

    //本地加锁
    public RecordEntity getEntityByIdByRedis(Long id){
        synchronized (this){
            ValueOperations<String, String> ops = redisTemplate.opsForValue();
            String recordData = ops.get("RecordData");

            if(recordData!=null){
                System.out.println("从数据库查询数据之前,有缓存数据。。。。");
                return JSON.parseObject(recordData, new TypeReference<RecordEntity>() {});
            }
            System.out.println("从数据库查询数据....");
            RecordEntity recordEntity = baseMapper.selectById(id);
            ops.set("RecordData",JSON.toJSONString(recordEntity), 1, TimeUnit.DAYS);
            return recordEntity;
        }
    }

    //redis分布式锁
    public RecordEntity getEntityByIdByFbs(Long id){
        String uuid = UUID.randomUUID().toString();
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        //保证原子性,加锁同时设置过期时间
        Boolean lock = ops.setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
        if(lock){
            System.out.println("获取分布式锁成功。。。。。");
            RecordEntity recordEntity = null;
            try{
                recordEntity = getEntityById(id);
            }finally {
                //删除锁保证原子性,使用脚本
                String script ="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return recordEntity;
        }else{
            System.out.println("获取分布式锁失败,等待重试。。。。");
            //重试,可以等待sleep一下
            try{
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getEntityByIdByFbs(id);
        }

    }

    //业务代码
    public RecordEntity getEntityById(Long id){
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        String recordData = ops.get("RecordData");

        if(recordData!=null){
            System.out.println("从数据库查询数据之前,有缓存数据。。。。");
            return JSON.parseObject(recordData, new TypeReference<RecordEntity>() {});
        }
        System.out.println("从数据库查询数据....");
        RecordEntity recordEntity = baseMapper.selectById(id);
        ops.set("RecordData",JSON.toJSONString(recordEntity), 1, TimeUnit.DAYS);
        return recordEntity;
    }

    //Redisson分布式锁
    public RecordEntity getEntityByIdByFbsRedisson(Long id){
        //保证原子性,加锁同时设置过期时间
        RLock lock = redissonClient.getLock("RecordData-lock");
        lock.lock();
        RecordEntity recordEntity = null;
        try {
            System.out.println("获取分布式锁成功。。。。。");
            recordEntity = getEntityById(id);
        }finally {
            System.out.println("释放锁成功。。。。。");
            lock.unlock();
        }

        return recordEntity;
    }

//    使用缓存注解方式
    @Override
    @Cacheable(value = {"record"},key = "#root.method.name")
    public RecordEntity getRecordAllInfoById(Long id) {
        //未使用注解缓存方案
//        RecordEntity recordEntity = null;
//
//        ValueOperations<String, String> forValue = redisTemplate.opsForValue();
//        String recordData = forValue.get("RecordData");
//        if(recordData==null){
//            System.out.println("缓存没数据,执行查询数据库方法。。。。");
//            recordEntity = getEntityByIdByFbsRedisson(id);
//        }else{
//            System.out.println("从缓存中获取数据。。。。。");
//            recordEntity = JSON.parseObject(recordData, new TypeReference<RecordEntity>() {
//            });
//        }

        //使用注解缓存后
        RecordEntity recordEntity = getEntityById(id);

        if(recordEntity!=null){
            Long categoryId = recordEntity.getCategoryId();
            Long[] catelogPath = findCatelogPath(categoryId);
            recordEntity.setCatelogPath(catelogPath);
        }
        return recordEntity;
    }

利用redis的set NX实现分布式

redissonDistributed Lock,Implementation Principle就是利用redis的set nxImplementation的。

SET key value [EX seconds] [PX milliseconds] [NX|XX]

Distributed LockPrinciple可以看redis官方文档set nxCommand

更多Distributed Lock可参考,另一个篇springboot整合Distributed Lockredisson。