Redis之事务操作
Posted 蚂蚁小哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis之事务操作相关的知识,希望对你有一定的参考价值。
一:基本介绍
1:事务的基本特性
① 原子性(atomicity):
事务是一个原子操作,有一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用
② 一致性(consistency):
一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中
③ 隔离性(isolation):
可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏
④ 持久性(durability):
一旦事务完成被提交,它对数据库中的数据操作是永久性的,反之发生什么系统错误,它的结果都不应该受到影响。
通常情况下,事务的结果被写到持久化存储器中
2:Redis事务的命令介绍
multi:开启事务
exec:结束事务并按照添加的事务内命令依次执行
discard:取消事务,放弃执行事务块中的所以命令
如果开启事务(multi),并写了几条redis语句的话,我们不想再要这个事务了,包括之前写的语句都不要的话,
我们可以使用discard语句关闭事务
watch:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
(添加乐观锁,就像mysql里面的乐观锁version)
unwatch:取消监控所以的key(取消加乐观锁)
二:Redis事务命令使用
1:开启关闭事务multi和exec
从1.2.0版本开始,redis引入了MULTI和EXEC指令,MULTI标志着一个事务的开始,后面的命令暂时不会执行,而是会存到队列中,等到EXEC执行之后,队列中的命令才会依次序执行
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name jack
QUEUED
127.0.0.1:6379(TX)> set address anhui
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
127.0.0.1:6379> get name
"jack"
127.0.0.1:6379> get address
"anhui"
执行MULTI总是会返回OK,标示一个事务开始了,后面进来的指令并不会马上执行,而是返回"QUEUED",这表示命令已经被服务器接受并且暂时保存起来,最后输入EXEC命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了两个OK,这里返回的结果与发送的命令是按顺序对应的,这说明这次事务中的命令全都执行成功了
2:取消事务discard
从2.0.0版本开始redis引入了DISCARD命令,其作用是刷新事务中先前排队的所有命令,并将连接状态恢复正常。就是清除之前存在队列中的所有指令,然后直接结束该事务
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name jack
QUEUED
127.0.0.1:6379(TX)> discard -- 执行discard则恢复正常状态(所以后面执行exec会报错)
OK
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> exec
(error) ERR EXEC without -- MULTI不带MULTI的错误EXEC
127.0.0.1:6379>
可以看出当我们执行EXEC指令时报错了:在没有MULTI的前提下执行了EXEC命令。代表当前环境中已经没有了事务,而这正是DISCARD做到的
3:监控watch和unwatch
Redis事务中watch是一个重要的命令,使用此命令可以监控一个或多个key,被监控的key就类似一个乐观锁,一旦被监控的key被更新或者删除都会触发回滚机制
Redis官方:WATCH is used to provide a check-and-set (CAS) behavior to Redis transactions.
从2.2.0版本开始redis引入了WATCH和UNWATCH命令。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,EXEC命令执行完之后被监控的键会自动被UNWATCH)。UNWATCH的作用是取消WATCH命令对多有key的监控,所有监控锁将会被取消。
乐观锁:
就像他的名字,不会认为数据不会出错,他不会为数据上锁,但是为了保证数据的一致性,他会在每条记录的后面添加一个
标记(类似于版本号),假设A 获取K1这条标记,得到了k1的版本号是1,并对其进行修改,这个时候B也获取了k1这个数据,
当然,B获取的版本号也是1,同样也对k1进行修改,这个时候,如果B先提交了,那么k1的版本号将会改变成2,这个时候,如果A
提交数据,他会发现自己的版本号与最新的版本号不一致,这个时候A的提交将不会成功,A的做法是重新获取最新的k1的数据,
重复修改数据、提交数据。
悲观锁:
这个模式将认定数据一定会出错,所以她的做法是将整张表锁起来,这样会有很强的一致性,但是同时会有极低的并发性
(常用语数据库备份工作,类似于表锁)
案例A:在事务开始后使用watch
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set name zhansgan
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> watch name
(error) ERR WATCH inside MULTI is not allowed
127.0.0.1:6379(TX)> set name jack
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
127.0.0.1:6379> get name
"jack"
从上例可以看到Redis不允许在事务内部使用WATCH命令,会报错,但是即使使用了WATCH也不会因为这个错误导致事务中止,事务照常执行
案例B:同一客户端下,在WATCH之后MULTI之前改变被监视的key
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> watch name
OK
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set address anhui
QUEUED
127.0.0.1:6379(TX)> set name jack
QUEUED
127.0.0.1:6379(TX)> exec
(nil) -- 代表事务已经被回滚了,监控的key被改变了
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> get address
(nil)
可以看出我在开启事务前监控了key为"name"的键,在开启事务后对key为“name”的键进行了更改;这就会导致回滚,所以事务里的全部操作都会恢复事务前最初的状态
案例C:不同客户端下,一个客户端先监控一个键进入事务,然后另一个客户端改变这个别监控键的值(序号是命令执行顺序)
由于WATCH命令的作用只是当被监控的键被修改后取消之后的事务,并不能保证其他客户端不修改监控的值,所以当EXEC命令执行失败之后需要手动重新执行整个事务
取消监控:
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> watch name -- 开启监控
OK
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> unwatch -- 取消全部监控
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name jack
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
127.0.0.1:6379> get name
"jack"
三:Redis事务错误处理
在事务期间可能会遇到两种命令错误:
1:一个命令可能排队失败,所以在调用EXEC之前可能会出错。 例如,命令可能在语法上是错误的(参数数量错误,命令名称错误,...),
或者可能存在诸如内存不足情况之类的关键条件(如果服务器被配置为使用 maxmemory 指令具有内存限制)
2:调用 EXEC 后命令可能会失败,例如因为我们对具有错误值的键执行了操作(例如对字符串值调用列表操作)
1:入队错误
遇到语法错误的会回滚全部操作
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name zhangsan -- 入列成功
QUEUED
127.0.0.1:6379(TX)> set address -- 入队语法错误
(error) ERR wrong number of arguments for \'set\' command
127.0.0.1:6379(TX)> set age 22 -- 入列成功
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
-- 由于前面的错误,EXECABORT事务被丢弃(回滚事务)
2:运行时错误
不会回滚整个事务,执行到失败的命令后抛出错误继续往下执行队列里的命令
127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set name zhangsan QUEUED 127.0.0.1:6379(TX)> set age 22 QUEUED 127.0.0.1:6379(TX)> incr name -- 运行是会出现问题 无法对 字符串数据 进行+1 QUEUED 127.0.0.1:6379(TX)> incr age QUEUED 127.0.0.1:6379(TX)> exec 1) OK 2) OK 3) (error) ERR value is not an integer or out of range 4) (integer) 23 127.0.0.1:6379>
四:为什么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 追求的简单高效的设计主旨不符合。
这里不支持事务回滚,指的是不支持运行时错误的事务回滚。
五:SpringBoot操作Redis事务
1:错误用法
//注入键值都为String对象的RedisTemplate对象 @Autowired private StringRedisTemplate stringRedisTemplate; // 错误示范 @Test void redisTemplateBase() //开启事务 stringRedisTemplate.multi(); //获取String类型的操作 ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue(); opsForValue.set("name","zhangsan"); opsForValue.set("age","22"); //关闭事务并获取每个命令执行结果 List<Object> exec = stringRedisTemplate.exec(); exec.forEach(System.out::println); org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR EXEC without MULTI
//说的是我们在执行到 EXEC 命令时 并没有发现之前开启事务MULIT
按照正常的思维我们要使用RedisTemplate来操作事务会按照上面的方式,但是最终会出现错误;
2:错误原因
在执行 EXEC 命令之前,没有执行 MULTI 命令。这很奇怪,我们明明在测试方法的第一句就执行了 MULTI。通过追踪 multi、exec 等方法,我们可以看到如下的执行源码(spring-data-redis):
@Nullable public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it"); Assert.notNull(action, "Callback object must not be null"); RedisConnectionFactory factory = getRequiredConnectionFactory();
// enableTransactionSupport代表是否开启事务支持;默认是fasle RedisConnection conn = RedisConnectionUtils.getConnection(factory, enableTransactionSupport); ...... 省略部分
//注入键值都为String对象的RedisTemplate对象 @Autowired private StringRedisTemplate stringRedisTemplate; @Test void redisTemplateBase() //开启事务支持 stringRedisTemplate.setEnableTransactionSupport(true); //开启事务 stringRedisTemplate.multi(); //进行命令操作添加数据 stringRedisTemplate.opsForValue().set("address","anhui"); //结束事务 List<Object> exec = stringRedisTemplate.exec(); System.out.println(exec);
网上大部分都是这种方式解决的,但我没有细究,应该是老版本的是可以这样的吧;但是下面我将使用来解决
3:开启事务并操作Redis
redisTemplate直接调用opfor..来操作redis数据库,每执行一条命令是要重新拿一个连接,因此很耗资源,让一个连接直接执行多条语句的方法就是使用SessionCallback,同样作用的还有RedisCallback,但不常用。这样在同一个连接里就可以使用Redis事务
//注入键值都为String对象的RedisTemplate对象 @Autowired private StringRedisTemplate stringRedisTemplate; @Test void redisTemplateBase() // 推荐使用 SessionCallback 因为对此有封装,而 RedisCallback 不推荐;;在这内部可以执行事务 Object dataA = stringRedisTemplate.execute(new SessionCallback<Object>() @Override public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException //把RedisOperations接口赋值给StringRedisTemplate(字符串的K,V) StringRedisTemplate srt = (StringRedisTemplate) operations; //获取String类型的k,v操作 ValueOperations<String, String> opsForValue = srt.opsForValue(); //开启事务 srt.multi(); opsForValue.set("name", "zhangsan"); opsForValue.set("salary", "40000.50"); opsForValue.get("name"); opsForValue.get("salary"); //结束事务并返回 return srt.exec(); ); //打印获取的值 if (dataA instanceof List) List<Object> data = (ArrayList<Object>) dataA; data.forEach(System.out::println);
.
以上是关于Redis之事务操作的主要内容,如果未能解决你的问题,请参考以下文章