Loading

redis-08-进阶之事务

redis-08-进阶之事务

概述

Redis 的事务是一组命令的集合,理论上要么都被执行,要么都不被执行。最常见的场景就是银行转账,从 A 取出100 元。给 B 增加 100 元,如果这一过程被打断就可能会出现 A 减少了 100 元但是 B 没有增加 100 元的情况。

事务的原理就是将属于一个事务的命令发送给 Redis,然后再让 Redis 依次执行这些命令。就像sq|那样,事务的开始和执行都有自己的标志,在 Redis 中以MULTI为标志代表事务开始,以EXEC为标志代表事务执行。

键入 MULTI 后 Redis 会返回 OK 表示准备就绪,随后每输入的条指令都会返回 QUEUED 表示命令进入事务队列,正排队等待被执行,最后键入 EXEC, Redis 会返回按照前后顺序排列的指令队列并且伺机行。


错误处理

但是 Redis 的事务性没有那么强,从它没有 rollback 这一点就可以看出!

没有回滚的话如果一个事务中的某个命令出现了错误,其它的命令怎么办?不支持回滚是否意味着将错就错?那这样存在瑕疵的事务还有什么意义?

对于不支持回滚,官方是这样说的:


Why Redis does not support roll backs?

If you have a relational databases background, the fact that Redis commands can fail during a transaction, but still Redis will execute the rest of the transaction instead of rolling back, may look odd to you.

However there are good opinions for this behavior:

  • Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
  • Redis is internally simplified and faster because it does not need the ability to roll back.

An argument against Redis point of view is that bugs happen, however it should be noted that in general the roll back does not save you from programming errors. For instance if a query increments a key by 2 instead of 1, or increments the wrong key, there is no way for a rollback mechanism to help. Given that no one can save the programmer from his or her errors, and that the kind of errors required for a Redis command to fail are unlikely to enter in production, we selected the simpler and faster approach of not supporting roll backs on errors.


不支持回滚的原因大致有二:

  • 错误只会由语法问题导致,语法错误不应该出现在生产环境中,而是在开发环境中就被侦测出来;
  • 回滚成本太高。

事务执行

创建简单事务

@Test
public void submitTransaction() {
    // 提交事务申请,返回一个事务对象
    Transaction transaction = j.multi();
    assertNotNull(transaction);

    Response<Long> response = transaction.sadd("user:1:following", "2");
    assertNotNull(response);	// respongse 内容为: "reponse long"
    response = transaction.sadd("user:1:following", "1");
    assertNotNull(response);

    // 执行事务,返回事务的执行结果,1 代表成功,0 代表失败
    List<Object> commands = transaction.exec();
    assertNotNull(commands);
    commands.forEach(Assert::assertNotNull);
}

放弃事务

@Test
public void discardTransaction() {
    // 提交事务申请,返回一个事务对象
    Transaction transaction = j.multi();
    assertNotNull(transaction);

    Response<Long> response = transaction.sadd("user:1:following", "2");
    assertNotNull(response);
    response = transaction.sadd("user:1:following", "1");
    assertNotNull(response);

    // 放弃事务,返回 OK
    String status = transaction.discard();
    assertEquals("OK", status);
}

错误处理

直接语法错误

由于我使用 Jedis,在上传命令前会在本地进行一次基于函数规则的语法校验,比如在下面这条指令中我试图将 key 值设置为空,Jedis 就返回了对应的错误信息,减少了进一步出现的语法错误的可能。

response = transaction.set("key", null);

// redis.clients.jedis.exceptions.JedisDataException: value sent to redis cannot be null

为了达到效果,我又直接在 redis-cli 里面测试了一次:

image-20211008092956586


可以看到对于明显的语法错误 Redis 非常人性化的放弃了我整个事务的命令。


隐蔽语法错误

在下面的代码片段中,我用集合操作对普通键值对进行了操作,初步语法检查执行正确却在最后执行的时候出现错误并且 Redis 也给出了相应的提示,但是!在它之后的代码片段仍然被执行了,因为最后我得到的 key 的值是 new_new_value!!!

语法错误基本上能够由 Redis 检查出来,无论是直接的还是隐蔽的,这些事情都应该在开发环境中完成,但是,对于逻辑上的错误,Redis 爱莫能助,这类错误才是我们最应该警惕和注意的!

@Test
public void transactionErrorHandleCovert() {
    // 入队时产生语法错误,整个事务不被执行
    Transaction transaction = j.multi();
    assertNotNull(transaction);

    Response<String> response1 = transaction.set("key", "value");
    assertNotNull(response1);
    assertEquals("Response string", response1.toString());

    Response<Long> response2 = transaction.sadd("key", "new_value");
    assertNotNull(response2);
    assertEquals("Response long", response2.toString());


    Response<String> response3 = transaction.set("key", "new_new_value");
    assertNotNull(response3);
    assertEquals("Response string", response3.toString());

    List<Object> commands = transaction.exec();
    assertNotNull(commands);
    commands.forEach(System.out::println);

    String value = j.get("key");
    assertEquals("new_new_value", value);
}

// OK
// redis.clients.jedis.exceptions.JedisDataException: WRONGTYPE Operation against a key holding the wrong kind of value
// OK

WATCH

WATCH,监察嘛,既然跟事务挂钩,那无非就是跟乐观锁啊什么的有关。

WATCH 命令能够监控一个或者多个键,一旦其中有一个键被改动(客户端修改或者是 Redis 自身的修改,注意,6.0.9 之前 过期的值是不会触发 WATCH 的,也就是说被监控的值过期了也不会影响到后续事务的执行),后续的事务就不会执行,一直持续到 EXEC 命令,先看一个简单的例子:

@Test
public void basicWatch() {
    String status = j.set("key", "1");
    assertEquals("OK", status);

    status = j.watch("key");
    assertEquals("OK", status);

    status = j.set("key", "2");
    assertEquals("OK", status);

    Transaction transaction = j.multi();
    assertNotNull(transaction);

    Response<String> response = transaction.set("key", "3");
    assertNotNull(response);
    assertEquals("Response string", response.toString());

    List<Object> commands = transaction.exec();
    assertNull(commands);	// commands 返回 null,因为没有事务命令被执行

    String value = j.get("key");
    assertEquals("2", value);
}

如果被 WATCH 的值在1事务执行前发生了改变,是 1 个,不是 2 个,那么整个事务都不会被执行!!!因为一个 WATCH 只能阻止一个事务的执行,在多客户端的情况下, 我们往往需要循环重复使用 WATCH 才能达到乐观锁那样的效果。

如果想要放弃对 key 的监控,使用 UNWATCH命令就行,这个命令会结束对所有 key 的监控

乐观锁

WATCH 命令似乎专为 CAS(检查并且设置) 行为而生:

val = GET mykey
val = val + 1
SET mykey $val


WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

后面的代码片段更是 CAS 的标准范式:

未命名文件 (1)

简单的尝试一下:

@Test
public void optimisticLock() {
    String status = j.set("key", "1");
    assertEquals("OK", status);

    status = j.watch("key");
    assertEquals("OK", status);

    String key = j.get("key");
    assertEquals("1", key);

    key = "2";
    Transaction transaction = j.multi();
    Response<String> res = transaction.set("key", key);
    assertNotNull(res);
    assertEquals("Response string", res.toString());

    List<Object> commands = transaction.exec();
    assertNotNull(commands);
    commands.forEach(Assert::assertNotNull);

    String value = j.get("key");
    assertEquals("2", value);
}
posted @ 2021-10-08 11:03  槐下  阅读(82)  评论(0编辑  收藏  举报