Redis缓存、Redis分布式锁

1、加菜单进Redis缓存

@Override
    public Map<String,List<Catelog2Vo>> listTest() {
    	//从缓存查询数据
        String category = stringRedisTemplate.opsForValue().get("category");
        if(StringUtils.isEmpty(category)) {
            System.out.println("查询数据库。。。。");
            Map<String,List<Catelog2Vo>> entities = getCategoryTest();
            String s = JSON.toJSONString(entities);
            stringRedisTemplate.opsForValue().set("category",s);
            return entities;
        }
        return JSON.parseObject(category,new TypeReference<Map<String,List<Catelog2Vo>>>(){});

    }

2、加锁解决缓存击穿

缓存击穿:缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,加锁解决。
缓存雪崩:缓存中大量数据同一时间过期,可以设置过期时间不一致
缓存穿透:大量请求不存在的数据,可以在缓存中设置null值

查看缓存,无缓存则查询数据库,并设置完缓存再解锁。
在这里插入图片描述
在这里插入图片描述

3、分布式锁

本地锁只能锁住当前服务,微服务情况下有多个服务,Redis分布式锁就是锁住Redis

①、设置过期时间

@Override
    public Map<String, List<Catelog2Vo>> listTestWithRedisLock() {
        Map<String, List<Catelog2Vo>> categoryTest = getCategoryTest();
        String s = JSON.toJSONString(categoryTest);
        //分布式加锁,设置过期时间,放在服务宕机,锁长期占不释放
        Boolean category = stringRedisTemplate.opsForValue().setIfAbsent("category", s, 300, TimeUnit.SECONDS);

        if(category){
            //加锁成功,执行业务
        }
        else{
            //加锁失败,自旋
            return listTestWithRedisLock();
        }

        return categoryTest;
    }

②、删除锁

如果业务时间太长,过期时间太短,则有可能删除了别的服务占的锁,因此应该业务做完再删除锁。

Map<String, List<Catelog2Vo>> categoryTest = getCategoryTest();
        String s = JSON.toJSONString(categoryTest);
        UUID uuid = UUID.randomUUID();
        //分布式加锁,设置过期时间,放在服务宕机,锁长期占不释放
        Boolean category = stringRedisTemplate.opsForValue().setIfAbsent("category", uuid, 300, TimeUnit.SECONDS);

        if(category){
            //加锁成功,执行业务
            //.......
            //业务执行完成,删除锁
            String category1 = stringRedisTemplate.opsForValue().get("category");
            if(!StringUtils.isEmpty(category1)&&category.equals(uuid)){
                stringRedisTemplate.delete("category");
            }
        }
        else{
            //加锁失败,自旋
            return listTestWithRedisLock();
        }

        return categoryTest;

③、lua脚本 原子性删除锁

有可能网络原因,业务完成时间为9.5秒,过期时间为10秒,当从redis传回来确定是自己的锁,想删时却删掉了其它服务占的锁。
获取值对比+对比成功删除=原子性
在这里插入图片描述

lua脚本

@Override
    public Map<String, List<Catelog2Vo>> listTestWithRedisLock() throws InterruptedException {
        Map<String, List<Catelog2Vo>> categoryTest = null;
        String uuid = UUID.randomUUID().toString();
        //分布式加锁,设置过期时间,放在服务宕机,锁长期占不释放
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if(lock){
            System.out.println("获取分布式锁成功。。。。");
            //加锁成功,执行业务
            try{
                categoryTest = listTest();
            }finally {
                //业务执行完成,删除锁
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Long execute = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                        Arrays.asList("lock"), uuid);
                return categoryTest;
            }
        }
        else{
            //加锁失败,自旋
            System.out.println("获取分布式锁失败。。。。重试");
            Thread.sleep(1000);
            return listTestWithRedisLock();
        }
    }

4、Redisson

1.锁自动续期(看门狗),不用担心业务时间过长,锁被删除,宕机后30秒自动解锁

加锁(设置释放时间)

//设置过期时间的话,不会自动续期
myLock.lock(10,TimeUnit.SECONDS);

2、读写锁

/**
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :必须等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁。写也需要等待
     * 只要有读或者写的存都必须等待
     * @return
     */
    @GetMapping(value = "/write")
    @ResponseBody
    public String writeValue() {
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        String s=UUID.randomUUID().toString();
        RLock rLock = readWriteLock.writeLock();
        try{
            rLock.lock();
            Thread.sleep(20000);
            stringRedisTemplate.opsForValue().set("writeValue",s);
        }catch (Exception e){

        }finally {
            rLock.unlock();
        }
        return s;
    }

    @GetMapping(value = "/read")
    @ResponseBody
    public String readValue() {
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        String s="";
        RLock rLock = readWriteLock.readLock();
        try{
            rLock.lock();
            Thread.sleep(10000);
            s=stringRedisTemplate.opsForValue().get("writeValue");
        }catch (Exception e){

        }finally {
            rLock.unlock();
        }
        return s;
    }

3、信号量

/**
     * 车库停车
     * 3车位
     * 信号量也可以做分布式限流
     */
    @GetMapping(value = "/park")
    @ResponseBody
    public String park() throws InterruptedException {

        RSemaphore park = redisson.getSemaphore("park");
        park.acquire();     //获取一个信号、获取一个值,占一个车位
        boolean flag = park.tryAcquire();

        if (flag) {
            //执行业务
        } else {
            return "error";
        }

        return "ok=>" + flag;
    }

    @GetMapping(value = "/go")
    @ResponseBody
    public String go() {
        RSemaphore park = redisson.getSemaphore("park");
        park.release();     //释放一个车位
        return "ok";
    }

4、缓存和数据库一致性

双写模式,失效模式
在这里插入图片描述
在这里插入图片描述
解决方案:

如果是用户纬度数据(订单数据、用户数据),这种只有用户自己发起操作,并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
通过读写锁可以实现,(对于经常读写的效率就会变低);
总结:

我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
我们不应该过度设计,增加系统的复杂性
遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
在这里插入图片描述

5、SpringCache

①、简化缓存的操作

1、配置依赖

<dependency>
    <groupId>org.springframework.b oot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2、指定缓存类型

spring:
  cache:
  	#指定缓存类型为redis
    type: redis
    redis:
      # 指定redis中的过期时间为1h
      time-to-live: 3600000

3、在主配置类上加上注解@EnableCaching

②、自定义内容

1、自定义ttl

# 指定redis中的过期时间为1h
      time-to-live: 3600000

2、指定缓存的key值

key = "#root.method.name"

3、配置缓存的value为json

//CacheProperties为一个配置文件类,通过@EnableConfigurationProperties可以将CacheProperties加入进容器
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    // @Autowired
    // public CacheProperties cacheProperties;

    /**
     * 配置文件的配置没有用上
     * 加入容器的方法直接用容器里的组件,不用@Autowired
     * @return
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // config = config.entryTtl();
        //配置key,value的存储格式(GenericJackson2JsonRedisSerializer为泛型)
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //将配置文件中所有的配置都生效
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }

}

③、SpringCache原理与不足

1)、读模式

缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。
2)、写模式:(缓存与数据库一致)

读写加锁。
引入Canal,感知到MySQL的更新去更新Redis
读多写多,直接去数据库查询就行
3)、总结:

常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):

写模式(只要缓存的数据有过期时间就足够了)

特殊数据:特殊设计

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值