1.1 redis有事务的概念(官方文档中transction),而在使用redis事务的过程中,很大程度上对于开发人员带来了一定的困扰;大部分软件从业人员都是基于传统关系型数据库建立技术栈,对于关系型数据库的事务,ACID的特性有着根深蒂固的情结;提到redis的事务时也总是会对于mysql的事务进行对比,总结,并沉淀指导业务开发中的范式。
在api使用层面,其实他们还是有很多的差异,在开发过程中也会有些疑惑; 不恰当的使用场景,甚至于错误使用的场景频频出现,本文算是redis事务的释疑。
1.2 本文首先讨论redis的事务相关的概念,然后聚焦redis事务命令实操以及spring-dada-redis中模板类RedisTemplate的使用,最后讲述redis事务的用武之地(适用的一些业务场景)。
不同于关系型数据库的事务ACID,我们以关系数据库ACID事务特性做对比(很多开发工程师的常规做法),我们先看下redis的事务是什么?(大家都知道,redis是单线程执行命令,我们把redis单线程执行作为redis事务讨论的基础,以下用到时会随时提及,大家务必牢记。)
redis提供了很多的原子性CAS操作,比如incr incrby decr decrby等,相信这些操作大家都比较清楚了,不作为本文讨论的重点。
我们看下multi操作开始事务后,命令行执行过程中异常,比如对于字符串类型incr,redis设置的key值到底是多少呢?
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 abc
QUEUED
127.0.0.1:6379> incr key1
QUEUED
127.0.0.1:6379> set key2 abc
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get key1
"abc"
127.0.0.1:6379> get key2
"abc"
127.0.0.1:6379>
以上操作后的结果,大家有疑问么? 看下mysql是如何处理类似case的呢: https://www.cnblogs.com/jkko123/p/10184532.html
以上操作后,redis并没有跟mysql类似执行回滚的操作或者一些其他策略的选择,中间命令的执行仍然是生效的。
原因是redis单线程执行命令,每个命令item(这里没有使用命令行,避免一行多个命令的误解)都是单线程独立执行的,即使失败也是在命令item的范围,不会影响到其他的命令item;即使被multi包住的命令也是如此,所以有了如上这般的输出。
redis的原子性背后的意义是:该组合命令被redis的执行过程中没有其他命令的插入,可以一直执行完成。
以上的案例,可以作为redis不满足关系型数据库一致性约束一个佐证。
redis单线程执行,原生的隔离性,甚至于在multi包围的命令item之间也具备隔离性
redis可以只在内存中执行,数据不不持久化
也可以选择RDB或者AOF的持久化策略,AOF配置always持久化策略时,一定程度上可以保证持久性
严格意义上讲,拿redis的transctional和关系数据库饿transaction做比较是不公平的,redis本身就不是关系型数据库,redis的优势在于内存操作,单线程执行的效率。
很多文章提到redis的事务,很多同事朋友也经常问到我redis的事务;以上算是本文对于redis的事务进行的总结,希望大家能够区别对待关系数据库事务的ACID特性和redis事务的单线程特性
序号 | 命令 | 解释 | 命令链接 | 备注 |
---|---|---|---|---|
1 | MULTI | 标记一个事务块的开始,在后续exec的执行前,所有的命令都被queued。 | https://redis.io/commands/multi | |
2 | EXEC | 执行所有事务块内的命令。 | https://redis.io/commands/exec | |
3 | DISCARD | 取消事务,放弃执行事务块内的所有命令。 | https://redis.io/commands/discard | |
4 | WATCH | 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断 | https://redis.io/commands/watch | |
5 | UNWATCH | 取消 WATCH 命令对所有 key 的监视。 | https://redis.io/commands/unwatch | |
127.0.0.1:6379> incr conns
(integer) 1
127.0.0.1:6379> watch conns
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> INCRBY conns 100
QUEUED
127.0.0.1:6379> incrby conns 10
QUEUED
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> get conns
"1"
127.0.0.1:6379> incr conns
(integer) 2
127.0.0.1:6379> get conns
"2"
127.0.0.1:6379>
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get conns
"2"
127.0.0.1:6379>
以上可以看到对于watch后的key,如果另外客户端(进程或线程)对于该key有更改操作,整个事务会失败。
总结下redis客户端执行命令的过程:
redis客户端执行multi后返回ok,表明redis进入事务状态。
进入事务状态以后redis并不会立即执行命令,会将redis客户端发送的命令存入队列,暂不执行,此时返回queued。
最后调用exec,将命令从队列中取出来,然后一次性执行,这些,命令同时成功同时失败,最后将命令执行结果一次性返回,并且将事务状态标志复位。
说明: 在执行这些命令的过程中,使用同一客户端,并且不会被其它客户端中断。
注意:
1. watch一定要在multi前执行,watch放到multi后面会曝出错误
1. 我们执行的例子以数值型value做例子;watch对象是 string value时,客户端2执行set操作(会覆盖之前的key,甚至value类型也可以变化,ttl也取消掉),也会有同样的错误爆出
1. unwatch命令的语义为取消关注, exec和discard也会有同样的效果
RedisTemplate.multi
RedisTemplate.watch
RedisTemplate.execute(SessionCallback)
RedisTemplate.enableTransctionSupport
@Transctional
RedisService.java
@Slf4j
@Service
public class RedisService {
private static final String REDIS_KEY = "conns";
@Autowired
private StringRedisTemplate redisTemplate;
// TODO 这里简单操作,线上环境放到redisTemplate初始化逻辑中
@PostConstruct
public void init() {
redisTemplate.setEnableTransactionSupport(true);
}
@Transactional(rollbackFor = Exception.class)
public Object transactionalWithExcepOption(Boolean excep) throws Exception {
log.info("transactionalWithExcepOption invoked, excep: {}, redis-value:{}",excep,redisTemplate.opsForValue().get(REDIS_KEY));
ValueOperations<String, String> oper = redisTemplate.opsForValue();
oper.increment(REDIS_KEY);
if (excep) {
throw new RuntimeException();
}
oper.increment(REDIS_KEY, 100);
return oper.get(REDIS_KEY);
}
// redis事务的另一种写法,大家参考下即可
public Object rawTransction(Boolean excep){
log.info("rawTransction invoked, excep: {}, redis-value:{}",excep,redisTemplate.opsForValue().get(REDIS_KEY));
return redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
redisOperations.multi();
redisOperations.opsForValue().increment(REDIS_KEY);
if (excep) {
throw new RuntimeException();
}
redisOperations.opsForValue().increment(REDIS_KEY,100);
return redisOperations.exec();
}
});
}
}
RedisController.java
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisService redisService;
@GetMapping("/transactionalWithExcepOption")
public Object transactionalWithExcepOption(@RequestParam Boolean excep) throws Exception {
return redisService.transactionalWithExcepOption(excep);
}
@GetMapping("/rawTransaction")
public Object rawTransaction(@RequestParam Boolean excep) throws Exception {
return redisService.rawTransction(excep);
}
}
访问: http://localhost:8080/redis/transactionalWithExcepOption?excep=true,可以看到执行结果,redis的事务生效了。
2020-07-05 09:46:23.057 [http-nio-8080-exec-1] INFO com.example.demo.service.RedisService - transactionalWithExcepOption invoked, excep: true, redis-value:10
2020-07-05 09:46:23.137 [http-nio-8080-exec-1] ERROR o.a.c.c.C.[.[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException] with root cause
java.lang.RuntimeException: null
at com.example.demo.service.RedisService$1.execute(RedisService.java:55)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:256)
命令行查看conns的value,对比下以上日志,重点关注(“10”): transactionalWithExcepOption invoked, excep: true, redis-value:10
127.0.0.1:6379> get conns
"10"
127.0.0.1:6379>
RedisTemplate.enableTransactionSupport:是否启用事务支持。
若果我们在代码中搜索下用到这个变量的地方,会看到,在调用RedisCallback之前,有一行代码是如果启用事务支持,那么
conn =RedisConnectionUtils.bindConnection(factory, enableTransactionSupport),也就是说,系统自动帮我们拿到了事务中绑定的连接;
可以在一个线程的多次对Redis增删该查中,始终使用同一个连接;
但是,即使使用了同样的连接,没有进行connection.multi()和connection.exec(),依然是无法启用事务的。
@Transactional: redis事务标记,用于启用事务
在调用RedisTempalte中的execute()方法的地方,加入这个注解,能让这个方法中的所有execute,自动加入multi()以及异常的回滚或者是正常运行时候的提交!
我们知道**redis是单线程处理命令**,执行multi后接下来的redis命令会被queued,exec时会原子执行所有queued命令,中间多个redis命令不会被中断
pipline是同一个连接一次性提交多个请求,请求的执行过程,可能会有其他命令穿插进行。
我们知道,redis的核心功能是对于key-value的存储,一些逻辑运算并不是redis的强项;
lua脚本设计的初衷就是用来嵌入到其他的应用中来执行;可以说和redis相得益彰,redis单线程执行lua脚本本定义的逻辑,中间过程不会有其他命令穿插执行。
虽然关系型数据库的Transaction和redis的Transaction可以使用同样的Annotation注解;
但是,关系型数据库在api层面每次操作有结果返回;redis的事务性操作,只是返回queued,这也是一点差异
电商系统,多个商品同时下单,多个sku同时扣减库存
为了提高系统的性能,很多电商系统会在缓存中维护当前可售卖的库存值,原子性并发操作该库存值;
多个商品sku时同时扣减的场景,redis的事务操作可以尝试使用下。
流控系统,控制并发量的操作
控制并发量操作的处理流程伪代码如下:
// 1. 先给并发量+1
long currentValue = redisOper.incr(currentKey);
try {
// 2. 跟配置的并发上限做比较
if(currentValue<currentLimit){
// 3. 执行业务逻辑
doBiz();
}
} finally {
// 4. 业务执行完,并发量-1
redisOper.decr(currentKey);
}
spring cloud gateway的流控模块,也是基于redis的实现。