Redis锁构造

     
 源代码地址:https://github.com/fancy-dawning/hello-world/blob/master/Goldpoint.cpp

单线程与隔离性

Redis是采用单线程的法子来实施工作的,事务以串行的点子运行,也就是说Redis中单个命令的实践和作业的举行都是线程安全的,不会相互影响,具有隔离性。

在多线程编程中,对于共享资源的访问要那些的小心:

import threading

num = 1
lock = threading.Lock()


def change_num():
    global num
    for i in xrange(100000):
        #lock.acquire()
        num += 5
        num -= 5
        #lock.release()


if __name__ == '__main__':
    pool = [threading.Thread(target=change_num) for i in xrange(5)]
    for t in pool:
        t.start()
    for t in pool:
        t.join()
    print num

在不加锁的图景下,num是不可能保障为1的。

而在Redis中,并发执行单个命令具有很好的隔离性:

import redis

conn = redis.StrictRedis(host="localhost", port=6379, db=1)
conn.set('num', 1)


def change_num(conn):
    for i in xrange(100000):
    ┆   conn.incr('num', 5)
    ┆   conn.decr('num', 5)


if __name__ == '__main__':
    conn_pool = [redis.StrictRedis(host="localhost", port=6379, db=1)
                 for i in xrange(5)]
    t_pool = []
    for conn in conn_pool:
        t = threading.Thread(target=change_num, args=(conn,))
        t_pool.append(t)
    for t in t_pool:
        t.start()
    for t in t_pool:
        t.join()
    print conn.get('num')

效仿的5个客户端同时对Redis中的num值举办操作,num最后结出会维持为1:

1
real    0m46.463s
user    0m28.748s
sys 0m6.276s

使用Redis中单个操作和事情的原子性可以做过多作业,最简便的就是做全局计数器了。

例如在短信验证码业务中,要限制一个用户在一分钟内只可以发送一回,假如利用关系型数据库,需要为每个手机号记录上次出殡短信的刻钟,当用户请求验证码时,取出与当下岁月展开相比。

这一情形下,当用户长时间点击多次时,不仅平添了数据库压力,而且还会出现同时询问均符合条件但数据库更新短信发送时间较慢的题目,就会重新发送短信了。

在Redis中解决这一题材就很简短,只需要用手机号作为key创立一个在世期限为一分钟的数值即可。key不设有时能发送短信,存在时则不可能发送短信:

def can_send(phone):
    key = "message:" + str(phone)
    if conn.set(key, 0, nx=True, ex=60):
    ┆   return True
    else:
    ┆   return False

至于有些不可名的30分钟内限制访问如故下载5次的效率,将用户ip作为key,值设为次数上限,过期岁月设为限制时间,每一次用户访问时自减即可:

def can_download(ip):
    key = "ip:" + str(ip)
    conn.set(key, 5, nx=True, ex=600)
    if conn.decr(key) >= 0:
    ┆   return True
    else:
    ┆   return False

       分工:
本周的课程任务是结伴编程实现黄金点游戏,我的结对对象是郑淑丹,分工情状是:驾驶员:袁文雪,领航员:郑淑丹。由于我们几个人都比价紧缺项目经验,因而在结对编程的进程中从未特意严刻的分工,而是不停的在交换相互的角色来胜利的姣好我们的系列的。

Redis基本业务与乐观锁

尽管Redis单个命令具有原子性,但当两个指令并行执行的时候,会有更多的题目。

例如举一个转速的例子,将用户A的钱转给用户B,那么用户A的账户缩小需要与B账户的充实并且拓展:

import threading
import time

import redis

conn = redis.StrictRedis(host="localhost", port=6379, db=1)
conn.mset(a_num=10, b_num=10)


def a_to_b():
    if int(conn.get('a_num')) >= 10:
        conn.decr('a_num', 10)
        time.sleep(.1)
        conn.incr('b_num', 10)
    print conn.mget('a_num', "b_num")


def b_to_a():
    if int(conn.get('b_num')) >= 10:
        conn.decr('b_num', 10)
        time.sleep(.1)
        conn.incr('a_num', 10)
    print conn.mget('a_num', "b_num")


if __name__ == '__main__':
    pool = [threading.Thread(target=a_to_b) for i in xrange(3)]
    for t in pool:
        t.start()

    pool = [threading.Thread(target=b_to_a) for i in xrange(3)]
    for t in pool:
        t.start()

运作结果:

['0', '10']
['0', '10']
['0', '0']
['0', '0']
['0', '10']
['10', '10']

现身了账户总额变少的场合。即使是人造的为自增自减命令之间添加了100ms延迟,但在事实上出现很高的景观中是很可能出现的,两个指令执行期间履行了其它的言语。

这就是说现在要保证的是五个增减命令执行期间不受其余命令的困扰,Redis的事体可以高达这一目标。

Redis中,被MULTI命令和EXEC命令包围的拥有命令会一个接一个的施行,直到所有命令都履行完毕结束。一个事务完毕后,Redis才会去处理其他的下令。也就是说,Redis事务是拥有原子性的。

python中得以用pipeline来创立工作:

def a_to_b():
    if int(conn.get('a_num')) >= 10:
    ┆   pipeline = conn.pipeline()
    ┆   pipeline.decr('a_num', 10)
    ┆   time.sleep(.1)
    ┆   pipeline.incr('b_num', 10)
    ┆   pipeline.execute()
    print conn.mget('a_num', "b_num")


def b_to_a():
    if int(conn.get('b_num')) >= 10:
    ┆   pipeline = conn.pipeline()
    ┆   pipeline.decr('b_num', 10)
    ┆   time.sleep(.1)
    ┆   pipeline.incr('a_num', 10)
    ┆   pipeline.execute()
    print conn.mget('a_num', "b_num")

结果:

['0', '20']
['10', '10']
 ['-10', '30']
['-10', '30']
['0', '20']
['10', '10']

可以观看,两条语句确实联合实施了,账户总额不会变,但出现了负值的动静。这是因为业务在exec命令被调用在此之前是不会实施的,所以用读取的多寡做判断与作业执行之间就有了时光差,期间实际数目发生了转变。

为了保持数据的一致性,我们还亟需使用一个工作命令WATCH。WATCH可以对一个键展开蹲点,监视后到EXEC命令执行往日,要是被监视的键值爆发了变动(替换,更新,删除等),EXEC命令会再次回到一个荒谬,而不会真的的施行:

>>> pipeline.watch('a_num')
True
>>> pipeline.multi()
>>> pipeline.incr('a_num',10)
StrictPipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
>>> pipeline.execute()
[20]
>>> pipeline.watch('a_num')
True
>>> pipeline.incr('a_num',10) #监视期间改变被监视键的值
30
>>> pipeline.multi()
>>> pipeline.incr('a_num',10)
StrictPipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
>>> pipeline.execute()
    raise WatchError("Watched variable changed.")
redis.exceptions.WatchError: Watched variable changed.

近来为代码加上watch:

def a_to_b():
      pipeline = conn.pipeline()
      try:
      ┆   pipeline.watch('a_num')
      ┆   if int(pipeline.get('a_num')) < 10:
      ┆   ┆   pipeline.unwatch()
      ┆   ┆   return
      ┆   pipeline.multi()
      ┆   pipeline.decr('a_num', 10)
      ┆   pipeline.incr('b_num', 10)
      ┆   pipeline.execute()
      except redis.exceptions.WatchError:
      ┆   pass
      print conn.mget('a_num', "b_num")


  def b_to_a():
      pipeline = conn.pipeline()
      try:
      ┆   pipeline.watch('b_num')
      ┆   if int(pipeline.get('b_num')) < 10:
      ┆   ┆   pipeline.unwatch()
      ┆   ┆   return
      ┆   pipeline.multi()
      ┆   pipeline.decr('b_num', 10)
      ┆   pipeline.incr('a_num', 10)
      ┆   pipeline.execute()
      except redis.exceptions.WatchError:
      ┆   pass
      print conn.mget('a_num', "b_num")

结果:

['0', '20']
['10', '10']
['20', '0']

中标落实了账户转移,不过有两遍尝试败北了,假若要尽量的使每一趟交易都得到成功,能够加尝试次数或者尝试时间:

def a_to_b():
    pipeline = conn.pipeline()
    end = time.time() + 5
    while time.time() < end:
    ┆   try:
    ┆   ┆   pipeline.watch('a_num')
    ┆   ┆   if int(pipeline.get('a_num')) < 10:
    ┆   ┆   ┆   pipeline.unwatch()
    ┆   ┆   ┆   return
    ┆   ┆   pipeline.multi()
    ┆   ┆   pipeline.decr('a_num', 10)
    ┆   ┆   pipeline.incr('b_num', 10)
    ┆   ┆   pipeline.execute()
    ┆   ┆   return True
    ┆   except redis.exceptions.WatchError:
    ┆   ┆   pass
    return False

这般,Redis可以行使工作实现类似于锁的编制,但那一个机制与关系型数据库的锁有所不同。关系型数据库对被访问的多寡行举办加锁时,其它客户端尝试对被加锁数据行开展写入是会被封堵的。

Redis执行WATCH时并不会对数码进行加锁,假若发现数目现已被此外客户端超过修改,只会通报执行WATCH命令的客户端,并不会阻止修改,这称为乐观锁。

       运行环境:Windows操作系统,vc++6.0开发条件。

用SET()构建锁

用WACTH实现的乐观锁一般意况下是适用的,但存在一个题目,程序会为形成一个履行破产的业务而不止地拓展重试。当负载扩展的时候,重试次数会上升到一个不得承受的境地。

倘使要和谐正确的实现锁的话,要避免下边多少个状况:

  • 五个经过同时获取了锁
  • 富有锁的进程在假释锁在此之前崩溃了,而其余进程却不清楚
  • 怀有锁的举办运作时刻过长,锁被机关释放了,进程本身不亮堂,还会尝试去放活锁

Redis中要兑现锁,需要使用一个指令,SET()或者说是SETNX()。SETNX只会在键不存在的景观下为键设置值,现在SET命令在加了NX选项的图景下也能实现那一个成效,而且仍可以设置过期时间,简直就是天生用来构建锁的。

只要以需要加锁的资源名为key设置一个值,要得到锁时,检查这么些key存不存在即可。若存在,则资源已被此外进程取得,需要阻塞到此外进程释放,若不存在,则树立key并获取锁:

import time
import uuid


class RedisLock(object):

    def __init__(self, conn, lockname, retry_count=3, timeout=10,):
        self.conn = conn
        self.lockname = 'lock:' + lockname
        self.retry_count = int(retry_count)
        self.timeout = int(timeout)
        self.unique_id = str(uuid.uuid4())

    def acquire(self):
        retry = 0
        while retry < self.retry_count:
            if self.conn.set(lockname, self.unique_id, nx=True, ex=self.timeout):
                return self.unique_id
            retry += 1
            time.sleep(.001)
        return False

    def release(self):
        if self.conn.get(self.lockname) == self.unique_id:
            self.conn.delete(self.lockname)
            return True
        else:
            return False

取得锁的默认尝试次数限制3次,3次拿走败北则赶回。锁的活着期限默认设为了10s,若不主动释放锁,10s后锁会活动清除。

还保存了收获锁时锁设置的值,当释放锁的时候,会先判断保存的值和最近锁的值是否同样,假设不同等,表明是锁过期被机关释放然后被其余进程取得了。所以锁的值必须保障唯一,以免释放了别样程序拿到的锁。

使用锁:

def a_to_b():
    lock = Redlock(conn, 'a_num')
    if not lock.acquire():
    ┆   return False

    pipeline = conn.pipeline()
    try:
    ┆   pipeline.get('a_num')
    ┆   (a_num,) = pipeline.execute()
    ┆   if int(a_num) < 10: 
    ┆   ┆   return False
    ┆   pipeline.decr('a_num', 10) 
    ┆   pipeline.incr('b_num', 10) 
    ┆   pipeline.execute()
    ┆   return True
    finally:
    ┆   lock.release()

放出锁时也可以用Lua脚本来告诉Redis:删除这些key当且仅当这多少个key存在而且值是本人期待的丰裕值:

    unlock_script = """
    if redis.call("get",KEYS[1]) == ARGV[1] then
    ┆   return redis.call("del",KEYS[1])
    else
    ┆   return 0
    end"""

可以用conn.eval来运行Lua脚本:

    def release(self):
    ┆   self.conn.eval(unlock_script, 1, self.lockname, self.unique_id)

这样,一个Redis单机锁就贯彻了。我们得以用这多少个锁来代替WATCH,或者与WACTH同时使用。

事实上行使中还要遵照业务来支配锁的粒度的题材,是锁住整个结构如故锁住结构中的一小部分。

粒度越大,性能越差,粒度越小,暴发死锁的几率越大。

 

     
 代码实现:黄金点游戏规则: N个同学(N通常大于10)参加游戏,每人写一个0~100之内的有理数(不包括0或100),系统自动算出所有数字的平均值,然后乘以0.618(所谓黄金分割常数),获得G值。玩家输入的数字最靠近G(取相对值)的同窗取得N分,离G最远的同班取得-2分,其他同学得0分。明白游戏规则后我们现在网上找了一部分博主的代码作为参考,看他们是咋样促成这个功能的,并且再他们代码的底蕴上进展了职能的修改和扩大得到我们的代码,具体效果如下:

       
1、玩家输入数字时加密。因为是单机实现此游玩,所以为了保证游戏的公平性,通过数组存储输入的数字,输出在体现屏幕上的用*号代替,即输入的数字对此外玩家透明。

       
2、输入完毕后呈现各玩家的输入。G值是在所有玩家输入后由系统自动统计出,输入完毕后显示可使用户验证黄金点的正确性。

        3、用户可自定义玩家人数和娱乐轮数,接纳for循环实现。

       
4、每一轮游戏截至后出示黄金点数字和各种玩家对应的得分,每一轮截至呈现累计得分。

        5、用户界面,设有很多指示性语句来充实程序友好性。

       
总括:本次结对编程实现的黄金点游戏即使效果还比较简单,但各类小功用的打响实现对大家的话都是一些提高。另外,这是第一次选用结对编程的办法来成功一个连串,在这些历程中自己能明了体会到这种方法的优势,很好的注释了合力就是力量,尽管只是四人,但我们有联合的天职,因而际遇问题的座谈和想方设法的提议等方面都比个人编程能更有得到。

发表评论

电子邮件地址不会被公开。 必填项已用*标注