缓存与分布式锁,Redision

文章讨论了缓存的使用策略,包括何时放入缓存、缓存失效问题(如雪崩、击穿和穿透)、以及如何确保数据一致性。还介绍了分布式锁的实现,如Redis和Redisson,以及解决锁相关问题的方法,如过期时间和lua脚本的使用。
摘要由CSDN通过智能技术生成

1.缓存的使用

1 适合放入缓存的数据:

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

● 目前将目录查到之后就放在缓存中,以后再查目录的时候如果已经查过了就不需要再查数据库,而是直接从缓存里面拿,避免了再一次查找数据库,加快效率。
● 注意:开发中,放入缓存的数据都应该设置一个过期时间,一方面是使系统即使没有主动更新数据也能触发数据加入缓存,另一方面也是避免程序崩溃的时候,缓存中的数据发了变更,造成数据永久不一致。

2.缓存失效

● 加了缓存之后,在高并发下,也会出现了很多问题,比如缓存雪崩,缓存击穿和缓存穿透。
缓存雪崩:很多key在同一时间失效,造成全部去查数据库,给数据库造成的压力很大。
解决: 设置一个随机的过期时间,不让key在同一时间失效
缓存击穿:对与某个热点key,如果在某一时间失效,这时大量的请求进来查数据库,也会给数据库造成很大的压力。
解决:对于大量的请求同时访问数据库做限制,加锁,只允许一个请求来访问数据库,其余的等待,在第一个请求进入数据库后,后面的时候直接从缓存中拿到数据
缓存穿透:对于数据库中一定不存在的数据(null),缓存中也不存在,那么访问这个不存在的数据每次都会去数据库中查找,造成了不必要的数据库访问。在流量大时,如果有人利用不存在的key频繁攻击服务器,可以会是DB挂了。
解决:将null也存入缓存中,并设置一个较短的过期时间

3.缓存数据一致性

● 定义:数据库中的数据已经更新了,但是缓存中的还没更新,导致用户访问到的还是旧的数据,造成了数据的不一致。
● 解决:
双写模式
■ 更新(写入)数据库的时候,同时更新(写入)缓存,虽然读到的可能有延迟,但能保证最终一致
■ 在并发情况下可能会出现脏数据,但只是暂时的,等缓存过期,下次主动查DB又会变成最新的

● 解决:可以加锁,但是不推荐。一般可以接受一段时间的数据不一致
失效模式
■ 更新(写入)数据库的时候,删除缓存,下一次请求会重新读数据库

○ 使用binlog订阅canal,当数据库发生变化,canal会对缓存进行修改

4.分布式锁的实现
● 本地锁
目前就使用本地锁(sychronized,JUC(lock))来解决缓存击穿的问题
sychronized(this){
Object target = redisTempalte.opsValue(key);
if(target存在){
return target;
}
//查询数据库
//将target放入缓存中
}
但是本地锁只能锁到当前服务实例,在分布式的情况下,每个服务可能部署在多个服务实例上
例如当前,开启四个商品的微服务,分别为10001,10002,10003,10004这几个端口,利用jmeter进行压测,发送500个请求(50个线程,每个线程循环10次),【request–>nigix–>网关(负载均衡)–>4个商品服务实例】,
结果每个服务实例上都进行了一次数据库的查询,但是我们想要的结果是只查一次数据库,不管底层是分布式的还是单体的。

3.分布式锁:

1.  redis其实就可以作为分布式锁,当所有的线程进来的时候,首先到redis中进行“占坑”,使用setNX指令,获取到锁之后,就进行业务的执行,没有获取到锁就进行等待,等前一个线程放了再次尝试获取。

一.

Boolean lock = redisTemplate.setIfAbsent("lock","111");
if(lock){
    //加锁成功..执行业务
    //删除key,释放锁
    redisTemplate.delete("lock");
}else{
    //加速失败,重试(自旋的方式)
}

问题:这里也会有一个其他的问题,就是当前一个线程获取锁之后发生了异常没有进行释放,就会造成死锁,解决方法是给redis的key设置一个过期时间

Boolean lock = redisTemplate.setIfAbsent("lock","111");
if(lock){
    //加锁成功..执行业务
    redisTemplate.expire("lock",30,TimeUnit.SECONDS);
    //删除key,释放锁
    redisTemplate.delete("lock");
}else{
    //加速失败,重试(自旋的方式)
}

问题:设置锁和过期时间不是原子性的,还是会出现上面的问题
二.

Boolean lock = redisTemplate.setIfAbsent("lock","111",10,TimeUnit.SECONDS);
if(lock){
if(lock){
    //加锁成功..执行业务
    //删除key  
    redisTemplate.delete("lock");
}else{
    //加速失败,重试(自旋的方式)
}

问题:在删锁的时候,删的可能不是自己的锁,例如,如果设置锁的过期时间为10,但是改业务已经执行了30s,自己的锁已经过期了,但是现在还能删除,说明删除的是别人占用的锁。
解决:利用UUID设置一个值,在山村的时候进行判断,如果是自己的才进行删除。

三.

Boolean lock = redisTemplate.setIfAbsent("lock",UUID,10,TimeUnit.SECONDS);
if(lock){
    //删除key  
    if(redis.get("lock")==UUID){     
    redisTemplate.delete("lock");                                      
    }
}else{
    //加速失败,重试(自旋的方式)
}

问题:redis.get(“lock”) 的网络可能会消耗一定的时间,比如给这个key设置了10s的过期时间,前面的业务执行话了9.5S,在获取原来的值的时候的过程中,过期被另一个线程获取了,这个时候删除的就是别的线程的锁了

四.

String uuid = UUID.randomUUID().toString();
Boolean lock =
redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb;
  try{
   dataFromDb = getDataFromDb();
    }finally {
     String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
     return redis.call('del', KEYS[1]) else return 0 end";
     Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
   , Arrays.asList("lock"), uuid);
     }
    return dataFromDb;
}else {
//加锁失败...重试。synchronized ()
//休眠 100ms 重试
System.out.println("获取分布式锁失败...等待重试");
    try{
    Thread.sleep(200);
     }catch (Exception e){
     }
    return getCatalogJsonFromDbWithRedisLock();//自旋的方式
    }
}

获取值对比 + 对比成功操作 保证原子性,使用lua脚本
总结:以上基于redis方式实现的分布式锁,通过使用过期时间,lua脚本等来解决死锁和删除别人的锁的问题,但是还是存在的其他的问题。比对于过期时间的大小选择,如果太小了,会造成提前释放锁,其他业务会提前进来,如果太大了,又会影响要性能。

5.Redision

方便分布式锁的使用,可以向使用JUC那样来使用分布式锁
//获取一把锁,只要锁的名字一样,就是一把锁

RLock lock = redisson.getLock("my-lock");

//加锁
lock.lock();//阻塞等待

try{
//加锁成功,执行业务
}catch(){
}finally(){
    //解锁  假设解锁代码没有执行, redision会不会重新死锁
    lock.unlock();
}

启动同一个服务的两个实例1,2,在实例1还在执行业务的过程中停掉该服务,这个时候没有实例1没有释放锁
但是实例2仍可以获取锁

看门狗机制:如果业务很长,会在运行期间给锁加上新的30S过期时间,不用担心业务没执行完锁就过期了,加锁的因业务只要执行完,就不会给锁一个过期时间,即使没有释放锁,也会在30S后就释放锁。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酱学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值