Redis实战 - 5事务:multi、exec和watch

时间:2021-08-08 10:48:29

介绍

  1. redis的目标的是: 简洁,高效,由于事务本身就是一个很复杂的东西,所有我们不能把事务做的太复杂。
  • DISCARD 取消事务,放弃执行事务块内的所有命令。

  • EXEC 执行所有事务块内的命令。

  • MULTI 标记一个事务块的开始。

  • UNWATCH 取消 WATCH 命令对所有 key 的监视。

  • WATCH key [key ...] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

    MULTI 和 EXEC

  127.0.0.1:6379> multi
OK
127.0.0.1:6379> lpush fruits orange
QUEUED
127.0.0.1:6379> lpush fruits nut
QUEUED
127.0.0.1:6379> lpush fruits apple
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 2
3) (integer) 3

我们发现命令是一起执行的,如果说我的某一条命令执行失败,会回滚吗?

答案是不会回滚。看看下面的原子性。

原子性

事务的原子性是指要么事务全部成功,要么全部失败,那么 Redis 事务执行是原子性的么? 下面我们来看一个特别的例子。

> multi
OK
> set books iamastring # 执行成功
QUEUED
> incr books # 执行失败
QUEUED
> set poorman iamdesperate # 执行成功
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
> get books
"iamastring"
> get poorman
"iamdesperate

  上面的例子是事务执行到中间遇到失败了,因为我们不能对一个字符串进行数学运算,事务在遇到指令执行失败后,后面的指令还继续执行,所以 poorman 的值能继续得到设置。

  到这里,你应该明白 Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。

Watch

我在执行lpush的时候,lpush被其他人改变了。

需求:在写multi的时候,不可以有其他的命令更改 “队列”中的集合。

出现并发问题,因为有多个客户端会并发进行操作。我们可以通过 Redis 的分布式锁来避免冲突,这是一个很好的解决方案。分布式锁是一种悲观锁,那是不是可以使用乐观锁的方式来解决冲突呢?

Redis 提供了这种 watch 的机制,它就是一种乐观锁。有了 watch 我们又多了一种可以用来解决并发修改的方法。 watch 的使用方式如下:

127.0.0.1:6379> watch msg
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set msg "hello wolrd"
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get msg
"12345"
127.0.0.1:6379>

注意事项

Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错。

.net操练,在StackExchange.Redis又该怎么做?

更复杂的事实是StackExchange.Redis使用的是多路复用器的方式。

我们不能只让并发调用方发布 WATCH / UNWATCH / MULTI / EXEC / DISCARD:这应该是混合在一起的。所以一个额外的抽象被给出:另外会让使事情更简单准确:约束。约束是预定义测试包括 WATCH 某种类型的测试并对结果进行检查。如果所有的约束都通过了,那么要么是以 MULTI / EXEC 发布(从事务开始,到执行整个事务块);要么是以 UNWATCH 发布(取消 WATCH 命令对所有 key 的监视)。阻止命令于其它调用方被混合在一起;所以例子可以是:

注意:从 CreateTransaction 返回的对象最后都是调用异步方法来执行命令(Execute方法最终也是调用ExecuteAsync,具体可以看源码):由于不知道每个操作的结果,除非在 Execute 或 ExecuteAsync 操作完成后。如果操作没有被执行,所有的Task 将被标记为取消,否则在命令执行后你可以获取每个正常的结果。

       static void Transaction()
{
using (ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379"))
{
IDatabase db = redis.GetDatabase();
ITransaction tran = db.CreateTransaction();
//为该事务添加先决条件
//强制指定指定的哈希字段不能存在。
tran.AddCondition(Condition.HashNotExists("transactionDemo", "UniqueID"));
tran.HashSetAsync("transactionDemo", "UniqueID","Unnn");
bool committed = tran.Execute();
Console.WriteLine(committed); //第1次执行,true,因为不存在,后面执行false,因为存在
}
}

通过 When 的内置操作

还应该注意的是,Redis已经为我们预料到了许多常见的场景(特别是:key/hash的存在,就像上面一样),还有单操作(single-operation)原子命令的存在。 通过 When 来访问,所以前面的示例也可以这样来实现:

var newId = CreateNewId();
bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists);

注意:When.NotExists 会使用命令 HSETNX 而不会使用 HSET

HSETNX:如果字段已经存在于哈希表中,操作无效。