目录
1、Redis 事务基础
1.1、基本命令
1.2、事务的执行流程:
1.3、应用
1、事务成功执行
2、事务取消
3、watch命令
4、事务异常
1、命令错误
2、运行时命令错误
1.4、不回滚原因
1.5、jedis实现
2、Redis 事务的特点
2.1、原子性(部分保证)
2.2、隔离性
2.3、持久性
2.4、无一致性保证
3、Redis的Pipeline机制
3.1、定义
3.2、作用
3.3、区别
4、Redis的事务和Lua的区别联系
4.1、原子性保证
4.2、交互次数
4.3、前后依赖
4.4、流程编排
5、Lua脚本如何保证原子性
前言
关于更多redis介绍,可参考:对Redis组件的深入探讨_redis 磁盘 内存-CSDN博客https://blog.csdn.net/weixin_50055999/article/details/147757520?spm=1011.2415.3001.5331
Redis 事务提供了一种将多个命令打包,然后一次性、按顺序执行的机制。虽然 Redis 的事务与关系型数据库的事务有所不同,但在特定场景下非常有用。
Redis的单个命令具有原子性,但在处理多个命令时需依赖事务功能。事务通过multi、exec、discard和watch命令实现命令序列的执行。
如下图所示:
当事务中出现错误,如命令语法错误,事务会被拒绝,但已正确执行的命令不会回滚,这与传统数据库事务的回滚机制不同。
注意:Redis不支持回滚是因为错误应由程序员在开发阶段避免,保持系统简单高效。
Redis的事务涉及到四个命令:multi(开启事务),exec(执行事务),discard(取消事务),watch(监视)。
MULTI
:标记事务开始
EXEC
:执行事务中的所有命令
DISCARD
:取消事务,放弃执行
WATCH
:监视键,实现 CAS 操作
UNWATCH
:取消所有键的监视
如下图所示:
MULTI # 开始事务
SET key1 value1
SET key2 value2
GET key1
EXEC # 执行事务
tom和mic各有1000元,tom向mic转账100元。
127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set mic 1000
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby tom 100
QUEUED
127.0.0.1:6379> incrby mic 100
QUEUED
127.0.0.1:6379> exec
1) (integer) 900
2) (integer) 1100
127.0.0.1:6379> get tom
"900"
127.0.0.1:6379> get mic
"1100"
通过multi的命令开启事务,multi执行后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即执行,而是被放到一个队列中。
当exec命令被调用时,所有队列中的命令才会被执行。如果没有执行exec命令,所有的命令都不会被执行。
如果中途不想执行事务了,可以使用discard命令清空事务队列,放弃执行。
127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set mic 1000
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby tom 100
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get tom
"1000"
为了防止事务过程中某个key的值被其他客户端请求修改,带来非预期的结果,在Redis中海提供了一个watch命令。
可以用watch监视一个或多个key,如果开启事务之后,至少有一个被监视key键在exec执行之前被修改了,那么整个事务都会被取消(key提前过期除外)。可以用unwatch取消。
如下所示:
作用:
当多个客户端更新变量的时候,会跟原值做比较,只有它没有被其他线程修改的情况下,才更新成新的值。它可以为Redis事务提供CAS乐观锁的行为(Compare and Swap)。
代码示例:
public void incr(String key) {
jedis.watch(key);
Integer num = Integer.valueOf(jedis.get(key));
Transaction multi = jedis.multi();
multi.set(key, String.valueOf(num + 1));
List
一般乐观锁都需要配合重试机制来实现,所以这里 watch 命令也可以配合重试机制来实现:
事务执行遇到的问题一般分为两种,一种是在执行exec之前发生错误,一种是在执行exec之后发生错误。
比如:入队的命令存在语法错误,包括参数数量、参数名等等(编译器错误)。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set zhangsan 123
QUEUED
127.0.0.1:6379> set lisi 321
QUEUED
127.0.0.1:6379> hset wangwu 333
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
结果:hset我们的语法有问题,事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。
比如:对String使用了hash命令,参数个数正确,但是数据类型错误,这是一种运行时错误。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> hset a aa bb
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 a
"1"
第一条命令是执行成功的,也就是在发生异常的情况下,之后错误的命令没有被执行,正确的命令还是执行成功,并没有被回滚。
根据上面的例子,显然不符合我们对原子性的定义。也就是没办法用Redis的这种事务机制来实现原子性,保证数据一致。
Redis命令只会因为错误的语法而失败,从实用性的角度来讲,失败的命令是由代码错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
客户端jedis执行事务的代码:
public static void testTran() {
// 开启事务
Transaction transaction = jedis.multi();
// 执行事务内的Redis命令
transaction.set("tran1", "hello");
transaction.set("tran2", "world");
// 提交事务
List
注意:
开启事务之后,执行命令的对象不是Jedis对象,而是Transaction对象,否则会抛出下面的异常:
Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException:
Cannot use Jedis when in Multi.
Please use Transation or reset jedis state.
命令队列的原子性:EXEC 命令会原子性地执行所有队列中的命令
无回滚机制:如果某个命令失败,后续命令仍会执行
执行错误处理:
命令入队时出错(如语法错误):整个事务都不会执行
命令执行时出错(如对字符串执行 INCR):只有该命令失败,其他命令仍执行
关于redis的线程模型可参考:关于多线程的Redis模型_redis线程模型-CSDN博客https://blog.csdn.net/weixin_50055999/article/details/147977886?spm=1011.2415.3001.5331
单线程保证:Redis 单线程特性自然提供了隔离性
无隔离级别概念:不像关系型数据库有读未提交、读已提交等隔离级别
取决于 Redis 的持久化配置(RDB/AOF)。可参考:对Redis组件的深入探讨_redis 磁盘 内存-CSDN博客https://blog.csdn.net/weixin_50055999/article/details/147757520?spm=1011.2415.3001.5331
Redis 不提供类似关系型数据库的一致性保证。可参考:
谈谈Redis缓存和数据库一致性的处理方案_redis缓存如何与数据库保持一致-CSDN博客https://blog.csdn.net/weixin_50055999/article/details/147782383?spm=1011.2415.3001.5331
Redis 的 Pipeline 机制是一种用于优化网络延迟的技术,主要用于在单个请求/响应周期内执行多个命令。
在没有 Pipeline 的情况下,每执行一个 Redis 命令,客户端都需要等待服务器响应之后才能发送下一个命令。
这种往返通信尤其在网络延迟较高的环境中会显著影响性能。
在 Pipeline 模式下,客户端可以一次性发送多个命令到 Redis 服务器,而无需等待每个命令的响应。Redis 服务器接收到这批命令后,会依次执行它们并返回响应。
如下图所示:
Pipeline通过减少客户端与服务器之间的往返通信次数,可以显著提高性能,特别是在执行大量命令的场景中。
不同点:
1、pipline模式下:
注意:Pipeline是不保证原子性的。它的多个命令都是独立执行的,并不保证这些命令可以以不可分割的原子操作进行执行。
2、redis事务模式下
但是Redis的事务提供了原子性保障,保证命令执行以不可分割、不可中断的原子性操作进行,而Pipeline则没有原子性保证。
相同点:
如果执行多个命令过程中,有一个命令失败了,其他命令还是会被执行,而不会回滚的。
Redis中,事务和Lua都是保证原子性的手段。
当我们有多个命令要执行,并希望它们以原子性方式执行的时候,就会考虑使用事务或者Lua脚本。
那么它们之间有哪些联系呢?
事务和Lua都是可以保证原子性操作的,指的是不可拆分、不可中断的原子性操作。所以,不管是Redis的事务还是Lua,都没办法回滚,一旦执行过程中有命令失败了,都是不支持回滚的。
但是,Redis的事务在执行过程中,如果有某一个命令失败了,是不影响后续命令的执行的,而Lua脚本中,如果执行过程中某个命令执行失败了,是会影响后续命令执行的。
在Redis的事务执行时,每一条命令都需要和Redis服务器进行一次交互。
可以在Redis事务过程中,在MULTI和EXEC之间发送多个Redis命令给Redis服务器,这些命令会被服务器缓存起来,但并不会立即执行。但是每一条命令的提交都需要进行一次网络交互。
而Lua脚本则不需要,只需要一次性地把整个脚本提交给Redis即可。网络交互比事务要少。
在Redis的事务中,事务内的命令都是独立执行的,并且在没有执行EXEC命令之前,命令是没有被真正执行的,所以后续命令是不会也不能依赖于前一个命令的结果的。
而在Lua脚本中是可以依赖前一个命令的结果的。
Lua脚本中的多个命令是依次执行的,利用前一个命令的结果进行后续的处理。
借助Lua脚本,我们可以实现非常丰富的各种分支流程控制,以及各种运算相关操作。而Redis的事务本身是不支持这些操作的。
原子性在并发编程中和在数据库中是两种不同的概念。
在数据库中,事务的ACID中原子性指的是“要么都执行要么都回滚”。
在并发编程中,原子性指的是“操作不可拆分、不被中断”。
Redis既是一个数据库,又是一个支持并发编程的系统,所以它的原子性有两种。因此,我们需要明确清楚,在“Lua脚本如何保证Redis原子性”的时候,指的到底是哪个原子性。
Lua脚本可以保证原子性,因为Redis会将Lua脚本封装成一个单独的事务,而这个单独的事务会在Redis客户端运行时,由Redis服务器自行处理并完成整个事务。
如果在这个进程中有其他客户端请求的时候,Redis将会把它暂存起来,等到Lua脚本处理完毕后,才会再把被暂存的请求恢复。
这样就可以保证整个脚本是作为一个整体执行的,中间不会被其他命令插入。但是,如果命令执行过程中命令产生错误,事务是不会回滚的,将会影响后续命令的执行。
也就是说,Redis保证保证并发编程中不可拆分、不被中断的执行Lua脚本的原子性,但是不保证脚本中所有操作要么都执行或者都回滚。
总结
Redis 事务提供了一种简单的方式来批量执行命令,虽然不如关系型数据库的事务强大,但在特定场景下非常有用。
参考文章:
1、redis事务详解,带你搞懂redis的事务-CSDN博客https://blog.csdn.net/A_art_xiang/article/details/130980892?ops_request_misc=&request_id=&biz_id=102&utm_term=redis%E4%BA%8B%E5%8A%A1%E6%9C%BA%E5%88%B6&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-130980892.142^v102^pc_search_result_base1&spm=1018.2226.3001.4187
2、【Redis经典面试题七】Redis的事务机制是怎样的?_面试题redis 事务-CSDN博客https://blog.csdn.net/qq_38196449/article/details/144876228?ops_request_misc=%257B%2522request%255Fid%2522%253A%25221da83ae9aaa0d8e39b1bf672f597f999%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=1da83ae9aaa0d8e39b1bf672f597f999&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-3-144876228-null-null.142^v102^pc_search_result_base1&utm_term=redis%E4%BA%8B%E5%8A%A1%E6%9C%BA%E5%88%B6&spm=1018.2226.3001.4187