redis中的分布式锁
Redis当中的分布式锁问题:
1.redis中key设置不当会造成的问题:
我们所熟知的redis中的三大问题:缓存穿透,缓存雪崩与缓存击穿。其中缓存击穿就是数据在某一时间点会被超高并发量的访问,如果在此时key恰好过期,那么所有的访问便会全部落到db上面,会带给数据库不小的压力,严重时候会造成数据库的崩溃。
2.解决方案:
解决这钟高并发量下的缓存击穿性的问题,我们有以下的解决方案:
1.加入同步代码块:
加入同步代码块也就是sychronized关键字可以有效的应对高并发下的问题,但是因为锁的时许问题会造成还是会有遗漏的访问请求打到数据库上,在高并发下这种问题会无限的被放大。
2.引入分布式锁:
分布式锁就是为了保证在分布式场景下共享资源在同一时刻只能被同一线程所访问,或者说用来控制分布式系统同步的访问资源。
分布式锁引入后,访问资源的特点有如下:
1.互斥性:在任意时刻,同一条数据只能被一个线程所访问
2.高可用性:当部分结点宕机以后,客户端仍可以正常的获取锁与释放锁
3.独占性:加锁以及解锁必须是同一个线程执行
此外,为了防止避免出现死锁问题,必须设置锁的超时时间
分布式锁的演化历程:
阶段一. 采用setnx获取锁 ----->获取到锁------>执行相应的业务操作-------->删除锁,图解如下:
这种分布式锁会带来严重的问题:
1.setnx占好了位置,业务代码异常或者是在执行业务代码中宕机没有执行删除锁的逻辑,这就会造成死锁。
解决方案:
1.设置锁的自动过期,即使没有删除,也会自动删除
阶段二:采用setnx获取锁 ----->获取到锁------>设置过期时间---->执行相应的业务操作-------->删除锁,图解如下:
即使设置了过期时间这种分布式锁也会带来严重的问题:
setnx设置好了,正好要去设置锁的过期时间,宕机,又会产生死锁
解决方案:
设置过期时间和占位符必须是原子的,redis支持使用setnx设置锁时候并且设置相应的过期时间。
阶段三:采用setnxex获取锁同时设置过期时间 ----->获取到锁------>执行相应的业务操作-------->删除锁,图示:
这种情况下还是有问题:
由于业务时间较长,锁自己过期了,我们就直接进行进行删除锁,会导致有可能把别人正在持有的锁给删除了
解决:
占锁的时候,值指定为uuid,每个人匹配自己锁才能删除
阶段四:采用setnxex获取锁同时设置过期时间和自己uuid----->获取到锁------>执行相应的业务操作-------->删除锁时判断是否是自己上的锁,是解锁,如图所示:
带来的问题:
如果正好判断是当前值,正好要删除锁的时候,锁已经过期,别人已经设置到的新的值,那么我们可能删除还是别人的值
解决:
删除锁必须保证判断与删除时候是原子性的,可以采用redis+lua脚本
基于以上的问题,我们总结出在分布式锁中我们加锁与设置过期时间需要保证原子性,同时在删除锁与判断是否是当前线程的锁同样需要保证是原子性的:
因此我们引入的redission分布式锁:
特性如下:
1.redission是redis官方推荐的客户端,提供Rlock锁,Rlock继承了Juc的lock接口,提供了中断超时,尝试获取锁等操作,支持可重入,互斥等特性。
2.客户端id:采用的是uuid+当前线程id
3.可重入锁:可以参考Java的可重入锁,用于表示该锁当前被递归的次数>=1,如果等于0是该锁才会被删除。
redission其他特性:
如果锁设置了持有锁的超时时间,在超时后会进行锁的释放,如果获取锁的时候不指定持有锁的超时时间,那么默认获取锁30s后超时。为了防止任务没有执行完就释放锁,redission会使用一个守护线程也称作为看门狗机制,定时刷新(超时时间的1/3,默认是10s,也就是每10s续约30s)。直到线程自己释放,这个锁超时时间续约也就是只要这个锁被获取了,则力保这个锁不超时,除非获取锁的线程主动释放,由于获取到锁和这个续命任务的守护线程是在同一线程的,当前锁的线程如果是挂掉了,意味着刷新任务的线程也会停止执行,就不会在刷新锁的超时时间。
解锁。会首先判断该锁是否存在,如果不存在直接放回null,如果该线程持有锁,则对当前的重入值+1,如果计算完后大于0,重新设置超时持有时间并且返回0.如果计算完毕后不大于0.则删除这个hash值,并且进行广播,通知watchLog停止进行刷新,并且放回1。