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,如果对事务操作比较熟悉的同学,可以很容易看出来,这是一个乐观锁的操作(咱们假设没人竞争来着,每次去拿数据的时候都不会上锁,真有人来改了再说。)乐观锁在高并发的情况下会显得很无力,文末的性能对比会显示这个问题。
新闻热点
疑难解答