day38 Redis分布式锁实现、误删问题与Lua脚本原子性

1. 今天做了什么

  • 学习分布式锁的概念和3种实现方式
  • 使用Redis实现分布式锁
  • 解决分布式锁基本实现后的两个问题:误删问题和原子性问题
  • 学习Lua脚本操作Redis的语法,使用Lua脚本保证释放锁的原子性

2. 核心知识点

  • 分布式锁有3种实现方式,这里使用Redis实现
    • 互斥原理是Redis的setnx
    • Redis高性能、高可用
    • 使用Redis的过期时间来确保不会出现死锁,但如果业务处理时间过长可能导致锁提前释放
  • 这里的Redis实现与之前学缓存击穿时使用的互斥锁方案类似,都是使用Redis的setnx实现
  • 基本实现以后还有两个问题:误删问题和原子性问题

误删问题:

  • 如果线程1拿到锁后发生阻塞,时间过长导致锁超时释放,线程2抢到锁,此时线程1恢复,释放了线程2的锁,以此类推,线程2也会释放其他线程的锁
  • 解决:使用线程id做锁的value,存入Redis,释放锁时获取锁的线程id与当前线程做比较,一致才能释放

原子性问题:

  • 如果线程1获取锁的线程标识之后,线程发生阻塞,锁超时过期,线程2拿到锁,此时线程1恢复,用之前查到的旧值与自己的线程标识对比,一致,删除了线程2的锁
  • 原因:获取锁的线程id、比较、删除锁的过程是分多次完成的,之间无法保证原子性
  • 解决:使用Lua脚本封装该过程的逻辑,一起提交给Redis,一次执行,可以保证原子性

Lua脚本操作Redis:

  • 基本调用:
1
2
redis.call('GET', key)
redis.call('SET', key, value)
  • Redis执行Lua脚本:EVAL script numkeys key [key ...] arg [arg ...]
    • numkeys是key参数的个数,前numkeys个是key类型参数,其余是其他参数
    • script是Lua脚本,脚本中接收参数可以用,下标从1开始
      • KEYS[1]表示第一个KEY类型参数
      • ARGV[1]表示第一个其他参数

释放锁的Lua脚本:

1
2
3
4
if ARGV[1]==redis.call("get",KEYS[1]) then
return redis.call("del",KEYS[1])
end
return 0

Java通过Lua操作Redis(代码待补充):

1

3. 深度思考

  • 不同JVM下线程id可能重复,所以改为uuid+线程id做线程标识,这样可以唯一标识这个锁对象
  • uuid使用静态属性,同一JVM下线程id可以区分,不同JVM下uuid可以区分

4. 踩过的坑

5. 遗留问题 / 想不通