面试官:讲一下你对分布式锁的理解,以及使用场景。
回答开始:
先看图
首先我们要聊下一下本地锁,本地锁只能在同一个进程中,避免多个线程(所有线程共享进程的内存)对于进程中同一个资源的并发访问,如果是单机环境,本地锁就可以满足。
但是在分布式环境中,本地锁无法保证多个进程之前的资源争夺,于是就需要使用分布式锁。
常见的实现方案:
-
利用数据库主键的唯一特性,单独创建一张表,每次insert id=1 的记录,插入成功即抢占,失败说明正在被使用,释放锁就是删除记录。
-
redis 的setNx命令,SETNX是 set If not exist的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。释放锁就是del key。
-
zookeeper,创建一个有序的临时节点来实现。
数据库的性能差,zookeeper用的则比较少维护成本高,redis便成为了最常用的方案,主要讨论下redis的分布式锁实现。
Redis分布式锁
根据上述使用 setNx 命令抢占锁,执行成功后,del 删除来释放锁,从技术的角度看:setnx 占锁成功,业务代码出现异常或者服务器宕机,没有执行删除锁的逻辑,就造成了死锁。
1、那如何规避死锁这个风险呢?
设置锁的自动过期时间,过一段时间后,自动删除锁,这样其他线程就能获取到锁了。
但是又出现了新的问题,过期时间到期后,任务还没执行完,便会有问题。
2、如何保证抢占锁和设置超时时间的原子性呢?
Redis 正好支持这种操作:设置某个 key 的值并设置多少毫秒或秒 过期。
set key value PX <多少毫秒> NX 或 set key value EX <多少秒> NX
3、如果提前过期删除了其他线程的锁怎么办?
这个问题还是要提一下,不过并没有解决提前过期的问题。可以给锁的值value设置为当前线程ID,删除时比较,如果是本线程抢占的锁,才可以执行删除,可以用LUA脚本,来保证 获取锁的值、删除锁 这两步的原子性。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
一般来说只要把锁的过期时间设置远远大于业务代码的执行时间,便可以支持99.99%的场景,但是在系统异常、STW、GC时,进程阻塞的场景,虽然极少出现,这时无法保证业务代码的执行时间。
4、如果防止锁提前过期?
终极方案
使用如 Redission(java)、Redsync(go)等开源框架,实现了锁续期、保证原子性等。
redis官方推荐的分布式锁实现:redis.cn/topics/dist…