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 | redis.call('GET', key) |
- Redis执行Lua脚本:
EVAL script numkeys key [key ...] arg [arg ...]- numkeys是key参数的个数,前numkeys个是key类型参数,其余是其他参数
- script是Lua脚本,脚本中接收参数可以用,下标从1开始
- KEYS[1]表示第一个KEY类型参数
- ARGV[1]表示第一个其他参数
释放锁的Lua脚本:
1 | if ARGV[1]==redis.call("get",KEYS[1]) then |
Java通过Lua操作Redis(代码待补充):
1 |
3. 深度思考
- 不同JVM下线程id可能重复,所以改为uuid+线程id做线程标识,这样可以唯一标识这个锁对象
- uuid使用静态属性,同一JVM下线程id可以区分,不同JVM下uuid可以区分