前言

随着互联网技术的不断发展,用户量的不断增加,越来越多的业务场景需要用到分布式系统。而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,这个时候就需要使用分布式锁。

业界常用解决方案基于 MySql 等数据库的唯一索引

基于 ZooKeeper 临时有序节点

基于 Redis 的 NX EX 参数

本文主要讲解基于 Redis 实现的分布式锁

分布式锁的特点互斥性。在任意时刻,只有一个客户端能持有锁

锁超时。即使一个客户端持有锁的期间崩溃而没有主动释放锁,也需要保证后续其他客户端能够加锁成功

加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给释放了。

实现

版本一

代码

# -*- coding: utf-8 -*-

# @DateTime : 2020/3/9 15:36

# @Author : woodenrobot

import uuid

import math

import time

from redis import WatchError

def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):

"""基于 Redis 实现的分布式锁:param conn: Redis 连接:param lock_name: 锁的名称:param acquire_timeout: 获取锁的超时时间,默认 3 秒:param lock_timeout: 锁的超时时间,默认 2 秒:return:"""

identifier = str(uuid.uuid4())

lockname = f'lock:{lock_name}'

lock_timeout = int(math.ceil(lock_timeout))

end = time.time() + acquire_timeout

while time.time() < end:

# 如果不存在这个锁则加锁并设置过期时间,避免死锁

if conn.setnx(lockname, identifier):

conn.expire(lockname, lock_timeout)

return identifier

# 如果存在锁,且这个锁没有过期时间则为其设置过期时间,避免死锁

elif conn.ttl(lockname) == -1:

conn.expire(lockname, lock_timeout)

time.sleep(0.001)

return False

def release_lock(conn, lockname, identifier):

"""释放锁:param conn: Redis 连接:param lockname: 锁的名称:param identifier: 锁的标识:return:"""

# python 中 redis 事务是通过pipeline的封装实现的

with conn.pipeline() as pipe:

lockname = 'lock:' + lockname

while True:

try:

# watch 锁, multi 后如果该 key 被其他客户端改变, 事务操作会抛出 WatchError 异常

pipe.watch(lockname)

iden = pipe.get(lockname)

if iden and iden.decode('utf-8') == identifier:

# 事务开始

pipe.multi()

pipe.delete(lockname)

pipe.execute()

return True

pipe.unwatch()

break

except WatchError:

pass

return False

加锁过程首先需要为锁生成一个唯一的标识,这里使用 uuid;

然后使用 setnx 设置锁,如果该锁名之前不存在其他客户端的锁则加锁成功,接着设置锁的过期时间防止发生死锁并返回锁的唯一标示;

如果设置失败先判断一下锁名所在的锁是否有过期时间,因为 setnx 和 expire 两个命令执行不是原子性的,可能会出现加锁成功但是设置超时时间失败出现死锁。如果不存在就给锁重新设置过期时间,存在就不断循环知道加锁时间超时加锁失败。

解锁过程首先整个解锁操作需要在一个 Redis 的事务中进行;

使用 watch 监听锁,防止解锁时出现删除其他人的锁;

查询锁名所在的标识是否与本次解锁的标识相同;

如果相同则在事务中删除这个锁,如果删除过程中锁自动失效过期又被其他客户端拿到,因为设置了 watch 就会删除失败,这样就不会出现删除了其他客户端锁的情况。

版本二

如果你使用的 Redis 版本大于等于 2.6.12 版本,加锁的过程就可以进行简化。因为这个版本以后的 Redis set 操作支持 EX 和 NX 参数,是一个原子性的操作。EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。

NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。

# -*- coding: utf-8 -*-

# @DateTime : 2020/3/9 15:36

# @Author : woodenrobot

import uuid

import math

import time

from redis import WatchError

def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):

"""基于 Redis 实现的分布式锁:param conn: Redis 连接:param lock_name: 锁的名称:param acquire_timeout: 获取锁的超时时间,默认 3 秒:param lock_timeout: 锁的超时时间,默认 2 秒:return:"""

identifier = str(uuid.uuid4())

lockname = f'lock:{lock_name}'

lock_timeout = int(math.ceil(lock_timeout))

end = time.time() + acquire_timeout

while time.time() < end:

# 如果不存在这个锁则加锁并设置过期时间,避免死锁

if conn.set(lockname, identifier, ex=lock_timeout, nx=True):

return identifier

time.sleep(0.001)

return False

def release_lock(conn, lockname, identifier):

"""释放锁:param conn: Redis 连接:param lockname: 锁的名称:param identifier: 锁的标识:return:"""

# python中redis事务是通过pipeline的封装实现的

with conn.pipeline() as pipe:

lockname = 'lock:' + lockname

while True:

try:

# watch 锁, multi 后如果该 key 被其他客户端改变, 事务操作会抛出 WatchError 异常

pipe.watch(lockname)

iden = pipe.get(lockname)

if iden and iden.decode('utf-8') == identifier:

# 事务开始

pipe.multi()

pipe.delete(lockname)

pipe.execute()

return True

pipe.unwatch()

break

except WatchError:

pass

return False

版本三

可能你也发现了解锁过程在代码逻辑上稍微有点复杂,别着急,我们可以使用 Lua 脚本实现原子性操作从而简化解锁过程。

# -*- coding: utf-8 -*-

# @DateTime : 2020/3/9 15:36

# @Author : woodenrobot

import uuid

import math

import time

def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):

"""基于 Redis 实现的分布式锁:param conn: Redis 连接:param lock_name: 锁的名称:param acquire_timeout: 获取锁的超时时间,默认 3 秒:param lock_timeout: 锁的超时时间,默认 2 秒:return:"""

identifier = str(uuid.uuid4())

lockname = f'lock:{lock_name}'

lock_timeout = int(math.ceil(lock_timeout))

end = time.time() + acquire_timeout

while time.time() < end:

# 如果不存在这个锁则加锁并设置过期时间,避免死锁

if conn.set(lockname, identifier, ex=lock_timeout, nx=True):

return identifier

time.sleep(0.001)

return False

def release_lock(conn, lock_name, identifier):

"""释放锁:param conn: Redis 连接:param lockname: 锁的名称:param identifier: 锁的标识:return:"""

unlock_script = """if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])elsereturn 0end"""

lockname = f'lock:{lock_name}'

unlock = conn.register_script(unlock_script)

result = unlock(keys=[lockname], args=[identifier])

if result:

return True

else:

return False

后续

截至到目前,我们已经有较好的方法获取锁和释放锁。基于Redis单实例,假设这个单实例总是可用,这种方法已经足够安全。但是如果 Redis 主节点挂了就会出现一些问题,比如主节点加锁后没有同步到从节点,从节点升为主节点,就会出现锁的丢失。如果你想要使用更加安全的 Redis 分布式锁实现可以参考一下 Redlock 的实现。

参考

第一时间获取最新文章更新,欢迎订阅我的微信公众号:woodenrobot

python redis分布式锁_Python 使用 Redis 实现分布式锁相关推荐

  1. python连接redis有中文_Python连接Redis并操作

    首先开启redis的外连 sch01ar@ubuntu:~$ sudo vim /etc/redis/redis.conf 把bind 127.0.0.1这行注释掉 然后重启redis sudo /e ...

  2. python操作redis集群_python操作redis集群

    strictRedis对象方法用于连接redis 指定主机地址,port与服务器连接,默认db是0,redis默认数据库有16个,在配置文件中指定database 16 上代码 1.对redis的单实 ...

  3. python redis 集群_python 连接redis集群 ,常见报错解决。

    背景:工作需要,处理的数据需要通过redis进行缓存处理,之后方便统计分析. 目标:python连接redis进行读取&写入. 连接 redis 与 redis集群 是不同的 !!! 一.连接 ...

  4. python redis 订阅发布_python 实现redis订阅发布功能

    redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorted set ...

  5. python按hash分组_Python操作redis系列以 哈希(Hash)命令详解(四)

    # -*- coding: utf-8 -*- import redis #这个redis不能用,请根据自己的需要修改 r =redis.Redis(host="123.56.74.190& ...

  6. python redis模块常用_Python基础-redis模块使用

    redis是一个数据库,他的数据全都是存放在内存里面的,redis每秒能支持30w次的读写,存放有两种格式,一种string类型,一种是hash类型 一,操作string类型 r=redis.Redi ...

  7. python微博爬虫教程_Python爬虫教程-新浪微博分布式爬虫分享

    爬虫功能: 此项目实现将单机的新浪微博爬虫重构成分布式爬虫. Master机只管任务调度,不管爬数据:Slaver机只管将Request抛给Master机,需要Request的时候再从Master机拿 ...

  8. python互斥锁_Python多线程如何使用互斥锁

    为解决多线程使用全局变量的问题,引入互斥锁,修改代码如下: from threading import Thread,Lock import time num = 0 def test1(): glo ...

  9. python阻塞子线程_Python多线程中阻塞与锁使用误区

    {"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],&q ...

最新文章

  1. golang float string int 相互转换 保留小数位
  2. mac如何将本地服务暴露到外网?
  3. python中的ans是什么意思_python ans
  4. python的zip方法_python zip()函数使用方法解析
  5. OBJECT_ID()的使用方法
  6. window 系统上传文件到linux 系统出现dos 格式换行符
  7. 详细解说五个Java项目
  8. 6.4信号灯(Semaphores)
  9. 干货文:企业 IT 基础架构|(精华篇)
  10. mt4怎么用云服务器跟单,免费好用的跟单系统 神速MT4跟单ea系统使用教程
  11. 新股发行制度五年改革历程
  12. 想进大公司先测你EQ
  13. cocos creator3.3.0休闲游戏(云浮消消乐)源码H5+安卓+IOS三端源码
  14. 中文文本分类语料库-TanCorpV1.0
  15. 【windows8开发】开发平台与开发框架
  16. Unity3D基础案例-双人坦克
  17. 13.6.3 程序案例:BLE低功耗蓝牙调试助手
  18. 影响IT人员未来发展的五个IT新技术方向
  19. stm32mcuid规则_STM32型号命名规则
  20. [object] 与 [object object] 区别!

热门文章

  1. 连载四:Oracle升级文章大全(完结篇)
  2. 《数据安全警示录》一书修订版出版
  3. Oracle DBA必须学会的11个Linux基本命令
  4. JerryScript:物联网开发者的得力工具
  5. 从源码角度解析线程池中顶层接口和抽象类
  6. SARIF在应用过程中对深层次需求的实现
  7. collections 使用教程
  8. java oscache 缓存_Java]用OSCache进行缓存对象
  9. idea database 添加字段不更新_如何借助IDEA数据库管理工具可视化使用TDengine?
  10. SpringCloud与zuul