前面学习了Redis的数据结构以及命令、Redis中的事务和Redis对Lua脚本的支持。
这一章就对Redis这些特性做一下实战性应用——基于Redis的分布式锁实现。
Lock和Distributed Lock
在这之前先来认识下锁(Lock)和分布式锁(Distributed Lock):
In computer science, a lock or mutex (from mutual exclusion) is a synchronization mechanism for enforcing limits on access to a resource in an environment where there are many threads of execution. A lock is designed to enforce a mutual exclusion concurrency control policy.
参考wiki的解释:在计算机科学领域中,锁是为了限制多线程环境访问一个资源的一种同步机制。锁被设计相互排斥的并发策略。
Lock的前提条件:
- 同一台机器上的共同资源;
- 多线程环境访问共同资源;
Lock目标:
- 保证多线程访问资源的一致性;
Lock的实现:
- 单核处理器上禁用中断,使得同步资源能够被访问结束;
- 硬件支持的原子指令,“比较和交换”等等,用于测试锁是否是空闲,如果空闲获取锁;
Operating systems use lock managers to organise and serialise the access to resources. A distributed lock manager (DLM) runs in every machine in a cluster, with an identical copy of a cluster-wide lock database. In this way a DLM provides software applications which are distributed across a cluster on multiple machines with a means to synchronize their accesses to shared resources.
参考wiki解释分布式锁:操作系统用锁管理器实现有组织有顺序的访问资源。分布式锁运行在集群环境中的每台机器上,使得数据具有相同的副本。分布式锁提供分布式软件应用同步访问共享资源。
Distribute Lock的前提条件:
- 分布式软件应用;
- 分布式软件中的共享资源;
Distribute Lock目标:
- 保证分布式应用访问共享资源的一致性
Distribute Lock实现方式:
- 基于Redis实现;
- 基于Zookeeper实现;
- 基于Etcd或者Consul实现;
- Google开发的Chubby(Lock Service);
独占式锁的特点和影响
按照用途、场景划分,锁的类型非常多。如:排它锁(独占式锁)、共享锁,自旋锁、互斥锁,读锁、写锁。但是在分布式环境中的所谓的分布式锁,大多数情况下都是指:分布式独占式锁。
1.特点分析:
- 每次只能一个占用锁;
- 可以重复进入锁;
- 只有占用者才可以解锁;
- 获取锁和释放锁都需要原子
- 不能产生死锁
- 尽量满足性能
本质:同步互斥,使得处理任务能够一个一个逐步的过临界资源。
造成的影响:
- 降低并发数,使得多任务处理,只能一个一个的进行;
- 任务的换进换出造成切换上的开销;
本质:使得吞吐量大打折扣。
基于Redis的实现
Redis实现分布式锁的基础
1.Redis本身就是单线程:
- 单个命令执行具有原子性、无竞态条件,这个特点符合一次只有一个客户端争用锁;
2.Redis提供了set if not exists操作:
- 存在即不设置,这个特点符合锁的独占性(排它特点);
下面来先来看下获取锁:
return jedis.set(lockKey, lockValue, NX, EX, expireTime) != null ? true : false;
这里使用set指令,具有原子操作特点,不会被其他客户端操作中断,在分布式环境中,是安全的,没有竞态条件产生,一次只能有一个客户端争用锁;使用nx,即存在不设置,符合独占特点;设置ex,有过期效果,不会产生永久独占即死锁;最后设置了lockValue,这样就和当前加锁任务做了绑定,后面可以用其作为解锁的钥匙;
再来看下解锁操作:
static final String RELEASE_LOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(RELEASE_LOCK_LUA, 1, lockKey, lockValue);
这里解锁是用了Lua脚本,上篇文章中介绍了Redis内置一个Lua解释器,Redis调用解释器执行Lua脚本也是具有原子性的,即同一时刻只有一个客户端的操作能被执行,所以这里使用Lua脚本解锁无竞态条件;解锁符合是占用锁的任务释放的原理;
但是以上实现的分布式锁缺点是:
- 不具有重入性,即当前任务获取了锁,在下次获取时将会死锁;
- 不能自旋获取,即获取失败时,将会立即返回失败;
本人对其进行了改造,分别做了适应以上两种场景的分布式锁,详情可以戮[Distributed Lock],欢迎大家一起来完善。
总结
本文从What、Features、How的角度分析了分布式锁。总的来说,单机应用中的多线程或者多进程的锁的放大版基本上就是分布式锁了。万变不离其宗,实现独占锁的关键性要素:
- 目标:互斥同步,资源访问原子化;
- 实现:一次只能有一个争用到锁,争用过程是个原子过程,只能争用到的解锁,不会发生死锁;
参考
Redis 分布式锁的正确实现方式
Rewriting our lock
Lock
题外话
参考Rewriting our lock中使用setnx实现的分布式,严格意义上来说是有死锁问题的。setnx和expire不具有原子性。当setnx成功后,expire前应用发生宕机,这会导致锁永远不会过期,别的应用始终争用不到锁。当然这种情况比较特殊,但是做代码是一件严谨的事!