Redis事务
在web/activity中见过事务机制保证发券环节对券码存量校验,这是典型的并发、读写操作的实例。
Redis的事务机制Transaction通过四个命令来完成:MULTI, EXEC, DISCARD and WATCH
,建议精读链接文章对Redis事务机制有详细介绍。
Redis事务机制特性
- 事务(transaction)的定义从
multi
开始,到exec
结束。 - 同一个事务内的多个命令,具有原子性,不会被打断
It can never happen that a request issued by another client is served in the middle of the execution of a Redis transaction. This guarantees that the commands are executed as a single isolated operation.
实例一:multi和exec之间的命令不会被其他并发处理打断,事务原子性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17clientA:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello a
QUEUED
// clientB cocurrently set hello to b
127.0.0.1:6379> get hello
QUEUED
127.0.0.1:6379> exec
1) OK
2) "a"
clientB:
127.0.0.1:6379> set hello b
OK
实例二:注意理解incr和set操作的区别1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18clientA:
127.0.0.1:6379> set hello 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr hello //
QUEUED
// clientB cocurrently set hello to 22
127.0.0.1:6379> get hello
QUEUED
127.0.0.1:6379> exec
1) (integer) 23
2) "23"
// clientB:
127.0.0.1:6379> set hello 22
OK
- 事务内的命令要么全部执行(仅指入带执行的命令队列成功),要么都不执行
Either all of the commands or none are processed, so a Redis transaction is also atomic.
multi
执行之前断开连接,则全部命令不会执行;exec
执行之后,所有命令操作都会被执行。
注意:这里仅仅指命令的执行,但同一事务内命令执行与否,并不意味着执行成功(存在命令执行后,返回异常的情况)。
- 事务期间的错误情况
命令入待执行命令队列失败,此时在
exec
前就会有错误:命令语法错误、内存不足等极端情况, 此时一般会中断事务,自动discard
事务。1
2
3
4
5
6
7
8127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello 1
QUEUED
127.0.0.1:6379> lget hello // 非法的命令导致错误
(error) ERR unknown command 'lget'
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.在执行
exec
后部分命令可能失败:即使部分命令失败,其他命令仍将正常执行完成
实例三:在执行exec
后部分命令可能失败1
2
3
4
5
6
7
8
9
10
11127.0.0.1:6379> multi
OK
127.0.0.1:6379> set hello 1
QUEUED
127.0.0.1:6379> lset hello 0 1 // 该命令可被执行,但执行失败
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get hello // 事务中的第一步执行成功
"1"
CAS(Check-And-Set)支持
watch
已监视的key,只允许在当前终端的multi
和exec
见被修改,其他情况的修改都将导致watch和此事务的失败。
CAS的实现主要通过watch
命令完成,也就是说在watch
一个key后,其他终端修改此key的值时,都将触发当前事务的失败。
实例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// clientA
127.0.0.1:6379> watch hello
OK
127.0.0.1:6379> multi
OK
// clientB cocurrently set hello to 33
127.0.0.1:6379> set hello 2
QUEUED
127.0.0.1:6379> exec // watched hello changed, exec will fail
(nil)
// clientB:
127.0.0.1:6379> set hello 33
OK
// redis monitor
1496247607.225085 [0 127.0.0.1:43168] "watch" "hello"
1496247611.476334 [0 127.0.0.1:43168] "multi"
1496247626.275239 [0 127.0.0.1:43216] "set" "hello" "33"
1496247632.092549 [0 127.0.0.1:43168] "exec"
注意:当前终端的watch
和multi
之间对key的修改,也会触发事务的失败详细。
predis的CAS使用
Laravel项目中有phpredis和predis两个扩展支持redis, phpredis
是C实现扩展,需要编译安装,性能卓越;predis
是php实现扩展,安装方便(composer install
),线上部署无需变更
这里对predis
下使用Redis的事务机制展开说明
Check-And-Set:
示例代码:https://github.com/nrk/predis/blob/v1.1/examples/transaction_using_cas.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function zpop($client, $key)
{
$element = null;
$options = array(
'cas' => true, // Initialize with support for CAS operations
'watch' => $key, // Key that needs to be WATCHed to detect changes
'retry' => 3, // Number of retries on aborted transactions, after
// which the client bails out with an exception.
);
$client->transaction($options, function ($tx) use ($key, &$element) {
@list($element) = $tx->zrange($key, 0, 0);
if (isset($element)) {
$tx->multi(); // With CAS, MULTI *must* be explicitly invoked.
$tx->zrem($key, $element);
}
});
return $element;
}
$client = new Predis\Client($single_server);
$zpopped = zpop($client, 'zset');
echo isset($zpopped) ? "ZPOPed $zpopped" : 'Nothing to ZPOP!', PHP_EOL;
在cas模式下,predis使用的注意事项:
- 必须显式的调用
$tx->multi()
- 在调用
multi
之前必须显式的调用任何命令$tx->validRedisCmd()
,否则直接调用multi并不会执行multi指令 - cas的目的是监视某个key在此期间不会变化,所以应用场景应该是:取某个key的值,做一些操作,再对此key执行其他命令
这里不合适的引用下php.net对memcached::cas()的解释来辅助理解Memcached::cas() performs a “check and set” operation, so that the item will be stored only if no other client has updated it since it was last fetched by this client.
cas = check and set, 某项将在没有其他终端修改的前提下保存,即当前终端是最新一次获取此项的值
实例:Lravel下通过predis的事务的CAS(check-and-set)机制保证竞争条件下的数据一致性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30use Illuminate\Support\Facades\Redis;
$keyTotal = 'key-total';
$keyOneUser = 'key-one-user';
$total = 10;
$oneUserLimit = 2;
$options = [
"cas" => true,
"watch" => [$keyTotal, $keyOneUser], // watch multi keys
"retry" => 3
];
try {
$redis = Redis::connection('redis_conn');
$transFunc = function ($tx) use ($keyTotal, $keyOneUser, $total, $oneUserLimit) {
$curTotal = $tx->get($keyTotal);
$curLimit = $tx->get($keyOneUser);
if ($curTotal >= $total || $curLimit >= $oneUserLimit) {
\Log::info('[Rule ruleUpdateCasTransaction] out of limit, ' . "$keyTotal:$curTotal>=$total,$keyOneUser:$curLimit>=$oneUserLimit");
return false;
}
$tx->multi(); // cas模式下必须调用multi,调用multi前必须执行其他redis命令初始化配置
$tx->incr($keyTotal);
$tx->incr($keyOneUser);
};
$redis->transaction($options, $transFunc);
return true;
} catch (\Exception $e) {
\Log::error("[Rule ruleCntDec]Fail to $type for total and limit, e:" . $e);
return false;
}
Redis monitor
命令下的cas事务执行过程:1
2
3
4
5
6
7
8
91496242439.690785 [0 127.0.0.1:43126] "AUTH" "e3b8d1e0xxxxxxxxxxxxxxVpEi"
1496242439.691136 [0 127.0.0.1:43126] "SELECT" "4"
1496242439.691356 [4 127.0.0.1:43126] "WATCH" "key-total" "key-one-user"
1496242439.691493 [4 127.0.0.1:43126] "GET" "key-total"
1496242439.691643 [4 127.0.0.1:43126] "GET" "key-one-user"
1496242439.691834 [4 127.0.0.1:43126] "MULTI"
1496242439.692136 [4 127.0.0.1:43126] "INCR" "key-total"
1496242439.692146 [4 127.0.0.1:43126] "INCR" "key-one-user"
1496242439.692152 [4 127.0.0.1:43126] "EXEC"