Redis事务详解

本文来说下redis事务相关的话题。


概述

Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有2种解决方法:

1.客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。

2.服务器角度,利用setnx实现锁。

MULTI,EXEC,DISCARD,WATCH 四个命令是 Redis 事务的四个基础命令。其中:

MULTI,告诉 Redis 服务器开启一个事务。注意,只是开启,而不是执行 。
EXEC,告诉 Redis 开始执行事务 。
DISCARD,告诉 Redis 取消事务。
WATCH,监视某一个键值对,它的作用是在事务执行之前如果监视的键值被修改,事务会被取消。

可以利用watch实现cas乐观锁


Redis事务命令

官网:https://redis.io/commands#transactions

命令说明备注
multi开启事务命令,之后的命令就进入队列,而不会马上被执行在事务生存期间,所有的 Redis 关于数据结构的命令都会入队
watch key1 [key2 …]监听某些键,当被监听的键在事务执行前被修改,则事务会被回滚使用乐观锁
unwatch key1 [key2 …]取消监听某些键
exec执行事务,如果被监听的键没有被修改,则采用执行命令,否则就回滚命令在执行事务队列存储的命令前, Redis 会检测被监听的键值对有没有发生变化,如果没有则执行命令 ,否则就回滚事务
discard回滚事务回滚进入队列的事务命令,之后就不能再用 exec命令提交了

事务基本使用

事务在其他语言中,一般分为以下三个阶段

  • 开启事务——Begin Transaction
  • 执行业务代码,提交事务——Common Transaction
  • 业务处理中出现异常,回滚事务——Rollback Transaction

以 Java 中的事务执行为例

// 开启事务
begin();
try {
    //......
    // 提交事务
    commit();
} catch(Exception e) {
    // 回滚事务
    rollback();
}

Redis 中的事务从开始到结束也是要经历三个阶段

  • 开启事务
  • 命令入列
  • 执行事务/放弃事务

其中,开启事务使用 multi命令,事务执行使用 exec命令,放弃事务使用 discard命令


开启事务

multi 命令用于开启事务,实现代码如下:

> multi
OK

multi 命令可以让客户端从非事务模式状态,变为事务模式状态,如下图所示:

在这里插入图片描述
注意:multi 命令不能嵌套使用,如果已经开启了事务的情况下,再执行 multi 命令,会提示如下错误:

(error) ERR MULTI calls can not be nested

执行效果,如下代码所示

127.0.0.1:6379> multi
OK
127.0.0.1:6379> multi
(error) ERR MULTI calls can not be nested
127.0.0.1:6379>

当客户端是非事务状态时,使用 multi 命令,客户端会返回结果 OK ,如果客户端已经是事务状态,再执行 multi 命令会 multi 命令不能嵌套的错误,但不会终止客户端为事务的状态,如下图所示:

在这里插入图片描述


命令入列

客户端进入事务状态之后,执行的所有常规 Redis 操作命令(非触发事务执行或放弃和导致入列异常的命令)会依次入列,命令入列成功后会返回 QUEUED ,如下代码所示:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k v
QUEUED
127.0.0.1:6379> get k
QUEUED
127.0.0.1:6379>

执行流程如下图所示

在这里插入图片描述
注意:命令会按照先进先出(FIFO)的顺序出入列,也就是说事务会按照命令的入列顺序,从前往后依次执行。


执行事务/放弃事务

执行事务的命令是 exec ,放弃事务的命令是 discard 。 执行事务示例代码如下:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set aa bb
QUEUED
127.0.0.1:6379> exec
1) OK
127.0.0.1:6379> get aa
"bb"
127.0.0.1:6379>

放弃事务示例代码如下

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set cc dd
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get cc
(nil)
127.0.0.1:6379>

执行流程如下图所示

在这里插入图片描述


事务错误&回滚

事务执行中的错误分为以下三类:

  • 执行时才会出现的错误(简称:执行时错误);
  • 入列时错误,不会终止整个事务;
  • 入列时错误,会终止整个事务。

执行时错误

示例代码如下

> get k
"v"
> multi
OK
> set k v2
QUEUED
> expire k 10s
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range
> get k
"v2"

执行命令解释如下图所示

在这里插入图片描述

从以上结果可以看出,即使事务队列中某个命令在执行期间发生了错误,事务也会继续执行,直到事务队列中所有命令执行完成。


入列错误不影响事务

示例代码如下

> get k
"v"
> multi
OK
> set k v2
QUEUED
> multi
(error) ERR MULTI calls can not be nested
> exec
1) OK
> get k
"v2"

执行命令解释如下图所示

在这里插入图片描述
可以看出,重复执行 multi 会导致入列错误,但不会终止事务,最终查询的结果是事务执行成功了。除了重复执行 multi 命令,还有在事务状态下执行 watch 也是同样的效果,下文会详细讲解关于 watch 的内容。


入列错误导致事务结束

示例代码如下

> get k
"v2"
> multi
OK
> set k v3
QUEUED
> set k
(error) ERR wrong number of arguments for 'set' command
> exec
(error) EXECABORT Transaction discarded because of previous errors.
> get k
"v2"

执行命令解释如下图所示

在这里插入图片描述


为什么不支持事务回滚

Redis 官方文档的解释如下

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.

大概的意思是,作者不支持事务回滚的原因有以下两个

  • 他认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
  • 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。

这里不支持事务回滚,指的是不支持运行时错误的事务回滚。


监控

watch 命令用于客户端并发情况下,为事务提供一个乐观锁(CAS,Check And Set),也就是可以用 watch 命令来监控一个或多个变量,如果在事务的过程中,某个监控项被修改了,那么整个事务就会终止执行。 watch 基本语法如下:

watch key [key …]

watch 示例代码如下

> watch k
OK
> multi
OK
> set k v2
QUEUED
> exec
(nil)
> get k
"v"

从以上命令可以看出,如果 exec 返回的结果是 nil 时,表示 watch 监控的对象在事务执行的过程中被修改了。从 get k 的结果也可以看出,在事务中设置的值 set k v2 并未正常执行。 执行流程如下图所示:

在这里插入图片描述
注意: watch 命令只能在客户端开启事务之前执行,在事务中执行 watch 命令会引发错误,但不会造成整个事务失败,如下代码所示:

> multi
OK
> set k v3
QUEUED
> watch k
(error) ERR WATCH inside MULTI is not allowed
> exec
1) OK
> get k
"v3"

执行命令解释如下图所示:

在这里插入图片描述
unwatch 命令用于清除所有之前监控的所有对象(键值对)。 unwatch 示例如下所示:

> set k v
OK
> watch k
OK
> multi
OK
> unwatch
QUEUED
> set k v2
QUEUED
> exec
1) OK
2) OK
> get k
"v2"

可以看出,即使在事务的执行过程中,k 值被修改了,因为调用了 unwatch 命令,整个事务依然会顺利执行。


事务在程序中使用

以下是事务在 Java 中的使用,代码如下

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TransactionExample {
    public static void main(String[] args) {
        // 创建 Redis 连接
        Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
        // 设置 Redis 密码
        jedis.auth("xxx");
        // 设置键值
        jedis.set("k", "v");
        // 开启监视 watch
        jedis.watch("k");
        // 开始事务
        Transaction tx = jedis.multi();
        // 命令入列
        tx.set("k", "v2");
        // 执行事务
        tx.exec();
        System.out.println(jedis.get("k"));
        jedis.close();
    }
}

Redis事务番外篇

你可能已经注意到「事务」这个词。在学习数据库原理的时候有提到过事务的 ACID,即原子性、一致性、隔离性、持久性。接下来,看看 Redis 事务是否支持 ACID

原子性,即一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。Redis 事务不支持原子性,最明显的是 Redis 不支持回滚操作。一致性,在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这一点,Redis 事务能够保证。

隔离性,当两个或者多个事务并发访问(此处访问指查询和修改的操作)数据库的同一数据时所表现出的相互关系。Redis 不存在多个事务的问题,因为 Redis 是单进程单线程的工作模式。

持久性,在事务完成以后,该事务对数据库所作的更改便持久地保存在数据库之中,并且是完全的。Redis 提供两种持久化的方式,即 RDB 和 AOF。RDB 持久化只备份当前内存中的数据集,事务执行完毕时,其数据还在内存中,并未立即写入到磁盘,所以 RDB 持久化不能保证 Redis 事务的持久性。再来讨论 AOF 持久化,我在《深入剖析 Redis AOF 持久化策略》中讨论过:Redis AOF 有后台执行和边服务边备份两种方式。后台执行和 RDB 持久化类似,只能保存当前内存中的数据集;边备份边服务的方式中,因为 Redis 只是每间隔 2s 才进行一次备份,因此它的持久性也是不完整的!

当然,我们可以自己修改源码保证 Redis 事务的持久性,这不难。

还有一个亮点,就是 check-and-set CAS。一个修改操作不断的判断X 值是否已经被修改,直到 X 值没有被其他操作修改,才设置新的值。Redis 借助 WATCH/MULTI 命令来实现 CAS 操作的。

实际操作中,多个线程尝试修改一个全局变量,通常我们会用锁,从读取这个变量的时候就开始锁住这个资源从而阻挡其他线程的修改,修改完毕后才释放锁,这是悲观锁的做法。相对应的有一种乐观锁,乐观锁假定其他用户企图修改你正在修改的对象的概率很小,直到提交变更的时候才加锁,读取和修改的情况都不加锁。一般情况下,不同客户端会访问修改不同的键值对,因此一般 check 一次就可以 set 了,而不需要重复 check 多次。


本文小结

本文详细介绍了redis事务相关的知识与内容。

  • 6
    点赞
  • 21
    收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 1

打赏作者

wh柒八九

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值