- 用 Redis 作为分布式锁是个轻量级的解决方案, 但很多同学在使用过程中并未弄明白其中的优劣, 知其然不知其所以然, 反而引入了一些那一排查的线上故障.
- 这里针对Redis分布式锁常用的几种方式, 从原理触发, 分析其适用场景及潜在缺陷
先回顾一下LESLIE LAMPORT大神在其1977年论文 Proving the Correctness of Multiprocess Programs 中对于分布式系统正确性的定义
Correctness == Safety and liveness
- Safety (安全性)
safety properties informally require that "something bad will never happen" in a distributed system or distributed algorithm - Liveness (活性)
liveness properties refers to a set of properties of concurrent systems, that require a system to make progress despite the fact that its concurrently executing components ("processes") may have to "take turns" in critical sections , parts of the program that cannot be simultaneously run by multiple processes
知乎大神翻译了数学证明, 有兴趣的同学可以自取
对于使用分布式锁的正确性, 我们不妨如下界定
- Safety
同一时间只有一个进程可以获得锁
没有获得锁的其他进程应该被正确的置为获取锁失败的状态 - Liveness
获得锁的进程最终应该释放锁, 让其他进程可以再次尝试获取
下面从三个阶段来分析Reids作为分布式锁的用法
- 锁的获取;
- 锁的持有;
- 锁的释放;
先看锁的获取
// redis version < 2.6.12 SETNX lock_key lock_value EXPIRE lock_key ttl_with_seconds
// redis version >= 2.6.12 SET lock_key lock_value NX PX ttl_with_seconds
Redis的SETNX可以保证只有第一次设置可以成功, 那么获取锁的Safety是可以保证的
但是在2.6.12之前SETNX和EXPIRE是两条命令, 这样会存在如下情况:
Command | Status |
---|---|
SETNX lock_key lock_value | 执行成功 |
EXPIRE lock_key ttl_with_seconds | 发送失败&Client Crash |
会导致lock_key无法正确释放, 从而不能满足Liveness
问题有了, 如何解决?
- Redis2.6.12之前版本可以采用 lua 脚本将命令一次提交, 保证操作原子性
- 升级到2.6.12之后版本
锁的持有比较复杂, 我们先来看锁的释放
野狐禅版本, golang示例
func AcquireLock(lock_key string, lock_value string, timeout uint32) bool { // SET lock_key lock_value NX PX ttl_with_seconds // return is_success } func ReleaseLock(lock_key string) { // DELETE lock_key } func Process(lock_key string, lock_value string, timeout uint32) { if AcquireLock(lock_key, lock_value, timeout) { // 无论业务逻辑执行是否成功, 一定释放锁 defer func() { ReleaseLock(lock_key) }() // do something // maybe process over lock TTL } }
看上去很完美的实现. But Really Good Job?
惨案是如何发生的
来看个正规军版本
func ReleaseLock(lock_key string, lock_value string) bool { // 在释放锁的时候加入乐观锁校验, 并通过lua脚本保证原子性 // return_val = eval ( // if redis.call("get",lock_key) == lock_value then // return redis.call("del",lock_key) // else // return 0 // end // ) return return_val!=0 } func Process(lock_key string, lock_value string, timeout uint32) { if AcquireLock(lock_key, lock_value, timeout) { // 无论业务逻辑执行是否成功, 一定释放锁 defer func() { release_success := ReleaseLock(lock_key) if !release_success { // 如果锁释放失败, 说明锁超时, 其他人已经获取了锁, 需要根据业务决定是否rollback刚刚的操作 // maybe rollback?? } }() // do something // maybe process over lock TTL } }
最后需要注意的事情
由于使用了lock_value作为释放锁时的乐观校验, 那么lock_value的选择就需要一定的技巧
值的生成 | 优点 | 缺点 |
---|---|---|
系统时间戳 | 程序实现简单 | 分布式环境下ntp时钟并不同步, 有概率碰撞 |
ID服务获取 | 全局保证唯一 | 引入了外部服务依赖, 降低健壮性 |
分布式ID算法 | 全局保证唯一 | 有一定技术门槛 |
这里推荐第三种方案, 可参考twitter的snowflake算法
顺便鄙视一下某些无良码农, 仅仅是对snowflake算法的位进行调整, 就人模狗样的跑出来说实现了nb的分布式ID算法>_<
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Shallows
Nicholas Carr / W. W. Norton & Company / 2011-6-6 / USD 15.95
"Is Google making us stupid?" When Nicholas Carr posed that question, in a celebrated Atlantic Monthly cover story, he tapped into a well of anxiety about how the Internet is changing us. He also crys......一起来看看 《The Shallows》 这本书的介绍吧!