最近在工作中用到了分布式锁,然后查了很多分布式锁的实现方式。比较熟悉redis或者说,redis的用法比较简单,所以查了一下redis使用setnx实现分布式锁的方式。其中有一篇文章搜索到的次数最多,多到我不知道哪个是原创文章,就贴一下看到的链接吧https://blog.csdn.net/lihao21/article/details/49104695。

reids > setnx(key,value)  //设置key.redis > delete(key)  //将key删除

当key存在的时候,设置会失败,返回-1。当key不存在的时候,设置成功,返回0。

实现方式一:

lock_key = "distribute_lock"
def get_lock():while True:lock = redis_client.setnx(lock_key,1)if lock: # 设置成功breakelse:time.sleep(0.5)  #如果没有获取成功,等待0.5继续获取,当然这个地方你也可以设置重试次数。return True
if get_lock():# do your work,处理临界资源redis_client.delete(lock_key) //为防止死锁,处理完临界资源要及时释放锁。

可以看一下这个代码有什么问题?

问题出在了,最后锁的释放,假如在处理临界资源的过程中,进程挂掉了或者在执行删除操作的时候redis链接断掉了,那么这个分布式锁将永远得不到释放,进而产生死锁。所以接下来的优化是如果进程挂掉了,能够及时的释放锁,你想到了什么?超时机制。

实现方式二:

为了避免出现方式一的问题,所以加入了超时机制。

setnx(key, <current Unix time + lock timeout + 1>)

本质就是将key的value值设置成一个超时时间,按照方式一流程:

lock_key = "distribute_lock"
def get_lock():while True:lock = redis_client.setnx(lock_key,<now_time+ lock timeout + 1>) # 直接设置成功if lock: # 设置成功breaknow_time = <current Unix time + lock timeout + 1>lock_time_out = redis.client.get(lock_key)if now_time > lock_time_out: #检查是否超时# 如果已经超时redis_client.delete(lock_key) # 将这个key删除,进行重新设置。lock = redis_client.setnx(lock_key,<now_time+ lock timeout + 1>)if lock: # 设置成功breakelse:time.sleep(0.5)  #如果没有获取成功,等待0.5继续获取,当然这个地方你也可以设置重试次数。return True
if get_lock():# do your work,处理临界资源redis_client.delete(lock_key) //为防止死锁,处理完临界资源要及时释放锁。

继续思考一下,这又会出现什么问题?

  1. 假如进程p1获取到了锁,同时进程p2和p3在不断的检测锁是否已经超时。
  2. 然后p1进程挂掉了,没有及时删除锁。
  3. 过了一段时间,p2和p3同时检测到了这个锁已经超时,即程序中now_time > lock_time_out都成立。
  4. 首先p2将锁删除了,然后将锁设置了超时时间,致此p2获取到了锁。
  5. p3也检测到了锁超时了,只不过它执行速度比慢,直接将锁删除了,实际上p3删除的是p2设置的锁,这个时候问题就出现了,p3和p2都获取到了锁。

实现方式三

实现方式二的关键问题是什么?是p3在删除锁的时候,没有检查是否又有新的进程获取到了该锁。
为了避免这种情况,p3在执行set操作的时候,用这个命令:

getset(lock_key,now_time+lock timeout + 1)

这个命令会返回旧,然后设置成新的值。在set之前判断一下当前时间是否大于lock_key的旧值。如果大于,说明已经超时,获取到了锁。假如再次出现上面的情况,p2和p3同时检测到锁的超时,然后p2删除、并获取到了锁。p3执行getset操作,然后当前时间和lock_key的旧值(p2设置的)比较,当前时间小于旧值。获取锁失败,继续下一轮的等待。

其次最后删除的时候,也不能像前几次一样直接删除。要先判断一下,当前时间小于锁的超时时间的话在删除。避免删除其他进程设置的锁。程序如下:

def get_lock():LOCK_TIMEOUT = 3lock = 0lock_timeout = 0lock_key = 'distribute_lock'# 获取锁while lock != 1:now = int(time.time())lock_timeout = now + LOCK_TIMEOUT + 1lock = redis_client.setnx(lock_key, lock_timeout)if lock == 1 or (now > int(redis_client.get(lock_key))) and now > int(redis_client.getset(lock_key, lock_timeout)):breakelse:time.sleep(0.001)
if get_lock():# dou your lock.处理临界资源()# 释放锁now = int(time.time())if now < lock_timeout:redis_client.delete(lock_key)

继续看,有什么问题?

  1. 为什么要+1 current Unix time + lock timeout + 1 ?
    +1是因为在 Redis 2.4 版本中,过期时间的延迟在 1 秒钟之内 —— 也即是,就算 key 已经过期,但它还是可能在过期之后一秒钟之内被访问到,而在新的 Redis 2.6 版本中,延迟被降低到 1 毫秒之内。

  2. 这段代码问题在哪里?1) 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2)当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3) 锁不具备拥有者标识,即任何客户端都可以解锁。

(1)首先第一个问题是存在的,但是一般对于同一个分布群的项目,时间肯定是同步的,而且你见过哪台机器时间是用本地时间的?一般都是联网同步互联网全球设定的时区时间。 (2)Redis是单线程的,所以不存在你说的这种情况。是不可能同时执行的。即使客户端A,B,C 在同一时刻(精确到纳秒)发送了getset给redis。redis也会按照队列顺序依次执行。因此绝对保证只有一个客户端获得锁并进行业务处理最后释放锁,其他客户端一定会返回一个大于当前时间的结果,从而导致或许锁失败,也就不可能出现其他任何客户端能够解锁。你可能会说,“同时”。但是你说“同时”的时候,我就知道,兄弟你对redis的底层没有了解到这一层面,redis是强制单线程的。除非你改人家源码,就算你改人家源码,如果你改了人家的单线程设计理念,就等于说你不赞同redis作者的理念。然而,redis的单线程,却没有影响它的高性能。

  1. 实践中我发现的问题.
if lock == 1 or (now > int(redis_client.get(lock_key))) and now > int(redis_client.getset(lock_key, lock_timeout))

会出现bug,因为当锁被删除之后,如果正好处于redis_client.get(lock_key)检查的时候,这个时候锁被删除了,所以redis_client.get(lock_key)为None,int(None)会出现类型转换错误。
然后这个时候如果为None的话,可以直接赋值为0,这样也不会出现问题,因为本身这个锁就被删除了,和时间超时是一样的,所以提出了优化。

if lock == 1 or (now > int(redis_client.get(lock_key) or 0)) and now > int(redis_client.getset(lock_key, lock_timeout) or 0)

其次为了便于使用,我还优化成了一个类:

class DistributeLock(object):def __init__(self, lock_key=None, lock_timeout=2):""":param lock_key: 分布式锁的key:param lock_timeout: 锁的超时时间"""self.LOCK_TIMEOUT = lock_timeoutself.lock = 0self.lock_timeout = 0self.lock_key = lock_key  #  # 分布式锁的keyself.redis_client = #初始化redis客户端。def __enter__(self):# 获取锁while self.lock != 1:now = int(time.time())self.lock_timeout = now + self.LOCK_TIMEOUT + 1lock = self.redis_client.setnx(self.lock_key, self.lock_timeout)if lock == 1 or (now > int(self.redis_client.get(self.lock_key) or 0)) and now > int(self.redis_client.getset(self.lock_key, self.lock_timeout) or 0):breakelse:time.sleep(0.001)def __exit__(self, ex_type, ex_value, traceback):# 释放锁now = int(time.time())if now < self.lock_timeout:self.redis_client.delete(self.lock_key)
# 使用方式:with DistributeLock(lock_key="your_distribute_lock",lock_timeout=3):# do your work.do_work() # 不用在关心锁的获取或者释放。

其实以上,还有有些问题:就是没有考虑redis挂掉或者主从切换的情况,后续再更新。

关注我,让我们一起成长。

Redis使用setnx实现分布式锁及其问题、优化相关推荐

  1. SpringBoot整合redis使用setnx完成分布式锁

    spring boot 版本2.2.0 pom依赖 <dependency><groupId>org.springframework.boot</groupId>& ...

  2. Redis:基于SETNX解决分布式锁误删问题

    Redis:SETNX解决分布式锁误删问题 一.概述 二. 分布式锁(初级) (1)锁接口 (2)锁实现类+上锁 (3)释放锁 (4)存在的问题 三. 改进释放锁 (1)准备unlock.lua脚本 ...

  3. redisson redlock(基于redisson框架和redis集群使用分布式锁)

    一.关于分布式锁的两篇文章 文章1 文章2 二.redis分布式锁存在的问题 redis实现分布式锁有很多种方案,比较完善的方案应该是用setNx + lua进行实现.简单实现如下: java代码-加 ...

  4. Redis与Zookeeper实现分布式锁的区别

    Redis与Zookeeper实现分布式锁的区别 1.分布式锁解决方案 1.采用数据库 不建议 性能不好 jdbc 2.基于Redis实现分布式锁(setnx)setnx也可以存入key,如果存入ke ...

  5. 在 Redis 上实现的分布式锁

    由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用 ...

  6. redis set 超时_redis分布式锁3种实现方式对比分析总结

    我在这篇文章提到了分布式锁,但没有展开来讲,抛砖引玉,今天就来说说高并发服务编程中的redis分布式锁. 这里罗列出3种redis实现的分布式锁,并分别对比说明各自特点. Redis单实例分布式锁 实 ...

  7. Redis应用学习——Redis事务与实现分布式锁

    2019独角兽企业重金招聘Python工程师标准>>> 1. Redis事务机制 1. 与MySQL等关系数据库相同,Redis中也有事务机制,Redis的事务实质上是命令的集合,但 ...

  8. 图解分析:基于setnx的分布式锁有什么缺陷

    spring boot 和redis集成 图解分析:基于setnx的分布式锁有什么缺陷? 基于setnx的分布式锁存在单点风险,如果存储的分布式锁key挂掉的话,就可能存在丢锁的风险.一旦丢锁,就会造 ...

  9. Day137-139.尚品汇:制作SKU、商品详情、项目优化:Redis缓存、redssion分布式锁

    目录 Day5  制作SKU 1. 制作SKU 2. 多表查询如何写? 3. 制作SKU 4. Thymeleaf Day06 商品详情 1. 获取分类信息 2. 获取最新价格信息 3. 获取销售信息 ...

最新文章

  1. mysql有关权限的表都有哪几个
  2. Android开发精要2--Android组件模型解析
  3. 号外号外!RancherOS v1.2.0发布啦!
  4. ICCV 2017 《Towards End-to-End Text Spotting with Convolutional Recurrent Neural Network》论文笔记
  5. C算法--入门 2.3
  6. python configparser 注释_使用configpar添加注释
  7. #191 sea(动态规划)
  8. mysql for windows_mysql安装教程-windows版
  9. pdsh命令控制多台树莓派超爽
  10. javascript的数组形式与php的数组形式
  11. mysql 重置表索引_第19期:索引设计(哈希索引数据分布与使用场景)
  12. 新版白话空间统计(11):ArcGIS中的PZ值标尺
  13. Python基础:pip的安装与卸载
  14. html语言字体大小修改,html怎么修改字体大小
  15. 迪文屏中关于MODBUS的接口配置
  16. 《人生七年》纪录片-个体心理学中的自卑与超越角度解读
  17. 物品分类游戏html5,物品的用途分类教案
  18. OpenMP 参考(指令详解)
  19. 怎么查看自己的笔记本电脑系统型号以及其他配置(DXDIAG打开DirectX诊断配置)
  20. 双11直播技术强力后盾——阿里云导播服务功能详解与场景应用

热门文章

  1. thingsboard-部件库开发 之 rpc-部件开发
  2. 全球搜索引擎排名百度第三
  3. python中repeat_Python Pandas Series.repeat()用法及代码示例
  4. 如何过滤掉xml字符串中的gt,lt,quot,amp,apos
  5. C617 redhat smicmmrf 工艺库安装 乞丐版
  6. 天正当前比例怎么设置_天正模型空间怎么设置(天正7图纸空间中如何改变当前比例)...
  7. Ubuntu 20.04.2.0 LTS 更改默认关联视频播放器VLC的方法
  8. Vue style里面使用scoped属性并@import引入外部css, 作用域是全局的解决方案
  9. 爱立信也扛不住了?员工变外包,不接受拿N+1赔偿
  10. 高德地图在打包apk后部分页面无法正常定位解决办法