redis事务方法释疑以及RedisTemplate事务实战

redis事务释疑以及RedisTemplate实战

    • 1.背景
    • 2. 事务释疑
        • redis支持原子性吗?
        • redis的一致性
        • redis的隔离性
        • redis的持久性
        • 小结
    • 3. redis基本命令操作和spring RedisTemplate api
      • redis基本命令操作
        • redis原生事务相关命令如下
        • redis基本命令操作 命令行操作一把
      • spring中RedisTemplate事务api是如何使用呢
        • spring-data提对redis操作的封装使用了模板模式,我们看下RedisTemplate对于事务相关操作的api
        • 先看下redisTemplate的事务性操作编码
        • 解释下spring RedisTemplate相关的知识点
      • 4. 其他
        • multi与pipline的区别
        • lua脚本的执行
        • redis事务和关系型数据库事物的区别
        • enableTransctionSupport=false的场景下,使用Redistemplate执行以上事务命令,是不可以的,大家可以试一下;另外, watch代码的写法就留给读者自己完成吧。
    • redis事务适用的业务场景分析

1.背景

1.1 redis有事务的概念(官方文档中transction),而在使用redis事务的过程中,很大程度上对于开发人员带来了一定的困扰;大部分软件从业人员都是基于传统关系型数据库建立技术栈,对于关系型数据库的事务,ACID的特性有着根深蒂固的情结;提到redis的事务时也总是会对于mysql的事务进行对比,总结,并沉淀指导业务开发中的范式。
在api使用层面,其实他们还是有很多的差异,在开发过程中也会有些疑惑; 不恰当的使用场景,甚至于错误使用的场景频频出现,本文算是redis事务的释疑。

1.2 本文首先讨论redis的事务相关的概念,然后聚焦redis事务命令实操以及spring-dada-redis中模板类RedisTemplate的使用,最后讲述redis事务的用武之地(适用的一些业务场景)。

2. 事务释疑

不同于关系型数据库的事务ACID,我们以关系数据库ACID事务特性做对比(很多开发工程师的常规做法),我们先看下redis的事务是什么?(大家都知道,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不满足关系型数据库一致性约束一个佐证。

redis的隔离性

redis单线程执行,原生的隔离性,甚至于在multi包围的命令item之间也具备隔离性

redis的持久性

redis可以只在内存中执行,数据不不持久化
也可以选择RDB或者AOF的持久化策略,AOF配置always持久化策略时,一定程度上可以保证持久性

小结

严格意义上讲,拿redis的transctional和关系数据库饿transaction做比较是不公平的,redis本身就不是关系型数据库,redis的优势在于内存操作,单线程执行的效率。
很多文章提到redis的事务,很多同事朋友也经常问到我redis的事务;以上算是本文对于redis的事务进行的总结,希望大家能够区别对待关系数据库事务的ACID特性和redis事务的单线程特性

3. redis基本命令操作和spring RedisTemplate api

redis基本命令操作

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

redis基本命令操作 命令行操作一把

  1. 客户端1执行如下:
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> 
  1. 客户端2执行:
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>
  1. 接下来客户端1提交事务
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也会有同样的效果

spring中RedisTemplate事务api是如何使用呢

spring-data提对redis操作的封装使用了模板模式,我们看下RedisTemplate对于事务相关操作的api

		RedisTemplate.multi
		RedisTemplate.watch
		RedisTemplate.execute(SessionCallback)
		RedisTemplate.enableTransctionSupport
		@Transctional

先看下redisTemplate的事务性操作编码

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>

解释下spring RedisTemplate相关的知识点

	RedisTemplate.enableTransactionSupport:是否启用事务支持。
		若果我们在代码中搜索下用到这个变量的地方,会看到,在调用RedisCallback之前,有一行代码是如果启用事务支持,那么
		conn =RedisConnectionUtils.bindConnection(factory, enableTransactionSupport),也就是说,系统自动帮我们拿到了事务中绑定的连接;
		可以在一个线程的多次对Redis增删该查中,始终使用同一个连接;
		但是,即使使用了同样的连接,没有进行connection.multi()和connection.exec(),依然是无法启用事务的。

	@Transactional: redis事务标记,用于启用事务
		在调用RedisTempalte中的execute()方法的地方,加入这个注解,能让这个方法中的所有execute,自动加入multi()以及异常的回滚或者是正常运行时候的提交!

4. 其他

multi与pipline的区别

	我们知道**redis是单线程处理命令**,执行multi后接下来的redis命令会被queued,exec时会原子执行所有queued命令,中间多个redis命令不会被中断
	pipline是同一个连接一次性提交多个请求,请求的执行过程,可能会有其他命令穿插进行。

lua脚本的执行

	我们知道,redis的核心功能是对于key-value的存储,一些逻辑运算并不是redis的强项;
	lua脚本设计的初衷就是用来嵌入到其他的应用中来执行;可以说和redis相得益彰,redis单线程执行lua脚本本定义的逻辑,中间过程不会有其他命令穿插执行。

redis事务和关系型数据库事物的区别

	虽然关系型数据库的Transaction和redis的Transaction可以使用同样的Annotation注解;
	但是,关系型数据库在api层面每次操作有结果返回;redis的事务性操作,只是返回queued,这也是一点差异

enableTransctionSupport=false的场景下,使用Redistemplate执行以上事务命令,是不可以的,大家可以试一下;另外, watch代码的写法就留给读者自己完成吧。

redis事务适用的业务场景分析

  1. 电商系统,多个商品同时下单,多个sku同时扣减库存
    为了提高系统的性能,很多电商系统会在缓存中维护当前可售卖的库存值,原子性并发操作该库存值;
    多个商品sku时同时扣减的场景,redis的事务操作可以尝试使用下。

  2. 流控系统,控制并发量的操作

     控制并发量操作的处理流程伪代码如下:
    
// 1. 先给并发量+1
        long currentValue = redisOper.incr(currentKey);
        
        try {
// 2. 跟配置的并发上限做比较
            if(currentValue<currentLimit){
// 3. 执行业务逻辑
                doBiz();                
            }
        } finally {
// 4. 业务执行完,并发量-1
            redisOper.decr(currentKey);
        }
	spring cloud gateway的流控模块,也是基于redis的实现。

你可能感兴趣的:(redis,spring,java)