python使用redis实现协同控制的分布式锁

python

python使用redis实现协同控制的分布式锁

上午的时候,有个腾讯的朋友问我,关于用zookeeper分布式锁的设计,他的需求其实很简单,就是节点之间的协同合作。 我以前用redis写过一个网络锁,趁着这个机会就分享了出去。 

其实核心的代码就那几行,就是借用redis setnx来原子性的加一个锁,然后用expire来控制过期时间。

注: setnx跟set的区别在于,setnx是原子性的操作,用set会出现一个问题,比如我先get key看看key是否存在,当我再次去判断的时候,有可能别人把这个key给配置了,这就成了非原子操作。 

在介绍下这个用redis实现的分布式锁,他还含有retry和timetout的功能。

麻痹的爬虫呀,哎,说明下原文地址是,http://xiaorui.cc

*   用with做锁的逻辑语句

*   timeout避免了避免了任务出现异常,没有做delete操作

*   对于长时间的业务,增加retry重试锁的次数

*   对于长时间的业务,增加retry重试锁的次数

 

1

2

3

4

5

6

7

8

9

10

11

#coding:utf-8

#my blog: http://xiaorui.cc

with_statement

redis

time

dist_lock

 

)

:

)

'welcome to my blog, http://xiaorui.cc'


同时运行test.py and test2.py

python test.py

 

1

2

# python test.py

//xiaorui.cc

python test2.py

 

1

2

3

4

5

6

# python test2.py

0.404031367603

1.19347232007

2.47668271021

4.55280708824

hello

已经把redis_netlock提交到了pypi项目里面。 

 

1

redis_netlock


下面是redis_netlock的git地址。

1

//github.com/rfyiamcool/redis_netlock.git


对Golang感兴趣的朋友可以加群: 278517979 !!!
另外如果大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码, 感谢!
另外再次标注博客原地址  xiaorui.cc

 

 

 

分布式锁一般有三种实现方式:

1. 数据库乐观锁;

2. 基于ZooKeeper的分布式锁;

3. 基于Redis的分布式锁;

这里大概说一下三种方式的优缺点,数据库乐观锁优点是实现简单,只需要for update关键词就可以实现,缺点是无法满足高并发量以及数据库读写频繁的系统

ZooKeeper分布式锁无论是从性能以及实现的功能来说都是非常优秀,只是在开发起来需要一定的基础,对新手可能不是很友好

而本文主要讲第三种利用redis实现分布式锁,优点是开发相对简单,能满足一定并发量的系统,缺点是存在线程争抢锁的问题,当并发量到达一定级别,多个线程去争抢同一个锁,对性能的影响较大

 

1.分布式锁需要解决的问题

  • 互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取
  • 安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
  • 死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生
  • 容错:当部分节点宕机,客户端仍能获取锁或者释放锁


2.如何通过Redis实现分布式锁:(非完善方法)

SETNX key value :如果key不存在,则创建并赋值

  • 时间复杂度: 0(1)
  • 返回值:设置成功,返回1;设置失败,返回0。

但是此时我们获取的key是长期有效的,所以我们应该如何解决长期有效的问题呢?

如何解决SETNX长期有效的问题

EXPIRE key seconds

  • 设置key的生存时间,当key过期时(生存时间为0) ,会被自动删除
  • 缺点:原子性得不到满足

下面是伪代码

//该程序存在危险,如果执行到第二行就崩溃了,则此时key会被一直占用而无法被释放

RedisService redisService = SpringUtils.getBean(Redi sService.class);

long status = redisService.setnx(key, "1");

if(status == 1) {

redisService.expire(key, expire);

//执行独占资源逻辑

doOcuppiedWork();

}


3.如何通过Redis实现分布式锁:(正确方式)

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :设置键的过期时间为second秒
  • PX millisecond :设置键的过期时间为millisecond毫秒
  • NX :只在键不存在时,才对键进行设置操作
  • XX:只在键已经存在时,才对键进行设置操作
  • SET操作成功完成时,返回OK ,否则返回nil

下面是伪代码

RedisService redisService = SpringUtils.getBean(RedisService.class); .

String result = redisService.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

if ("OK".equals(result)) {

//执行独占资源逻辑

doOcuppiedWork();

}

 


4.大量的key同时过期的注意事项

  • 集中过期,由于清除大量的key很耗时,会出现短暂的卡顿现象
  • 解放方案:在设置key的过期时间的时候,给每个key加上随机值

 

Redis事务机制和分布式锁

 

Redis事务机制

严格意义来讲,Redis的事务和我们理解的传统数据库(如mysql)的事务是不一样的;Redis的事务实质上是命令的集合,在一个事务中要么所有命令都被执行,要么所有事物都不执行。
一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务。
  2. 命令入队。
  3. 执行事务。

在MySQL中我们使用START TRANSACTION 或 BEGIN开启一个事务,使用COMMIT提交一个事务;而在Redis中我们使用MULTI 开始一个事务,由 EXEC 命令触发事务, 一并执行事务中的所有命令。

可以看到,MULTI 开始到 EXEC结束前,中间所有的命令都被加入到一个命令队列中;当执行 EXEC命令后,将QUEUE中所有的命令执行。

此外我们可以使用DISCARD取消事务。

需要注意的是:
1.Redis的事务没有关系数据库事务提供的回滚(rollback),所以开发者必须在事务执行失败后进行后续的处理;
2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3.如果在一个事务中出现运行错误,那么正确的命令会被执行。

WATCH

研究过java的J.U.C包的人应该都知道CAS,CAS是一种保证原子性的操作。那么redis也提供了这样的一个机制,就是利用watch命令来实现的。

WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

分布式锁

什么是分布式锁?

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

实现分布式锁有很多实现方式和工具,如Zookeeper、Redis等。

使用Redis实现分布式锁原理:

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,基于此,Redis中可以使用SETNX命令实现分布式锁。

SETNX——SET if Not eXists(如果不存在,则设置):

setnx key value

将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。

如果需要解锁,使用 del key 命令就能释放锁:

左图首先使用setnx对键加锁成功返回1,右图再次使用setnx命令对键加锁失败返回0,说明有客户端持有锁。使用del释放锁以后,右图就可以使用setnx命令对键加锁。

解决死锁

如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?

答:给锁设置一个过期时间,可以通过两种方法实现:通过命令 “setnx 键名 过期时间 “;或者通过设置锁的expire时间,让Redis去删除锁。

第一种实现方式:
使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。
具体做法如下:

客户端2发送SETNX lock.test 想要获得锁,由于之前的客户端1还持有锁,所以Redis返回一个0
客户端2发送GET lock.test 以检查锁是否超时了,如果没超时,则等待或重试。
反之,如果已超时,客户端2通过下面的操作来尝试获得锁:
GETSET lock.test 过期的时间
通过GETSET,客户端2拿到的时间戳如果仍然是超时的,那就说明,客户端2如愿以偿拿到锁了。
如果在客户端2之前,有个客户端3比客户端2快一步执行了上面的操作,那么客户端2拿到的时间戳是个未超时的值,这时,说明客户端2没有如期获得锁,需要再次等待或重试。
尽管客户端2没拿到锁,但它改写了客户端3设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

第二种就非常简单了:
通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。

1.客户端1使用setnx获得了锁,并且使用expire设定一个过期时间,假定是10ms

2.过了4ms后,客户端1不幸运的宕机了,此时客户端2想要通过setnx尝试获得锁,但是锁还没有过期,任然被客户端1所持有。

3.到了11ms时,锁过期了,Redis帮我们删除了锁,此时客户端2想要通过setnx尝试获得锁,此时就能成功获得锁。

在实际过程中,我们可以设定一个时间T,用来表示客户端在初次尝试获得锁失败以后,在多次尝试获得锁所花的时间。如果次时间为0,表示除此尝试获得锁失败以后就不会再去尝试获得锁了。

 

 

浅析Redis实现lock互斥访问资源

 

          Redis是当前很流行的一种开源键值数据库。目前睿思的后台架构在数据库层采用了Redis和MySQL组合的形式,其中Redis主要用来存储状态信息(比如当前种子的peer)和读写频繁的数据。Redis完全运行在内存之上,无lock设计,速度非常快!通过实测,在睿思服务器上读写速度达到3万次/s。

 

          在高并发的应用中,很多时候我们需要对某些资源进行竞争访问,比如在很多人下载一个热门资源,就可能存在很多请求去修改某个资源的peer信息(就是保存了当前保种人的ip地址和端口号),需要保证某个请求修改peer信息的时候,不允许其他请求修改,否则就会出现数据覆盖的问题。但是Redis没有提供对数据的加锁,所以需要我们通过Redis提供的命令自己实现:

 

思路一:通过get 和set 命令实现

        这种方式很容易想到,就是当每次请求到来时通过get判断这个锁是否存在,如果不存在则set创建。这种方法有一个弊端,由于get和set是两次Redis请求,二者之间有延时,在高并发的环境下,有可能在get检测到锁不存之后在set之前已经被其他线程set,这时当前线程再set,这样锁就失效了。所以这种方法只能应对并发量不是很高的情况。

 
思路二:通过setnx 和 expire命令实现

       在访问需要互斥访问的资源时,通过setnx命令去设置一个lock 键,setnx的作用是判断锁是否存在,如果不存在则创建,返回成功,如果存在则返回失败,服务器返回给客户端,指示客户端稍后重试。expire命令用于给该锁设定一个过期时间,用于防止线程crash,导致锁一直有效,从而导致死锁。例如:设定锁的有效期为100秒,那么即使线程奔溃,在100秒后锁会自动失效。

setnx lock "lock"


如果成功则设置过期时间

expire lock 100


访问互斥资源结束后,删除锁

del lock

 
思路三:通过watch和Redis的事务命令实现

     这种方式的效果和思路二类似。在请求到时先watch改资源锁,然后再通过在事务执行 创建锁的过程,锁的键值能唯一标识改请求(比如用时间+用户标识)。如果当前还有其他线程请求该资源,当判断该锁存在时则返回错误重试(例如睿思BT tracker返回“服务器过载,自动重试的”的提示就属于此类情况)。如果有多个请求同时判断改锁不存在而创建锁,这样也会由于watch了这个锁,导致之前watch的线程执行事务失败,返回客户端自动重试。这样达最终达到了锁的目的。

 

 

以上是 python使用redis实现协同控制的分布式锁 的全部内容, 来源链接: utcz.com/z/387102.html

回到顶部