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操作当成一个原子操作

我们知道,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 redis事务_python实现redis三种cas事务操作相关推荐

  1. python实现redis三种cas事务操作

    cas全称是compare and set,是一种典型的事务操作. 简单的说,事务就是为了存取数据库中同一数据时不破坏操作的隔离性和原子性,从而保证数据的一致性. 一般数据库,比如MySql是如何保证 ...

  2. python请输入_python中的三种输入方式

    python中的三种输入方式 python2.X python2.x中以下三个函数都支持: raw_input() input() sys.stdin.readline() raw_input( )将 ...

  3. python常用格式化_python的常用三种格式化方法

    最近看别人代码时,发现一个新的打印输出格式,很有意思,print(f'{}'),所以找了些资料学习总结了一下,现把几个常用的方法记录下来. 神奇的 % 号 % 号格式化字符串沿用的是C语言的方法,py ...

  4. python编程模式是什么_python 开发的三种运行模式详细介绍

    Python 三种运行模式 Python作为一门脚本语言,使用的范围很广.有的同学用来算法开发,有的用来验证逻辑,还有的作为胶水语言,用它来粘合整个系统的流程.不管怎么说,怎么使用python既取决于 ...

  5. Spring的4种事务管理(1种编程式事务+三种声明事务)

    2019独角兽企业重金招聘Python工程师标准>>> Spring的4种事务管理(1种编程式事务+三种声明事务) 一.Spring事务的介绍 二.编程式事务xml的配置 注入后直接 ...

  6. python csv库,Python 中导入csv数据的三种方法

    Python 中导入csv数据的三种方法,具体内容如下所示: 1.通过标准的Python库导入CSV文件: Python提供了一个标准的类库CSV文件.这个类库中的reader()函数用来导入CSV文 ...

  7. python打开文件不存在-Python判断文件是否存在的三种方法

    原标题:Python判断文件是否存在的三种方法 通常在读写文件之前,需要判断文件或目录是否存在,不然某些处理方法可能会使程序出错.所以最好在做任何操作之前,先判断文件是否存在. 这里将介绍三种判断文件 ...

  8. python 命令-python解析命令行参数的三种方法详解

    这篇文章主要介绍了python解析命令行参数的三种方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 python解析命令行参数主要有三种方法: ...

  9. 分布式事务中常见的三种解决方案

    分布式事务中常见的三种解决方案 目录 一.分布式事务前奏 二.柔性事务解决方案架构 (一).基于可靠消息的最终一致性方案概述 (二).TCC事务补偿型方案 (三).最大努力通知型 三.基于可靠消息的最 ...

最新文章

  1. vs2008格式化代码
  2. Python 笔试集(1):关于 Python 链式赋值的坑
  3. POJ 2828 Buy Tickets 线段树
  4. iOS攻防——(四)class-dump-与-Dumpdecrypted-使用
  5. MVCC(Multi-version Cocurrent Control)
  6. 一维数组kmeans聚类c语言,一维数组的 K-Means 聚类算法理解
  7. 【文末有福利】炒鸡蛋竟然与无人驾驶有关?
  8. 【WordPress 建站教程】在 正文顶端或末尾插入固定的内容
  9. java找不到符号IOException_java:7: 找不到符号
  10. 2020.07.08_Multi-passage BERT: A Globally Normalized BERT Model for Open-domain Question Answering
  11. 分析:云存储和电子发现的结合
  12. rails 2.3.5 + jquery ui datepicker 不能显示中文
  13. 前端笔记之——ajax请求出现406的原因
  14. alert(isPresented: Binding<Bool>, content: () -> Alert) (SwiftUI 中文文档手册 教程含源码)
  15. setheading指令_set echo on/off,set term on/off,set feedback off,set heading off命令(转)
  16. XCTF-PWN welpwn
  17. pikachu-sql注入(皮卡丘)
  18. Oracle知识点总结(一)
  19. 描述性统计部分(一)----统计量
  20. thuwc2020咕咕记+题解

热门文章

  1. latex 子图_MATLAB学习笔记4:如何优雅地控制子图
  2. 聚数引智,承德大数据产业对接交流会将于2019中国国际数字经济博览会期间召开...
  3. 《大数据》2015年第2期“研究”——大数据时代的数据传输网
  4. 【面向对象】面向对象程序设计测试题13-Java异常处理测试题
  5. 关于libtorrent库的安装
  6. Docker折腾手记-安装
  7. 关于验证码的DEMO
  8. Java对象转出json并过滤指定属性
  9. 解决gdal集成libkml的链接错误
  10. 版权归原作者的飞鸽传书(IPMSG)