- 用 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算法>_<
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
互联网的误读
詹姆斯•柯兰(James Curran)、娜塔莉•芬顿(Natalie Fenton)、德 斯•弗里德曼(Des Freedman) / 何道宽 / 中国人民大学出版社 / 2014-7-1 / 45.00
互联网的发展蔚为壮观。如今,全球的互联网用户达到20亿之众,约占世界人口的30%。这无疑是一个新的现象,对于当代各国的经济、政治和社会生活意义重大。有关互联网的大量大众读物和学术著作鼓吹其潜力将从根本上被重新认识,这在20世纪90年代中期一片唱好时表现尤甚,那时许多论者都对互联网敬畏三分,惊叹有加。虽然敬畏和惊叹可能已成过去,然而它背后的技术中心主义——相信技术决定结果——却阴魂不散,与之伴生的则......一起来看看 《互联网的误读》 这本书的介绍吧!