分布式锁

- 为什么要使用分布式锁?
因为存在并发产生的数据安全问题。
在生产环境中,通常会出现并发的场景,多个用户请求同一个接口时,若接口不对并发进行处理,则会造成数据的不安全,传统单体架构的应用我们可以使用synchronized锁或者是lock锁进行解决,但是在现在,多数应用都是集群部署,使用synchronized只是在单应用层面对JVM进行加锁,对集群部署的应用几乎没用,所以需要使用分布式锁。
我的理解是,高并发实际上就是多个线程在争抢唯一的一块资源,需要使用锁来证明是哪个线程拿到了这块资源,谁拿到这块资源,谁就可以执行自己的业务逻辑代码,证明谁拿到这块资源需要将锁放在所有线程都能看到的地方(存放在Redis),所以使用synchronized就力不从心,使用synchronized相当于是在自己的JVM内锁住,可是相同的方法不值存在于一台JVM的方法栈中。
若以下代码是单体架构部署,则不会出现线程安全问题,若是集群部署,则会出现线程安全问题。

为什么分布式锁用redis 为什么用redis做分布式锁_分布式锁

- 测试可能存在并发问题的接口。

使用Jmeter压测接口,可以发现上图代码在单体架构线程安全,可是在集群部署很容易出现线程间数据不安全的问题
如何使用Jmeter:我还没写(回头写,先挖个坑(逃))

- 如何解决上图代码集群部署线程不安全问题?
:使用分布式锁。
:如何实现分布式锁?
:借助Redis单线程以及setnx的特性实现分布式锁。
:什么是setnx?

为什么分布式锁用redis 为什么用redis做分布式锁_分布式锁_02


---setnx key value

---setnx Grover 666 执行成功
---setnx Grover 777 执行失败(因为Grover的key已存在)
对上图线程不安全代码进行改进(入门级别分布式锁)
加锁

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,v. "zhuge");

释放锁

stringRedisTemplate.delete(lockkey);

为什么分布式锁用redis 为什么用redis做分布式锁_redis_03


:上述修改后的入门分布式锁有没有什么问题?


:死锁问题:当程序拿到锁在执行业务逻辑时,可能会出现抛异常的情况,这个时候程序代码就不能执行到释放锁的代码,此时别的线程也拿不到锁,就出现了死锁问题。


:那抛异常可以使用try-catch-finally解决,将释放锁的代码放在finally代码块中。


:那要是服务宕机呢?


:我可以给锁的key设置一个过期时间,只要超过过期时间自动释放锁。


像这样:


为什么分布式锁用redis 为什么用redis做分布式锁_为什么分布式锁用redis_04


:那要是第一行执行成功后就宕机了呢?


:那就保证这个操作的原子性(不可分割的最小部分),这样写:


为什么分布式锁用redis 为什么用redis做分布式锁_java_05


:这个代码还存在什么问题吗?


:存在锁永久失效的可能。图解:线程一执行业务逻辑到了10s,线程一的锁被释放,此时线程二拿到锁,线程一继续执行剩下的5s的业务代码,执行完毕后该释放锁,此时锁已经是线程二的锁了,线程一释放了线程二的锁,以此类推 ,相当于锁的永久失效。


为什么分布式锁用redis 为什么用redis做分布式锁_redis_06


:如何解决锁永久失效的问题?


:现在的问题是线程一释放掉线程二的锁,线程二释放线程三的锁,只要保证当前线程只能释放自己的锁就好。


代码如下:给每个线程一个clientID,没次释放锁的时候通过clientId判断一下当前释放的锁锁是不是当前线程的锁。


为什么分布式锁用redis 为什么用redis做分布式锁_为什么分布式锁用redis_07


:有的公司确实这样实现分布式锁屏,可是当前代码是否还存在什么问题?


:有,还是会可能出现锁失效的可能,假如线程一花费了9.9s已经执行完finally中的if判断,正准备去释放锁,此时出现了系统卡顿的情况 ,释放锁的代码并没有被执行,可是10s已经到了(系统卡顿代码未执行与redis无关,redis该到期就到期),此时线程一释放的就是线程二的锁了。


:你说的还是释放锁的代码原子性的问题,如何解决呢?


:我们先不探讨代码原子性问题,先试着可以将锁超时时间变得久一点看行不行。


:那也无法根本解决问题,业务逻辑的执行时间仍然不确定,而且锁的时间越久,出现问题后用户等待的时间也就越久,所以锁不宜设置的太久,设置的时间太久也没有意义,有没有什么其他方案保证线程安全呢?


:可以使用锁续期的解决方案,锁续期就是除了主线程之外,每条线程再配备一个B线程,B线程定时去判断当前主线程的锁是否还存在,若存在,就将锁进行续期,若不存在,就说明锁已经被释放,不需要续期。需要注意的是,锁的失效时间大于B线程定时任务判断锁是否存在的任务时间,若锁失效时间是10s,定时任务时间要小于10s.


:可是我给代码加定时任务,加超时时间,加那么多的控制线程安全的代码,我怕写错或者考虑欠缺,总是没有安全感,怎么办呢?


:可以使用Redisson.

Redisson-Github

:什么是Redisson呢?


:可以理解为Java操作Redis的客户端,和Jedis性质一样。Redisson特别善于解决分布式问题。

- 如何使用Redisson?
  1. 在项目中加入Redisson的依赖


为什么分布式锁用redis 为什么用redis做分布式锁_redis_08


  1. 配置Redis,创建Bean


为什么分布式锁用redis 为什么用redis做分布式锁_为什么分布式锁用redis_09



此处创建的是Redis单机模式(只有一台Redis),在生产环境中有很多模式,如下图的哨兵模式,主从模式,集群等等。


为什么分布式锁用redis 为什么用redis做分布式锁_分布式锁_10




  1. 使用Redisson解决高并发问题(三行代码搞定)


为什么分布式锁用redis 为什么用redis做分布式锁_为什么分布式锁用redis_11



- Redisson底层 .lock()实现原理

为什么分布式锁用redis 为什么用redis做分布式锁_redis_12

redisson.lock();最核心的加锁代码如下:

为什么分布式锁用redis 为什么用redis做分布式锁_redisson_13


:上面带那种script是什么东西呀?


:是

Lua脚本


:什么是Lua脚本?


:Lua脚本就是一段运行在服务端的脚本语言,可以被多个语言调用,Redis2.6之后推出了Lua脚本功能,也就是说,可以在Redis中运行一段Lua脚本,而在Redis中运行Lua脚本的好处有以下几点。


为什么分布式锁用redis 为什么用redis做分布式锁_redisson_14


:那上面那段Lua脚本是什么意思呢?


:上面那段Lua代表,设置一个hset key value;


key为当前锁的名称(redisson.getLock(锁名称));


value为当前主线程ID。


并设置一个默认过期时间30s;


:那锁续期在哪里实现呢?


:在以下定时任务中这段Lua脚本中实现,判断当前线程锁是否存在,若存在,则续期。(这个定时任务循环调用自己,并且是延时执行,相当于是30/3=10s秒钟执行一次)

为什么分布式锁用redis 为什么用redis做分布式锁_为什么分布式锁用redis_15

以下代码还存在其他问题吗?

为什么分布式锁用redis 为什么用redis做分布式锁_java_16


大多数互联网公司使用Redisson直接这样使用就可以解决并发问题,还需要注意的就是redis的集群问题,因为多数公司的redis都不止一台。

还存在哪些问题?

问题一:当线程一使用Redis的主节点加锁成功后,线程一继续执行自己的业务代码,这时,Redis的主节点挂了,由于Redis主从复制是异步操作,加锁信息还没有同步到子节点,这时候线程二访问Redis,发现没有加锁信息,于是线程二也开始执行业务代码,就出现了Redis在主从切换时,key丢失导致锁失效的问题

如何解决Redis在主从切换时,key丢失导致锁失效的问题?
使用zk替换redis作为分布式锁

  • 首先要知道的是:Redis遵循CAP原则中的AP原则-可用性(Availability)、分区容错性(Partition tolerance),在加锁成功之后,redis直接将加锁成功的信息返回给业务逻辑主线程,这时候业务逻辑线程开始执行。
  • 再了解一下Zookeeper的机制:Zookeeper遵循CAP中的CP-一致性(Consistency)、分区容错性(Partition tolerance),他的机制是在加锁成功后,先将加锁信息传递给半数以上子节点后,再将加锁成功的信息返回给业务逻辑主线程,这时候业务逻辑线程开始执行。若zk的leader节点挂了之后,zk中的选举机制会保证同步成功的子节点变为leader节点。而且zk有主节点才允许被写入key。使用Zookeeper就不会出现主从切换key丢失的问题。

使用Redlock红锁解决不推荐,红锁中还存在很多小问题)

  • 每次加锁的时候也给半数以上节点加锁,才能算是加锁成功,才能继续执行业务逻辑代码。


为什么分布式锁用redis 为什么用redis做分布式锁_redis_17



为什么分布式锁用redis 为什么用redis做分布式锁_java_18

为什么分布式锁用redis 为什么用redis做分布式锁_为什么分布式锁用redis_19

问题二性能问题,分布式锁其实就是将原本应该并行执行的场景进行串行化,只能等待前一个线程执行完毕释放锁之后,之后等待的线程才有可能执行。

如何解决加锁后的性能问题?

  • 将加锁的粒度降低:将加锁代码的范围减少,只对处理可能产生并发的代码段进行加锁。
  • 使用分段锁的机制提高锁的性能(ConcurrentHashMap中使用的也是分段锁)
  • 分段锁:
    现在要对Product_101的商品进行秒杀100份
    我们将Product_101分为若干个小份
    Product_101_0 20份
    Product_101_1 20份
    Product_101_2 20份
    … … . … … … .20份
    此时就有5个线程可以并发执行,以Product_101_n为加锁的key.
    在高并发场景下,例如12306抢票或者双11秒杀,一定有redis集群,我们还可以将Product_101_n通过某些方式放在不同的redis上提高性能。

完结撒花~