如何通过redis实现分布式锁

分布式锁

介绍

分布式锁是在分布式环境下,保持数据一致性的一种方案。

例如,抽奖的业务逻辑如下:

抽奖业务流程

用户A有1个积分,在抽奖时,短时间内进行了两次请求。由于请求间隔很短,在第一个请求执行积分减1之前,第二个请求检查用户积分,会返回大于1。这样导致用户使用一个积分进行了两次抽奖。

采用分布式锁的逻辑如下:

加锁后抽奖业务流程

先到的请求会将A抽奖的行为加上锁,在释放锁之前,其它的请求都无法进行抽奖操作,这样就保证了数据的一致性。

要求

一个正确的分布式锁需要同时满足以下三个条件:

  1. 互斥性。在任意时刻,只有一个进程占用锁;
  2. 不能发生死锁。即使一个进程在持有锁的期间,由于某种原因崩溃,没有主动释放锁,也需要保证其他进程能够加锁成功;
  3. 解锁和加锁的进程必须是同一个。不能释放别的进程加的锁。

redis版实现方案

分布式锁需要存储在一个所有进程都能看到的地方,通过mysql,redis,zookeeper等都可以实现。这里主要说明下如何使用redis实现分布式锁。

加锁

在加锁时,为了保证互斥性,需要检查锁是否已被其他进程占用;为了保证不发生死锁,需要给锁一个过期时间。

检查锁是否被占用可以通过setnx来实现,设置过期时间有两种方式:一是给这个lock设定过期时间,二是通过lock的值来判断。

首先来看两个错误示范:

错误示范1

res, err := redisClient.Setnx(lockKey, value); 
if err == nil && res == 1 {  
    // 假如该进程在这里退出,则无法设置过期时间,将发生死锁   
    redisClient.Expire(lockKey, expireTime) 
} 

上面的代码只能保证互斥性,不能保证不发生死锁。因为 setnx 和 expire 是分开执行的,不具备原子性。如果进程在执行完 setnx 之后,由于某种原因退出,导致没有设定锁的过期时间,就会导致死锁。锁永远无法被释放,导致其他进程无法加锁,一直阻塞,形成死锁。

错误示范2

expireTime := time.Now().Add(expire).UnixNano()   

// 如果锁不存在,则加锁成功 
res, err := redisClient.Setnx(lockKey, expireTime) 
if err == nil && res == 1 {  
    return true 
}   

// 如果锁存在,获取锁的过期时间 
lockValue := redisClient.Get(lockKey) 
if lockValue < time.Now().UnixNano() {  
    // 锁已过期,通过getset加锁(此时会有多个进程在加锁) 
    oldLockValue := reidsClient.GetSet(lockKey, expireTime) 
    if oldLockValue == lockValue { 
        // 多进程同时抢占加锁,只有第一个加锁后返回之前旧锁的值,认为加锁成功 
        return true 
    } 
}
// 其他情况,返回加锁失败 
return false

上面的代码通过lock的值来作为过期时间的判断,并通过getset来设置新的值。这里存在一个问题:多个进程在发现lock过期后,抢占加锁,虽然只有一个返回加锁成功,但是有可能lock的值被其他进程覆盖,导致在解锁时出现问题。

如果考虑到服务器时间不一致redis主从延迟的情况,这种方案的问题就更多了:

服务器时间不一致:假如A,B两个服务器时间不一致,A比B早1s(实际上不会有这么大),A加锁后1s之内,如果B进行加锁操作,都会认为A加的锁已过期,从而抢占锁;

主从延迟:为了高可用,redis通常会有主从,写操作在主上进行,读操作在从上进行。从会同步主上的数据,但是会存在一定的延迟。按照上面的方案,A,B两个进程同时进行加锁,假如A setnx成功,是在主库上进行,此时B去get,由于主从延迟,B取到的还是之前的旧值,会认为锁已经过期,从而抢占锁;

正确方式

正确的加锁方式如下:

// 生成一个唯一的token作为标识,用于解锁,而不是采用服务器时间 
token := GenUniToken() 

// 通过set的两个参数nx px,保证互斥和过期时间的原子性 
redisClient.Set(lockKey, token, "nx", "px", expireTime);

解锁

解锁时需要注意的是:需要保证解锁的进程和加锁的进程是相同的,不能删除别的进程加的锁。

错误示范1

redisClient.Del(lockKey)

暴力删除lock,不做任何判断,会导致删除别的进程的锁。

错误示范2

lockValue, err := redisClient.Get(lockKey) 
// 与加锁时的token做比较,确认是否是自己加的 
if err == nil && lockVale == token {  
    // 如果此时lockKey过期,其他进程加了锁,会被删除 
    redisClient.Del(lockKey) 
} else {  
    // 解锁失败,锁被其他进程占用,此时需要根据业务来决定如何操作,通常是rollback加锁后的操作 
}

与加锁的错误示范1类似,get和del操作没有保证原子性,导致可能会删除别的进程的锁。

正确方法

由于没有对应的redis命令可以实现get和del的原子操作,此时需要借助lua脚本来实现解锁。

// 判断get到的值是否与参数相等,相等则执行del操作,否则返回0 
delScript := `if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`
res, err := redisClient.Do("eval", delScript, 1, lockKey, token) 
if err == nil && res == 1 {  
    // 删除key,解锁成功 
    return true 
} 
return false

总结

在实现分布式锁的时候,只要保证了以下3点,就不会有问题:

  1. 互斥性;
  2. 不能发生死锁;
  3. 加锁解锁的主体要一致;

通过redis实现分布式锁,加锁时需要注意:

  1. 通过set的nx,px来保证互斥和设定过期时间的原子性;
  2. 锁的value最好采用一个唯一标识,进程保留这个标识解锁时用;

解锁时需要注意:

  1. 需要通过标识判断,锁的值是否已被修改,避免删除别人的锁;
  2. 比较操作和删除操作需要保证原子性,可以通过lua脚本实现;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容