redis事务

redis事务

本文记录一些redis事务相关的原理。

1、基本概念

1)什么是redis的事务?

简单理解,可以认为redis事务是一些列redis命令的集合,并且有如下两个特点:

a)事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

b)事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

2)事务的性质ACID

一般来说,事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性。

a)原子性atomicity:redis事务保证事务中的命令要么全部执行要不全部不执行。有些文章认为redis事务对于执行错误不回滚违背了原子性,是偏颇的。

b)一致性consistency:redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。

c)隔离性Isolation:redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式,可以保证命令执行过程中不会被其他客户端命令打断。

d)持久性Durability:redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

3)redis事务的错误

使用事务时可能会遇上以下两种错误:

a)入队错误:事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。

b)执行错误:命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

注:第三种错误,redis进程终结,本文并没有讨论这种错误。

2、redis事务的用法

redis事务是通过MULTIEXECDISCARD和WATCH四个原语实现的。

MULTI命令用于开启一个事务,它总是返回OK。

MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

另一方面,通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务。

下面给出几种事务场景。

1)正常执行

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 1
QUEUED
127.0.0.1:6379> HSET key2 field1 1
QUEUED
127.0.0.1:6379> SADD key3 1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 1
3) (integer) 1

EXEC 命令的回复是一个数组,数组中的每个元素都是执行事务中的命令所产生的回复。 其中,回复元素的先后顺序和命令发送的先后顺序一致。

当客户端处于事务状态时,所有传入的命令都会返回一个内容为 QUEUED 的状态回复(status reply),这些被入队的命令将在 EXEC命令被调用时执行。

2)放弃事务

当执行 DISCARD 命令时,事务会被放弃,事务队列会被清空,并且客户端会从事务状态中退出:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 1
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI

3)入队错误回滚

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key1 1
QUEUED
127.0.0.1:6379> HSET key2 1
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> SADD key3 1
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

对于入队错误,redis 2.6.5版本后,会记录这种错误,并且在执行EXEC的时候,报错并回滚事务中所有的命令,并且终止事务。

3)执行错误放过

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> HSET key1 field1 1
QUEUED
127.0.0.1:6379> HSET key2 field1 1
QUEUED
127.0.0.1:6379> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 1

当遇到执行错误时,redis放过这种错误,保证事务执行完成。

这里要注意此问题,与mysql中事务不同,在redis事务遇到执行错误的时候,不会进行回滚,而是简单的放过了,并保证其他的命令正常执行。这个区别在实现业务的时候,需要自己保证逻辑符合预期。

3、使用WATCH

WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。

WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回空多条批量回复(null multi-bulk reply)来表示事务已经失败。

127.0.0.1:6379> WATCH key1
OK
127.0.0.1:6379> set key1 2
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key1 3
QUEUED
127.0.0.1:6379> set key2 3
QUEUED
127.0.0.1:6379> EXEC
(nil)

使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 key1 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。

这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。

4、python实现redis事务的demo

这里展示一个用python实现对key计数减一的原子操作。

# -*- coding:utf-8 -*-

import redis
from redis import WatchError
from concurrent.futures import ProcessPoolExecutor

r = redis.Redis(host='127.0.0.1', port=6379)


# 减库存函数, 循环直到减库存完成
# 库存充足, 减库存成功, 返回True
# 库存不足, 减库存失败, 返回False
def decr_stock():

    # python中redis事务是通过pipeline的封装实现的
    with r.pipeline() as pipe:
        while True:
            try:
                # watch库存键, multi后如果该key被其他客户端改变, 事务操作会抛出WatchError异常
                pipe.watch('stock:count')
                count = int(pipe.get('stock:count'))
                if count > 0:  # 有库存
                    # 事务开始
                    pipe.multi()
                    pipe.decr('stock:count')
                    # 把命令推送过去
                    # execute返回命令执行结果列表, 这里只有一个decr返回当前值
                    print pipe.execute()[0]
                    return True
                else:
                    return False
            except WatchError, ex:
                # 打印WatchError异常, 观察被watch锁住的情况
                print ex
                pipe.unwatch()


def worker():
    while True:
        # 没有库存就退出
        if not decr_stock():
            break


# 实验开始
# 设置库存为100
r.set("stock:count", 100)

# 多进程模拟多个客户端提交
with ProcessPoolExecutor(max_workers=2) as pool:
    for _ in range(10):
        pool.submit(worker)

观察打印

/Users/didi/anaconda/bin/python /Users/didi/test/pythoneer/redis/transaction.py
99
98
97
Watched variable changed.
96
95
94
93
Watched variable changed.
92
Watched variable changed.
91
Watched variable changed.
90
Watched variable changed.
89
Watched variable changed.
88
Watched variable changed.
Watched variable changed.
87
86
Watched variable changed.
85
Watched variable changed.
84
Watched variable changed.
Watched variable changed.
83
82
Watched variable changed.
81
Watched variable changed.
Watched variable changed.
80
79
Watched variable changed.
Watched variable changed.
78
77
Watched variable changed.
Watched variable changed.
76
75
Watched variable changed.
Watched variable changed.74

Watched variable changed.
73
72
Watched variable changed.
Watched variable changed.
71
70
Watched variable changed.
69
Watched variable changed.
68
Watched variable changed.
67
Watched variable changed.
66
Watched variable changed.
Watched variable changed.65

64
Watched variable changed.
63
Watched variable changed.
Watched variable changed.
62
Watched variable changed.
61
60
Watched variable changed.
59
Watched variable changed.
Watched variable changed.
58
57
Watched variable changed.
Watched variable changed.
56
Watched variable changed.
55
54
Watched variable changed.
53
Watched variable changed.
52
Watched variable changed.
Watched variable changed.
51
50
Watched variable changed.
49
Watched variable changed.
48
Watched variable changed.
47
Watched variable changed.
Watched variable changed.46

Watched variable changed.
45
Watched variable changed.
44
43
Watched variable changed.
42
Watched variable changed.
Watched variable changed.
41
40
Watched variable changed.
Watched variable changed.
39
Watched variable changed.
38
Watched variable changed.
37
Watched variable changed.36

Watched variable changed.
35
34
Watched variable changed.
33
Watched variable changed.
Watched variable changed.32

Watched variable changed.
31
30
Watched variable changed.
Watched variable changed.
29
Watched variable changed.
28
Watched variable changed.27

26
Watched variable changed.
25
Watched variable changed.
24
Watched variable changed.
23
Watched variable changed.
22Watched variable changed.

Watched variable changed.21

20Watched variable changed.

19
Watched variable changed.
18
Watched variable changed.
17
Watched variable changed.
16
Watched variable changed.
Watched variable changed.
15
Watched variable changed.
14
Watched variable changed.
13
12
Watched variable changed.
Watched variable changed.
11
Watched variable changed.
10
Watched variable changed.9

8
Watched variable changed.
7
Watched variable changed.
Watched variable changed.
6
5
Watched variable changed.
Watched variable changed.
4
Watched variable changed.
3
2
Watched variable changed.
1
Watched variable changed.
0
Watched variable changed.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏AI研习社

Python数据清洗实践

“数据科学家们80%的精力消耗在查找、数据清理、数据组织上,只剩于20%时间用于数据分析等。”——IBM数据分析

25630
来自专栏大数据文摘

自然语言处理背后的数据科学

NLP是人与机器之间的沟通,使得机器既可以解释我们的语言,也可以就此作出有效回答。自20世纪50年代以来,这个领域一直存在,你可能听说过Alan Turing开...

10920
来自专栏算法channel

Python 闭包坑点

上篇参考:Python 闭包使用注意点,接下来,介绍使用闭包,经常会犯的一个错误:演示代码如下,

13220
来自专栏机器之心

PyTorch最佳实践,怎样才能写出一手风格优美的代码

虽然这是一个非官方的 PyTorch 指南,但本文总结了一年多使用 PyTorch 框架的经验,尤其是用它开发深度学习相关工作的最优解决方案。请注意,我们分享的...

8030
来自专栏未闻Code

一日一技:不用get获取字典中不存在的Key

现在问题来了,每次都用 .get方法,虽然能解决问题,但是这样写起来代码不美观。并且实际上,只要key不存在,直接返回 None即可。

11710
来自专栏Python绿色通道

一键拼出你的微信好友图片墙

上午发了张微信近 2000 位好友的头像拼图,让大伙儿看能不能快速找到自己的头像,没想到反响很强烈,引得阵阵惊呼与膜拜,没有料到。

21230
来自专栏崔庆才的专栏

Python 的高级特征你知多少?来对比看看

Python 是一种美丽的语言,它简单易用却非常强大。但你真的会用 Python 的所有功能吗?

14430
来自专栏Python爬虫与数据挖掘

Python 获取微信好友地区、性别、签名信息并将结果可视化

本篇博客是itchat库系列的第三篇文章,它主要实现的功能是获取微信好友地区、性别、签名信息并将结果可视化

10040
来自专栏AI研习社

数据工程师和数据科学家有什么不同

我们最近在Reddit上做了问答活动。有个最常见的问题是数据科学家和数据工程师之间的区别。因此,我们想在这个主题上下写一篇文章来深入探讨下这个话题。

8930
来自专栏Python小屋

Python多线程编程的一个掉进去不太容易爬出来的坑

感谢山东工商学院计算机学院数字媒体技术专业16级郝佳驷和谭泽浩两位同学在上课讲的代码中发现并及时提出文中描述的问题。

24120

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励