分布式锁需满足四个条件

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
  4. 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

常见分布式锁方案对比

分类 方案 实现原理 优点 缺点
基于数据库 基于mysql 表唯一索引 1.表增加唯一索引 2.加锁:执行insert语句,若报错,则表明加锁失败 3.解锁:执行delete语句 完全利用DB现有能力,实现简单 1.锁无超时自动失效机制,有死锁风险 2.不支持锁重入,不支持阻塞等待 3.操作数据库开销大,性能不高
基于MongoDB findAndModify原子操作 1.加锁:执行findAndModify原子命令查找document,若不存在则新增 2.解锁:删除document 实现也很容易,较基于MySQL唯一索引的方案,性能要好很多 1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员 2.锁无超时自动失效机制
基于分布式协调系统 基于ZooKeeper 1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点 2.解锁:删除节点 1.由zk保障系统高可用 2.Curator框架已原生支持系列分布式锁命令,使用简单 需单独维护一套zk集群,维保成本高
基于缓存 基于redis命令 1. 加锁:执行setnx,若成功再执行expire添加过期时间 2. 解锁:执行delete命令 实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好 1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入
基于redis Lua脚本能力 1. 加锁:执行SET lock_name random_value EX seconds NX 命令 2. 解锁:执行Lua脚本,释放锁时验证random_value — ARGV[1]为random_value, KEYS[1]为lock_nameif redis.call(“get”, KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1])else return 0end 同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。 不支持锁重入,不支持阻塞等待
redisson分布式锁 redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作

Redisson分布式锁的实现

Redisson 分布式重入锁用法 https://www.jianshu.com/p/f302aa345ca8
Redisson 支持单点模式、主从模式、哨兵模式、集群模式

image.png

普通实现

说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value px milliseconds nx。后一种方式的核心实现命令如下:

  1. - 获取锁(unique_value可以是UUID等)
  2. SET resource_name unique_value NX PX 30000
  3. - 释放锁(lua脚本中,一定要比较value,防止误解锁)
  4. if redis.call("get",KEYS[1]) == ARGV[1] then
  5. return redis.call("del",KEYS[1])
  6. else
  7. return 0
  8. end

参考:

  1. https://mp.weixin.qq.com/s/JLEzNqQsx-Lec03eAsXFOQ
  2. https://www.jianshu.com/p/f302aa345ca8