Redis 分布式锁

Redis 分布式锁

分布式锁的演变

  1. 本地锁(单机用)
  2. 利用redis进行分布式锁 使用 set
  3. 防止死锁 加过期时间 使用 setnx
  4. 防止A请求未执行完 锁过期删除 B请求加锁后 A完成后误删该锁 使用 Hash结构, 规定每个请求只能删除自己的锁
  5. 保证并发安全,申请锁和加过期时间需要 原子性 ,用 lua脚本 加锁或解锁
  6. 考虑到 重入性 (每个请求只拿到一个锁后,可以多个函数或线程共用) 使用 Hash结构进行加减(hincrby) 操作
  7. 为了保证业务执行过长,锁不会过期。需要对锁进行 续期

1. setNX

  • 注意死锁
  • 注意lock过期时间和业务执行时间
  • 一般情况下完全够用

2. 考虑look重入性

  • 当一个锁被创建出来之后再同一个请求中不需要再额外申请其他锁,一个锁可以被重复使用,所以使用到 Hash数据结构的锁
  • 如果业务上需要应用到多个函数的锁的情况下,申请一个锁之后固定当前请求的uuid。入一个函数(或者线程)需要用到时 向当前锁的uuid +1
  • 直到请求结束后,检查线程锁是否归0 如果是释放该锁,如果不是说明其他线程未能执行完毕。
锁的操作保证原子性应对高并发
  • 当一个锁(Hash结构) 创建锁和增加过期时间,两步需要lua脚本进行执行保证原子性。
php 复制代码
// lua 脚本编写 
// 加锁操作  如果锁不存在或者当前请求锁的uuid字段存在 则进行加一 (重入性)

KEYS[1]  //  -- 分布式锁的key
ARGV[1]  //  -- 锁的唯一标识,通常是线程ID或调用者标识
ARGV[2]  //  -- 锁的过期时间,单位为毫秒

if redis.call('exists', KEYS[1]) == 0 //锁不存在
or redis.call('hexists', KEYS[1], ARGV[1]) == 1 // 当前请求有锁 进入函数(或新开线程)无需额外申请锁
then
  redis.call('hincrby',KEYS[1], ARGV[1], 1)
  redis.call('expire',KEYS[1], ARGV[2])
  return true
else
  return false
end

//解锁操作
  if redis.call("hexists",KEYS[1], ARGV[1]) == 0 then // 无锁  无需解锁
      return false
  else if redis.call("hincrby",KEYS[1], ARGV[1], -1) == 0 then  // 锁存在且是自己请求的锁 进行减一操作,如果减为0 则解锁(删key)
      retrun redis.call("DEL",KEYS[1])
  else
      return 0
  • 当lock 创建时,需要同步启动一个定时续期任务,锁存在并过该定时时间进行续期,防止业务未完直接释放锁。
  • 当主线程执行完业务流程 并释放锁之后,续期机制同时结束。