什么场景需要使用锁?
使用锁的场景有两个特征:
分布式锁要解决的问题?
保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
分布式锁需要具备的特征?
基于MySQL的分布式锁的实现是使用MySQL的唯一索引去实现,主要流程:
唯一索引可以保证这一条数据只被插入一次,即只有一个客户端能正确插入,其他都将返回插入失败
eg:
CREATE TABLE `method_lock` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`method_name` VARCHAR(64) NOT NULL COMMENT '方法名',
`method_desc` VARCHAR(1024) NOT NULL COMMENT '方法描述',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE INDEX `uniq_method_name` (`method_name`)
)
COMMENT='分布式锁'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
唯一索引是方法名,即同一个方法只能被插入一次,不能重复插入,即只有一个客户端能获取到锁,当该客户端业务逻辑执行完毕后,需要将该记录删除,即释放锁
insert into method_lock(method_name, method_desc) values("methodName", "desc");
delete from method_lock where method_name = "methodName";
基于MySQL实现分布式锁有哪些缺陷?
解决方法:
setNx
和 setEx
实现redis命令setnx
加解锁:
if (setnx(key, 1) == 1){
expire(key, 30)
try {
//TODO 业务逻辑
} finally {
del(key)
}
}
存在问题:
单条redis指令是原子的,但是组合并不具备原子性setnx + expire
组合是非原子性的,可能会出现setnx
成功但是expire
失败的场景,没有成功设置超时时间,还是会出现其他节点无法获取到锁的情况
解决方法:LUA脚本,LUA脚本在redis中的执行是原子的
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)
then return 0;
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));
return 1;
// 使用实例
EVAL "if (redis.call('setnx',KEYS[1],ARGV[1]) < 1) then return 0; end; redis.call('expire',KEYS[1],tonumber(ARGV[2])); return 1;" 1 key value 100
redis命令setex,解决了setnx + expire组合非原子的问题
setnx + expire key timeout
的组合setex key time value
del key
或者获取锁的节点崩了导致锁无法释放,导致后序节点无法正常加锁完成相关业务逻辑加解锁:
if (setex(key,30,1) == 1){
try {
//TODO 业务逻辑
} finally {
del(key)
}
}
第二个问题:锁误消除,即加锁和解锁的线程不是同一个
场景:线程A成功获取到锁,且锁失效时间是30s,但是线程A因为网络阻塞等原因,执行了超过30s,那么当到了30s时,线程A还没执行结束,锁过期自动删除,线程B获取到锁,执行线程B的业务逻辑,此时线程A也执行完成,执行线程A的finally块的del(key),但是此时解锁的线程B的锁
解决方法:
第三个问题, 超时解锁导致并发
场景:如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
解决方法:
锁重入实现
在redis实现的分布式锁中,Redisson实现的分布锁通过 redis 的hashmap结构实现了可重入锁
Redisson加锁解锁源码分析:
首先Redisson
底层也是基于lua
脚本实现原子加锁解锁,获取不到锁的客户端会监听锁topic
redis的可用性问题
redis的副本异步复制导致无法担保锁的互斥性,可能导致两个客户端持有锁
为了保证 Redis 的可用性,一般采用主从方式部署。主从数据同步有异步和同步两种方式,Redis 将指令记录在本地内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一致的状态,一边向主节点反馈同步情况。
在包含主从模式的集群部署方式中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。此时就会导致有两个客户端持有锁
redlock算法,redis集群中只要大部分节点存活客户端就可以进行加锁和解锁逻辑
使用多个redis实例来完成分布式锁,这是为了保证在发生单点故障时仍然可用
步骤:
如果加锁失败:
del key
失败重试机制:
解锁:
del key
如果我们的节点没有持久化机制,client 从 5 个 master 中的 3 个处获得了锁,然后其中一个重启了,这是注意 整个环境中又出现了 3 个 master 可供另一个 client 申请同一把锁! 违反了互斥性。如果我们开启了 AOF 持久化那么情况会稍微好转一些,因为 Redis 的过期机制是语义层面实现的,所以在 server 挂了的时候时间依旧在流逝,重启之后锁状态不会受到污染。但是考虑断电之后呢,AOF部分命令没来得及刷回磁盘直接丢失了,除非我们配置刷回策略为 fsnyc = always,但这会损伤性能。解决这个问题的方法是,当一个节点重启之后,我们规定在 max TTL 期间它是不可用的,这样它就不会干扰原本已经申请到的锁,等到它 crash 前的那部分锁都过期了,环境不存在历史锁了,那么再把这个节点加进来正常工作。
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父节点为 /app1。
节点类型
监听器watch机制:
为节点注册一个监听器 watch node
,当节点node状态发生改变,会给客户端发送消息
实现:
/lock
lock
下创建临时顺序子节点/lock
下的子节点目录,如果自己创建的子节点是当前子节点目录下序号最小的节点,则认为成功获取到锁,否则则监听自己的前一个子节点,相当于排队,获得子节点变更后,继续比较自己创建的节点序号是否是最小锁超时
如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。
羊群效应:
还有一种实现是以成功创建节点代表成功获取锁,没有获取到锁的客户端监听该节点,那么在锁释放节点被删除时,所有客户端都会收到通知,导致等待的所有节点一起尝试去向zookeeper服务端创建节点,可能会导致网络的一时阻塞
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。