分布式锁需要解决下面问题
- 互斥性
- 续命(比如想延长redis锁的加锁时间)
- 锁释放
- HA一致性(比如redis主备切换导致锁丢失不互斥了)
下面列出几种分布式锁的实现和对比。
方案 | 互斥性 | 锁释放 | HA一致性 | 续命 | 其他优点 | 其他缺点 |
---|---|---|---|---|---|---|
关系型数据库 | 很好 | 很好(事务保证) | 很好需要配置数据库在完成主备同步之后才返回上层sql成功 | 不需要续命(不需要像KV方案那样设置expireTime) | 互斥性和锁释放指标很好 | 因为加锁期间整个事务都要维持会造成事务时间很长,暂用宝贵连接 |
KV存储 | 一般(如果获得锁之后因为load高或者网络差等造成业务处理时间超过expireTime会造成别的请求可以再次获得锁,造成锁不互斥) | 一般(像应用程序宕机等极端情况需要等待expireTime才能释放锁) | 主备同步完成之后才返回生成成功,对于redis可以使用wait | 延长expire | 互斥性和锁释放相对其他两种方案稍差 | 互斥性和锁释放在可接受范围内,而且性能很好 |
ZK | 很好 | 很好(会话保证) | ZAB协议保证 | 不需要续命(不需要像KV方案那样设置expireTime) | 互斥性和锁释放指标很好 | ZK的性能较低 |
数据库锁(有多种方案,这里只考虑在一个事务里方案)
- 加锁
开启事务并insert - 解锁
rollback
KV数据库锁(以redis举例)
参考:https://help.aliyun.com/document_detail/146758.html
- 加锁
SET resource_1 random_value NX EX 5 - 续命
在Redis中通常需要用Lua脚本来实现自锁自解:
在Redis中可使用如下Lua脚本来实现续租:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("expire",KEYS[1], ARGV[2])
else
return 0
end
- 解锁
在Redis中通常需要用Lua脚本来实现自锁自解:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
- HA一致性
Redis的WAIT命令会阻塞当前客户端,直到这条命令之前的所有写入命令都成功从master同步到指定数量的replica,命令中可以设置单位为毫秒的等待超时时间。在云Redis版中使用WAIT命令提高分布式锁一致性的示例如下:
SET resource_1 random_value NX EX 5
WAIT 1 5000
使用以上代码,客户端在加锁后会等待数据成功同步到replica才继续进行其它操作,最大等待时间为5000毫秒。执行WAIT命令后如果返回结果是1则表示同步成功,无需担心数据不一致。相比红锁,这种实现方法极大地降低了成本。
需要注意的是:
WAIT只会阻塞发送它的客户端,不影响其它客户端。
WAIT返回正确的值表示设置的锁成功同步到了replica,但如果在正常返回前发生高可用切换,数据还是可能丢失,此时WAIT只能用来提示同步可能失败,无法保证数据不丢失。您可以在WAIT返回异常值后重新加锁或者进行数据校验。
解锁不一定需要使用WAIT,因为锁只要存在就能保持互斥,延迟删除不会导致逻辑问题。
ZK锁
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。
当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
网友评论