springboot整合redis

admin
2022-03-03 / 0 评论 / 50 阅读 / 正在检测是否收录...

适合当如缓存场景:

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

承担持久化工作。

读模式缓存使用流程:

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

解决分布式缓存中本地缓存导致数据不一致问题,可以使用redis中间件解决。

springboot整合redis

1、引入springboot整合的start:pom文件引入依赖

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

引入依赖之后就会有RedisAutoConfiguration,里面可以看的到redis的配置文件Redis.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.Properties文件里面包含了redis的配置信息:

@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、redis配置,可以根据上面的Redis.Properties类型,再applicatioin.yml中配置相关属性。

spring:
  redis:
    host: 127.0.0.1
    port: 6379

上面一个最简单的redis配置就完成了。

3、使用redis

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

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

由于使用String类型的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()类型序列化。

使用配置好的StringRedisTemplate。

StringRedisTemplate的value由多种不同类型的值。

保存获取值:

    @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

redis数据库的值:

注意保存获取时,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);

同样获取的json数据也需要反序列化后才能使用:比如转化成一个复杂数据格式

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

注意:

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

    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 + ')');
            }
        }

    }

解决方案:不能只用-Dio.netty.maxDirectMemory去调大内存。

1、升级netty客户端。

2、切换使用jedis

如何使用jedis

redis默认使用的是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要用的。

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

缓存穿透:

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

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

解决方案:null结果也缓存到redis,并加入短暂的过期时间。

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

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

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

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

缓存穿透:

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种费用“热点”的数据。

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

解决方案:

加锁

大量并发只让一个去查,其他人等待,查到以后释放锁,其它人获取到锁,先差缓存,就会有数据了,而不用去DB查询。

总结:

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

2、解决缓存雪崩:设置过期时间(随机值)。

3、解决缓存击穿:加锁。

例如:

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

解锁:

1、通过synchronized/Lock本地锁。

2、分布式锁解决方案Redisson。

但是本地锁synchronized/Lock不能解决分布式带来的击穿问题。因此分布式还得用Redisson进行加锁解决。

redisson分布式锁,可参考另一篇redisson分布式锁。

    //本地加锁
    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实现分布式

redisson分布式锁,实现原理就是利用redis的set nx实现的。

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

分布式锁原理可以看redis官方文档set nx命令

更多分布式锁可参考,另一个篇springboot整合分布式锁redisson。

6

评论 (0)

取消