使用redis设计一个简单的分布式锁

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 原文:使用redis设计一个简单的分布式锁最近看了有关redis的一些东西,了解了redis的一下命令,就记录一下: redis中的setnx命令: 关于redis的操作命令,我们一般会使用set,get等一系列操作,数据结构也有很多,这里我们使用最简单的string来存储锁。
+关注继续查看
原文:使用redis设计一个简单的分布式锁

最近看了有关redis的一些东西,了解了redis的一下命令,就记录一下:

redis中的setnx命令:

关于redis的操作命令,我们一般会使用set,get等一系列操作,数据结构也有很多,这里我们使用最简单的string来存储锁。

redis下提供一个setnx命令用来将key值设为value,类似于set功能,但是它和set是有区别的,在于后面的nx,setnx是SET if Not eXists。就是:当且仅当key值不存在的时候,将该key值设置为value。

也就是说使用setnx加入元素的时候,当key值存在的时候,是没有办法加入内容的。

加锁:

下面将使用python控制redis,python控制redis的方式和命令一样,有一个setnx(key, value)方法,通过这个方法可以实现setnx命令的效果。

首先连接redis:

文件connect.py

1 import redis
2 
3 def connect_redis():
4     return redis.Redis(host='127.0.0.1', port=6379)

然后就可以使用setnx加锁了,key对应的value中需要填入相应的值,这里设置Value为一个uuid值。获取到uuid之后,将key和value填入redis,其他的客户端想要访问并获取到锁,也使用setnx方式,key值只要相同就好。那么代码如下:

文件operate.py

1 # 加锁的过程
2 def acquire_lock(conn, lockname):
3     identifier = str(uuid.uuid4())
4     conn.setnx('lock:' + lockname, identifier): 

这样就通过setnx将key值写入了。但是,这样显然是不合理的,客户端可以等待一会再次获取锁,这样,不至于每次请求都会出现问题。那可以设置30秒的时间,让程序在30秒内不停的尝试获取锁,知道30秒的时间过了或者由其他客户端释放了锁。

所以加锁可以变为如下:

 1 # 加锁的过程
 2 def acquire_lock(conn, lockname, args, acquite_timeout = 30):
 3     identifier = str(uuid.uuid4())
 4     end = time.time() + acquite_timeout
 5     while time.time() < end:
 6         # 这里尝试取得锁 setnx 设置-如果不存在的时候才会set
 7         if conn.setnx('lock:' + lockname, identifier): 
 8             # 获得锁之后输出获得锁的‘进程’号
 9             print('获得锁:进程'+ str(args))
10             return identifier
11     return False

这样就可以通过setnx加锁了,这个加锁的过程实际上就是在redis中存入了一个值,之后当其他的客户端再次想要通过这个key加入这个值的时候,发现这个key已经存在就不往进写值了,但是在这30秒内还会不断尝试的去获取锁,也就是不断的尝试写入这个值,一旦key被删除——也就是锁被释放,则该客户端就可以竞争获取锁——进程写入这个值。这种方式就像是操作系统中的自旋锁。

自旋锁

这里先简单介绍一下自旋锁。

和自旋锁对应的还有一种锁,叫做对于互斥锁。

互斥锁:如果资源已经被占用,资源申请者只能进入睡眠状态。

自旋锁:自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

释放锁:

既然锁可以添加,那么也就可以释放。释放锁实际上就是将redis中的数据删除。这里可以使用redis提供的事务流水线去执行。为了保障在执行的时候确确实实释放的所示没有问题的。

代码如下,调用delete()方法——del命令删除redis中这个key的元素。

 1 # 释放锁
 2 def release_lock(conn, lockname, identifier):
 3     pipe = conn.pipeline(True)
 4     lockname = 'lock:' + lockname
 5     while True:
 6         try:
 7             pipe.watch(lockname)
 8             identifier_real = pipe.get(lockname)
 9             if identifier_real == identifier:
10                 pipe.multi()
11                 pipe.delete(lockname)
12                 pipe.execute()
13                 return True;
14             pipe.unwatch()
15             break
16         except redis.exceptions.WatchError:
17             pass
18     return False

这里就遇到了python3中的一个坑,获取到的数据为byte类型,所以identifier_real这个变量实际上是这个字符串之前加了个b的,例如b'xxxx',所以这和传入的identifier的类型为string,这样比较当然会出现Fasle,所以我们在这里将这个字符串类型转码:

1 pipe.get(lockname).decode()

这样才是整整的字符串类型的字符串,最终的代码如下:

 1 def release_lock(conn, lockname, identifier):
 2     pipe = conn.pipeline(True)
 3     lockname = 'lock:' + lockname
 4     while True:
 5         try:
 6             pipe.watch(lockname)
 7             identifier_real = pipe.get(lockname).decode()
 8             if identifier_real == identifier:
 9                 pipe.multi()
10                 pipe.delete(lockname)
11                 pipe.execute()
12                 return True;
13             pipe.unwatch()
14             break
15         except redis.exceptions.WatchError:
16             pass
17     return False

执行:

为了验证这个分布式锁的正确性,可以写一个测试的方法进行测试,先去获取锁,如果获取到之后,则sleep三秒,等待3秒之后,执行释放锁。

模拟过程入下:

文件operate.py

1 # 模拟加锁解锁的过程
2 def exec_test(conn, lockname, args):
3     id = acquire_lock(conn, lockname, args)
4     if id != False:
5         print(id)
6         time.sleep(3)
7         release_lock(conn, lockname, id)

这样我们就可以使用多进程并发访问的方式进行对锁进行抢占和释放:

使用9个进程进行测试,操作方式如下:

1 from connect import connect_redis
2 from operate import acquire_lock
3 from multiprocessing import Process
4 from operate import exec_test
5 
6 if __name__ == '__main__':
7     redis = connect_redis()
8     for i in range(0, 9):
9         Process(target = exec_test, args = (redis, 'test', i)).start()

执行结果如下所示:

注:以上python运行环境为python3.6

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
12月前
|
存储 缓存 负载均衡
Redis cluster去中心化设计的思考与总结
Redis cluster去中心化设计的思考与总结
171 0
|
存储 缓存 NoSQL
详细设计- Redis 设计|学习笔记
快速学习详细设计- Redis 设计
86 0
详细设计- Redis 设计|学习笔记
|
NoSQL Redis
【Redis系列】为啥Redis Cluster设计成16384个槽?
这意味着它们包含原始形式的节点的插槽配置,该节点使用2K的空间和16384个slot,但使用65535的插槽会使用令人望而却步的 8K 的空间。所以16384是在正确的范围内,以确保每个 master 有足够的插槽,最多 1000 个 maters,但这个数量足够小,可以轻松地将插槽配置作为原始位图传播。在小集群中,位图将难以压缩,因为当 N 小时,位图将设置的槽位/N 位占很大比例的位。集群节点越多,心跳包的消息体内携带的数据越多。集群规模较小的场景下,每个分片负责大量的slot,很难压缩。
130 0
【Redis系列】为啥Redis Cluster设计成16384个槽?
|
存储 缓存 运维
|
NoSQL Unix Linux
通过Redis学习事件驱动设计
通过Redis学习事件驱动设计
124 0
通过Redis学习事件驱动设计
|
存储 缓存 NoSQL
Redis点赞业务的设计与实现(Redis键值设计)
案例分享Redis点赞业务实现!
1036 2
Redis点赞业务的设计与实现(Redis键值设计)
|
存储 NoSQL 算法
阿里面试这样问:redis 为什么把简单的字符串设计成 SDS?
阿里面试这样问:redis 为什么把简单的字符串设计成 SDS?
119 0
阿里面试这样问:redis 为什么把简单的字符串设计成 SDS?
|
NoSQL Unix Redis
Redis 中的持久化技术《Redis设计与实现》
本篇将介绍 Redis 中的持久化技术,主要有两种: RDB持久化 和 AOF持久化
102 0
Redis 中的持久化技术《Redis设计与实现》
|
存储 JSON NoSQL
Spring之借助Redis设计一个简单访问计数器
为什么要做一个访问计数?之前的个人博客用得是卜算子做站点访问计数,用起来挺好,但出现较多次的响应很慢,再其次就是个人博客实在是访问太少,数据不好看😢... 前面一篇博文简单介绍了Spring中的RedisTemplate的配置与使用,那么这篇算是一个简单的应用case了,主要基于Redis的计数器来实现统计
281 0
Spring之借助Redis设计一个简单访问计数器
|
缓存 NoSQL Java
Redis 缓存设计
本文介绍Redis 缓存设计:穿透优化、无底洞优化、雪崩优化、热点key 重建优化
推荐文章
更多