redis原子操作&lua脚本

一、背景

在koala项目中, 很多处用到了redis锁, 比如ota升级、获取photo表的自增id、控制翻译加载等,   多数用法示例如下:

这种用法, 通过expire设置过期时间来防止未释放锁带来的问题, 但引来了其他问题:

  • setnx 与 expire分开调用,不能保证原子性, 因此可能存在expire未成功调用且锁未成功释放的问题

二、调研

2.1、将setnx 与 expire组合成原子操作的方法

  • redis版本>=2.6.12后(我们用的是3.0.6版本),为SET命令增加了一系列选项SET key value [EX seconds] [PX milliseconds] [NX|XX] ,   可以完美替代setnx+expire
  • set命令官方文档:http://www.redis.cn/commands/set.html
  • 关于锁的一些考虑点:http://redis.cn/topics/distlock.html
  • python redis锁的规范实现https://github.com/SPSCommerce/redlock-py,     内部也是用的set命令, 且应保存一个随机token,   释放锁时判断token是否一致,   防止释放其他进程的锁(即自身锁已经过期)
  • python 测试:
    • 测试代码

      # coding=utf-8
      import sys, os
      sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
      
      import time
      from app import create_app
      from app.foundation import redis
      create_app(False)
      
      
      def test_redis():
          r = redis.db.set('setnx_test', "1", ex=10, nx=True)
          print ('test_redis....set..r:{0}'.format(r))
      
          for i in range(5):
              r = redis.db.set('setnx_test', "1", ex=10, nx=True)
              print ('test_redis....set.i:{0}, r:{1}'.format(i, r))
      
          r = redis.db.get('setnx_test')
          print ('test_redis.....get:{0}'.format(r))
          time.sleep(10)
      
          r = redis.db.get('setnx_test')
          print ('test_redis.....get after sleep:{0}'.format(r))
      
          r = redis.db.set('setnx_test', "1", ex=10, nx=True)
          print ('test_redis...set...r:{0}'.format(r))
      
      test_redis()

    • 测试结果输出: 

    • 结论:  可以完美替代setnx+expire,  且为原子操作, 更加安全和规范

2.2、如果setnx 与 expire之间, 存在其他判断逻辑, 如何保证原子性

  • 参考http://redisdoc.com/script/eval.html
  • Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
  • 用lua脚本实现限制并发数:
    • @system.route('/redis')
      def test_redis():
          """
          逻辑: 限制并发数, 最大为5 
          """
          key = 'sync_pad_sign'
          try:
              lua = """
                  local key = KEYS[1]
                  local limit = tonumber(KEYS[2])
                  local curentLimit = tonumber(redis.call('get', key) or "0")
      
                  if (curentLimit + 1 > limit) 
                  then
                      return 0
                  else
                      redis.call("INCRBY", key, 1)
                      redis.call("EXPIRE", key, 20)
                      return curentLimit + 1
                  end
              """
              r = redis.db.eval(lua, 2, key, 5)
              logger.info('-----------r:{0}, type:{1}'.format(r, type(r)))
              if r == 0:
                  from flask import make_response
                  return make_response('', 304)
      
          except Exception, e:
              logger.info('sync pad get redis failed e:{0}'.format(e))
      
          sleep(2)
          try:
              redis.db.decr(key)
          except Exception, e:
              logger.error('release redis key failed e:{0}'.format(e))
      
          return success_result()

2.3、拓展一下:多个操作, 是否可以打包成一个原子操作

  • lua脚本 

2.4、官方限制请求量的例子

三、总结

  • 使用set 代替setnx +  expire命令
  • value应设置一个随机token
  • 释放时, 应判断token是否一致, 防止释放其他进程的锁
  • 对于有复杂逻辑的,  采用lua脚本保证原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值