python实现redis三种cas事务操作


Posted in Python onDecember 19, 2017

cas全称是compare and set,是一种典型的事务操作。

简单的说,事务就是为了存取数据库中同一数据时不破坏操作的隔离性和原子性,从而保证数据的一致性。

一般数据库,比如MySql是如何保证数据一致性的呢,主要是加锁,悲观锁。比如在访问数据库某条数据的时候,会用SELECT FOR UPDATE ,这MySql就会对这条数据进行加锁,直到事务被提交(COMMIT),或者回滚(ROLLBACK)。如果此时,有其他事务对被加锁的数据进行写入,那么该事务将会被阻塞,直到第一个事务完成为止。它的缺点在于:持有锁的事务运行越慢,等待解锁的事务阻塞时间就越长。并且容易产生死锁(前面有篇文章有讲解死锁)!

本文会介绍三种redis实现cas事务的方法,并会解决下面的虚拟问题:
维护一个值,如果这个值小于当前时间,则设置为当前时间;如果这个值大于当前时间,则设置为当前时间+30。简单的单线程环境下代码如下:

# 初始化
r = redis.Redis()
if not r.exists("key_test"):
  r.set("key_test", 0)

def inc():
  count = int(r.get('key_test')) + 30 #1
  # 如果值比当前时间小,则设置为当前时间
  count = max(count, int(time.time())) #2
  r.set('key_test', count) #3
  return count

很简单的一段代码,在单线程环境下可以跑的很欢,但显然,是无法移植到多线程或者是多进程环境的(进程A和B同时运行到#1,获取了相同的count值,然后运行#2#3,会导致count值总共只增加了30)。而为了能在多进程环境下运行,我们需要引入一些其他的东西。

py-redis本身自带的事务操作

redis有这么几个和事务相关的命令,multi,exec,watch。通过这几个命令,可以实现‘将多个命令打包,然后一次性、按顺序执行,且不会被终端'。事务会从MULTI开始,执行EXEC后触发事件。另外,我们还需要WATCH,watch可以监视任意数量的键,当在调用EXEC执行事务时,如果任意一个键被修改了,整个事务不会执行。

下边是使用redis本身的事务解决cas问题的代码。

class CasNormal(object):
  def __init__(self, host, key):
    self.r = redis.Redis(host)
    self.key = key
    if not self.r.exists(self.key):
      self.r.set(self.key, 0)

  def inc(self):
    with self.r.pipeline() as pipe:
      while True:
        try:
          #监视一个key,如果在执行期间被修改了,会抛出WatchError
          pipe.watch(self.key)
          next_count = 30 + int(pipe.get(self.key))
          pipe.multi()
          if next_count < int(time.time()):
            next_count = int(time.time())
          pipe.set(self.key, next_count)
          pipe.execute()
          return next_count
        except WatchError:
          continue
        finally:
          pipe.reset()

代码也不复杂,引入了之前说到的multi,exec,watch,如果对事务操作比较熟悉的同学,可以很容易看出来,这是一个乐观锁的操作(咱们假设没人竞争来着,每次去拿数据的时候都不会上锁,真有人来改了再说。)乐观锁在高并发的情况下会显得很无力,文末的性能对比会显示这个问题。

使用基于redis的悲观锁

悲观锁,就是很悲观的锁,每次拿数据都会假设别人也要拿,先给锁起来,用完再把锁释放掉。redis本身没有实现悲观锁,但我们可以先用redis实现一个悲观锁。

ok,咱们现在有悲观锁了,做起事来也有底气了,根据上边的代码,咱们只要加上@ synchronized注释就能保证同一时间只有一个进程在执行。下边是基于悲观锁的解决方案。

lock_conn = redis.Redis("localhost")

class CasLock(object):
  def __init__(self, host, key):
    self.r = redis.Redis(host)
    self.key = key
    if not self.r.exists(self.key):
      self.r.set(self.key, 0)

  @synchronized(lock_conn, "lock", 10)
  def inc(self):
    next_count = 30 + int(self.r.get(self.key))
    if next_count < int(time.time()):
      next_count = int(time.time())
    self.r.set(self.key, next_count)
    return next_count

代码看上去少多了(因为引入了synchronized...)

基于lua脚本实现

上边两种方法都是用锁来实现的,锁的实现总会出现竞争的问题,区别无非是出现竞争了咋办的问题。使用redis lua脚本的实现,可以直接把这个cas操作当成一个<b>原子操作</b>。

我们知道,redis本身的一系列操作,都是原子操作,且redis会按顺序执行所有收到的命令。先看代码

class CasLua(object):
  def __init__(self, host, key):
    self.r = redis.Redis(host)
    self.key = key
    if not self.r.exists(self.key):
      self.r.set(self.key, 0)
    self._lua = self.r.register_script("""
    local next_count = redis.call('get',KEYS[1]) + ARGV[1]
    ARGV[2] = tonumber(ARGV[2])
    if next_count < ARGV[2] then
      next_count = ARGV[2]
    end
    redis.call('set',KEYS[1],next_count)
    return tostring(next_count)
        """)

  def inc(self):
    return int(self._lua([self.key], [30, int(time.time())]))

这里先注册了这个脚本,后边可以直接去使用他。关于redis lua脚本的文章有不少,感兴趣的可以去搜搜看,这边就不赘述了。

性能对比

这边的测试只是一个非常简单的测试(不过还是能看出效果来的),测试换机就是自己的开发机,数字看个大小就行了。

分别测了三种操作在单线程,五个线程,十个线程,五十个线程情况下,进行1000次操作各自的表现,时间如下

optimistic Lock pessimistic lock  lua
1thread       0.43       0.71 0.35
5thread       5.80       3.10 0.62
10thread      17.80       5.60 1.30
50thread      245.00       29.60 6.50

依次是redis本身事务实现的乐观锁,基于redis实现的悲观锁以及lua实现。

在比较悲观锁和乐观锁之前,需要先说明一点,这边的测试对乐观锁不是很公平,乐观锁本身就是假设不会有很多的并发的。在单线程情况下,悲观锁要差一些。单线程下,不存在竞争关系,悲观锁耗时长仅因为是多了一次redis的网络交互。随着线程的增加,悲观锁的性能逐渐变好,毕竟悲观锁本身就是为了解决这种高并发高竞争的环境而诞生的。在50线程的时候,乐观锁的实现单次操作的时间要0.245秒,非常恐怖,如果是生产环境,几乎都不能用了。

至于lua的性能,快的不可思议,几乎就是线性增加。(50线程的情况下,平均的1000次完成时间是6.5s,换言之,6.5秒内执行了50 * 1000次cas操作)。

以上测试都是本地redis,本地测试,如果redis是远端的,网络交互时间会增加,lua优势会更加明显。希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python格式化压缩后的JS文件的方法
Mar 05 Python
操作Windows注册表的简单的Python程序制作教程
Apr 07 Python
详解Python爬虫的基本写法
Jan 08 Python
python3中bytes和string之间的互相转换
Feb 09 Python
python批量替换多文件字符串问题详解
Apr 22 Python
pandas.loc 选取指定列进行操作的实例
May 18 Python
使用Python创建简单的HTTP服务器的方法步骤
Apr 26 Python
Python 将json序列化后的字符串转换成字典(推荐)
Jan 06 Python
pycharm 激活码及使用方式的详细教程
May 12 Python
pyqt5实现井字棋的示例代码
Dec 07 Python
pycharm 使用tab跳出正在编辑的括号(){}{}等问题
Feb 26 Python
Python基础之pandas数据合并
Apr 27 Python
Python2/3中urllib库的一些常见用法
Dec 19 #Python
Python与人工神经网络:使用神经网络识别手写图像介绍
Dec 19 #Python
Python random模块用法解析及简单示例
Dec 18 #Python
Python方法的延迟加载的示例代码
Dec 18 #Python
Python字符串拼接六种方法介绍
Dec 18 #Python
Python Socket使用实例
Dec 18 #Python
浅谈python数据类型及类型转换
Dec 18 #Python
You might like
用PHP写的MySQL数据库用户认证系统代码
2007/03/22 PHP
php引用返回与取消引用的详解
2013/06/08 PHP
Laravel 修改验证异常的响应格式实例代码详解
2020/05/25 PHP
css图片自适应大小
2007/11/28 Javascript
利用JS延迟加载百度分享代码,提高网页速度
2013/07/01 Javascript
js 左右悬浮对联广告代码示例
2014/12/12 Javascript
JavaScript黑洞数字之运算路线查找算法(递归算法)实例
2016/01/28 Javascript
JavaScript动态生成二维码图片
2016/04/20 Javascript
JavaScript自定义文本框光标
2017/03/05 Javascript
js模拟支付宝密码输入框
2017/04/11 Javascript
详解EasyUi控件中的Datagrid
2017/08/23 Javascript
限时抢购-倒计时的完整实例(分享)
2017/09/17 Javascript
详解Vue2 添加对scss的支持
2019/01/02 Javascript
vue登录页面cookie的使用及页面跳转代码
2019/07/10 Javascript
PHP读取远程txt文档到数组并实现遍历
2020/08/25 Javascript
python中split方法用法分析
2015/04/17 Python
详解使用 pyenv 管理多个版本 python 环境
2017/10/19 Python
python中文乱码不着急,先看懂字节和字符
2017/12/20 Python
Python基础教程之利用期物处理并发
2018/03/29 Python
django的ORM操作 删除和编辑实现详解
2019/07/24 Python
树莓派4B+opencv4+python 打开摄像头的实现方法
2019/10/18 Python
python 使用raw socket进行TCP SYN扫描实例
2020/05/05 Python
Django model重写save方法及update踩坑详解
2020/07/27 Python
实例讲解使用CSS实现多边框和透明边框的方法
2015/09/08 HTML / CSS
html5 localStorage本地存储_动力节点Java学院整理
2017/07/06 HTML / CSS
印度尼西亚在线时尚购物网站:ZALORA印尼
2016/08/02 全球购物
高二美术教学反思
2014/01/14 职场文书
CAD制图设计师自荐信
2014/01/29 职场文书
竞选部门副经理的自荐书范文
2014/02/11 职场文书
蓝颜请假条
2014/04/11 职场文书
C++程序员求职信范文
2014/04/14 职场文书
结婚通知短信大全
2015/04/17 职场文书
入党团支部推荐意见
2015/06/02 职场文书
毕业班工作总结
2015/08/10 职场文书
商业计划书如何写?关键问题有哪些?
2019/07/11 职场文书
GPU服务器的多用户配置方法
2022/07/07 Servers