Redis分布式锁的理解
背景:小徐是一个程序员,他开发了一个秒杀功能,但出现了超卖问题。
- 超卖问题的出现:多线程并发环境下,如果同时对一个共享资源进行读写,数据会出现错乱的问题
这一次他加了一个同步锁 synchronized,这次终于不会超卖了,那我们都知道当多线程并发情况下我们加了同步锁,在同一时刻保证只有一个线程能拿到锁,其他进程进来会进行一个互斥,需要排队等待需要等持有锁线程处理完释放锁
但是随着用户量越来越多小徐发现服务器压力越来越大,性能达到了瓶颈,于是小徐通过nginx进行了负载均衡,他将服务器进行了水平扩展,通过nginx进行了分布式集群部署,但是测试时吞吐量确实上来了,但是秒杀功能又出现了超卖问题,经过发现原来是同步锁的问题,因为同步锁它是JVM级别的,它只能锁住单个进程,但是经过分布式部署之后呢,每台服务器在并发的情况下只能锁住一个线程
所以要解决这个问题我们就要用到分布式锁,分布式锁,顾名思义,分布式锁就是分布式场景下的锁,比如多台不同服务器上的进程,去竞争同一项资源,就是分布式锁。
主流的分布式锁的解决方案有Redis和zookeeper
这里我们主要讲解Redis的分布式锁
1.我们先实现一个最简单的分布式锁
直接用Redis的setnx命令,这个命令的语法是:setnx key value如果key不存在,则会将key设置为value,并返回1;如果key存在,不会有任务影响,返回0。
基于这个特性,我们就可以用setnx实现加锁的目的:通过setnx加锁,加锁之后其他服务无法加锁,用完之后,再通过delete解锁。
但是一定要加过期时间,因为如果用户在请求的过程中,服务器挂了,那么其他的服务器正常请求时,就会出现一个阻塞的情况,因为其他服务器的线程通过setnx进行上锁的时候,发现这个键里面一直有值,就会永远不会上锁成功,之前挂掉的服务器它一直持有锁从而造成了一个死锁的现象,所以此时我们要加上一个过期时间进行兜底,经过这个时间后锁就会自动释放,从而不影响其他服务器的正常请求
二个问题的解决(锁续期与锁误删)以及Lua脚本
虽然业务的扩展,我们又发现了问题,当业务(线程1)的处理时间超过了这把锁的过期时间时,此时业务还没有处理完,锁就释放掉了,其他的线程(线程2)就会趁虚而入,线程1处理完业务后,回来释放锁,此时释放的就是线程2的锁,而其他的线程此时又会趁虚而入,以此类推。总结下来就是有两个问题:
- 锁过期时线程还在处理业务当中
- 存在线程1释放掉线程2的锁,即锁误删现象(分布式锁需要满足谁申请谁释放原则,不能释放别人的锁,也就是说,分布式锁,是要有归属的。)
我们先来解决第一个问题-锁过期时线程还在处理业务当中怎么办呢
- 我们可以加长锁的一个过期时间,并且我们还需要考虑到如果我加长的这个时间还是不够怎么办呢,我们增加一个兜底的方案,在业务代码当中我们添加一个子线程,每10秒去确认主线程是不是在线,如果在线则将过期时间重置,也就是将锁续期
我们再来解决第二个问题-锁误删现象怎么解决
- 就是我们给锁增加一个唯一ID(UUID),这样就能保证每一把锁的它的KEY是绑定的自己的那一个线程,从而业务执行完毕后会先检查锁是不是自己的,最后进行释放。就不会释放其他线程的锁
Lua脚本
也就是说我们完整的流程是竞争者获取锁执行任务,执行完毕后检查锁是不是自己的,最后进行释放。但是执行完毕后,检查锁,再释放,这些操作如何保证它是原子化的操作呢?Redis还有个特性,专门整合原子操作,就是Lua脚本。
Lua脚本可以保证原子性,因为Redis会将Lua脚本封装成一个单独的事务,而这个单独的事务会在Redis客户端运行时,由Redis服务器自行处理并完成整个事务,如果在这个进程中有其他客户端请求的时候,Redis将会把它暂存起来,等到 Lua 脚本处理完毕后,才会再把被暂存的请求恢复。
但是我们发现如果我们自己实现锁误删和锁续期这些代码非常的麻烦,还要保证它的一个健壮性,所以Redis有没有提供相关的组件来完成这些功能呢,有的兄弟有的,这就是Redisson
Redisson原理
Redis提供了一个Redisson完成我们刚刚说到的功能,实现起来也非常的简单,只要添加Redisson相关的一个依赖,把Redisson的客户端自动装配起来通过lock.lock()就可以实现Redisson的分布式锁

我们来说说Redisson的原理是什么,同样的多个线程请求同一资源,当然只有一个线程才能获取这把锁,比如线程1获取到了这把锁,它的key呢就如我们刚刚所说,它用的是UUID+线程ID合并起来保证我们的key和当前线程绑定在一起,这样就不会出现锁误删的问题,当线程1获取锁成功去处理业务的时候,它内部会有一个看门狗机制,它呢会每隔10秒看一下当前线程是否还持有锁,如果持有的话就延长生存时间,从而给这把锁续命。如果我们实现了Redis的集群呢,它就会选择Redis当中的某一个集群
那如果没有获取到锁的话,线程就会不停的自旋尝试获取锁只到超时为止
以上就是Redisson的实现原理
红锁的实现
在Redis中如果使用了主从集群的一个模式,因为Redis采用的是AP模式,也就是它只能保证这个高可用和高性能,但是不能保证高一致性,当我们设置一个锁的时候它其实只会往一个节点去设置一个锁,设置完了就会立马告诉你设置成功,然后内部进行主从同步,如果我们把这个key设置到了主节点,我们主从同步的时候正好主节点挂了,从节点并没有同步到这把锁,导致新的主节点也没有同步过来锁信息,客户端可能重新获取新的主节点的锁,出现了多客户端同时持锁,导致数据不一致的问题。(主从异步复制+主从切换可能出现“旧主没释放锁,新主也没这把锁”的锁丢失/多客户端同时持锁问题。)这个时候应该怎么做呢
其实Redis也提供了相应的解决方法,那就是红锁RedLock,Redlock 是 Redis 官方提出的分布式锁算法,通过在N个(通常为5)相互独立的 master 上同时加锁,并且拿到多数派的锁(≥ N/2+1) 才算成功,来提升锁的安全性和可用性。如果没有RedLock就如我们刚刚所说,我们往一个主节点去设置一个锁,设置完了就会立马响应设置成功,而不去管从节点是否完成了同步,我们使用了RedLock,它要保证你提供的多数派节点(5个主节点,其中要5/2+1,即4个)都存储完毕了,它才会给你响应设置完成,来提升锁的安全性和可用性