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.

 

 

 

 
posted @ 2017-09-17 15:08  扎心了老铁  阅读(13596)  评论(0编辑  收藏  举报