spring-data-redis的事务操作深度解析--原来客户端库还可以攒够了事务命令再发?
一、官方文档
简单介绍下redis的几个事务命令:
redis事务四大指令: MULTI、EXEC、DISCARD、WATCH。
这四个指令构成了redis事务处理的基础。
1.MULTI用来组装一个事务;
2.EXEC用来执行一个事务;
3.DISCARD用来取消一个事务;
4.WATCH类似于乐观锁机制里的版本号。
被WATCH的key如果在事务执行过程中被并发修改,则事务失败。需要重试或取消。
以后单独介绍。
下面是最新版本的spring-data-redis(2.1.3)的官方手册。
https://docs.spring.io/spring-data/redis/docs/2.1.3.RELEASE/reference/html/#tx
这里,我们注意这么一句话:
Redis provides support for transactions through the
multi,exec, anddiscardcommands. These operations are available onRedisTemplate. However,RedisTemplateis not guaranteed to execute all operations in the transaction with the same connection.
意思是redis服务器通过multi,exec,discard提供事务支持。这些操作在RedisTemplate中已经实现。然而,RedisTemplate不保证在同一个连接中执行所有的这些一个事务中的操作。
另外一句话:
Spring Data Redis provides the
SessionCallbackinterface for use when multiple operations need to be performed with the sameconnection, such as when using Redis transactions. The following example uses themultimethod:
意思是:spring-data-redis也提供另外一种方式,这种方式可以保证多个操作(比如使用redis事务)可以在同一个连接中进行。示例如下:
//execute a transaction
List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForSet().add("key", "value1"); // This will contain the results of all operations in the transaction
return operations.exec();
}
});
System.out.println("Number of items added to set: " + txResults.get(0));
二、实现事务的方式--RedisTemplate直接操作
在前言中我们说,通过RedisTemplate直接调用multi,exec,discard,不能保证在同一个连接中进行。
这几个操作都会调用RedisTemplate#execute(RedisCallback<T>, boolean),比如multi:
public void multi() {
execute(connection -> {
connection.multi();
return null;
}, true);
}
我们看看RedisTemplate的execute方法的源码:
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();
RedisConnection conn = null;
try {
9 --开启了enableTransactionSupport选项,则会将获取到的连接绑定到当前线程
if (enableTransactionSupport) {
// only bind resources in case of potential transaction synchronization
conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
} else {
-- 未开启,就会去获取新的连接
conn = RedisConnectionUtils.getConnection(factory);
}
boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = preProcessConnection(conn, existingConnection);
。。。忽略无关代码。。。
RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
T result = action.doInRedis(connToExpose); -- 使用获取到的连接,执行定义在业务回调中的代码
。。。忽略无关代码。。。
// TODO: any other connection processing?
return postProcessResult(result, connToUse, existingConnection);
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
查看以上源码,我们发现,
- 不启用enableTransactionSupport,默认每次获取新连接,代码如下:
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.multi(); template.opsForValue().set("test_long", 1); template.opsForValue().increment("test_long", 1); template.exec();
- 启用enableTransactionSupport,每次获取与当前线程绑定的连接,代码如下:
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setEnableTransactionSupport(true);
template.multi();
template.opsForValue().set("test_long", 1);
template.opsForValue().increment("test_long", 1);
template.exec();
三、实现事务的方式--SessionCallback
采用这种方式,默认就会将所有操作放在同一个连接,因为在execute(SessionCallback<T> session)(注意,这里是重载函数,参数和上面不一样)源码中:
public <T> T execute(SessionCallback<T> session) {
Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
Assert.notNull(session, "Callback object must not be null");
RedisConnectionFactory factory = getRequiredConnectionFactory();
//在执行业务回调前,手动进行了绑定
RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
try { // 业务回调
return session.execute(this);
} finally {
RedisConnectionUtils.unbindConnection(factory);
}
}
四、SessionCallback方式的示例代码:
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("192.168.19.90");
JedisConnectionFactory factory = new JedisConnectionFactory(configuration);
factory.afterPropertiesSet();
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setDefaultSerializer(new GenericFastJsonRedisSerializer());
StringRedisSerializer serializer = new StringRedisSerializer();
template.setKeySerializer(serializer);
template.setHashKeySerializer(serializer);
template.afterPropertiesSet();
try {
List<Object> txResults = template.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().set("test_long", 1);
int i = 1/0;
operations.opsForValue().increment("test_long", 1);
// This will contain the results of all ops in the transaction
return operations.exec();
}
});
} catch (Exception e) {
System.out.println("error");
e.printStackTrace();
}
有几个值得注意的点:
1、为什么加try catch
先说结论:只是为了防止调用的主线程失败。
因为事务里运行到23行,(int i = 1/0)时,会抛出异常。
但是在 template.execute(SessionCallback<T> session)中未对其进行捕获,只在finally块进行了连接释放。
所以会导致调用线程(这里是main线程)中断。

2.try-catch了,事务到底得到保证了没
我们来测试下,测试需要,省略非关键代码
2.1 事务执行过程,抛出异常的情况:
List<Object> txResults = template.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().set("test_long", 1);
int i = 1/0;
operations.opsForValue().increment("test_long", 1);
// This will contain the results of all ops in the transaction
return operations.exec();
}
});
执行上述代码,执行到int i = 1/0时,会抛出异常。我们需要检查,抛出异常后,是否发送了“discard”命令给redis 服务器?
下面是我的执行结果,从最后的抓包可以看到,是发送了discard命令的:

2.2 事务执行过程,不抛出异常的情况:
这次我们注释了抛错的那行,可以看到“EXEC”命令已经发出去了:

3 抛出异常,不捕获异常的情况:
有些同学可能比较奇怪,为啥网上那么多教程,都是没有捕获异常的,我这里要捕获呢?
其实我也奇怪,但在我目前测试来看,不捕获的话,执行线程就中断了,因为template.execute是同步执行的。
来,看看:

从上图可以看到,主线程被未捕获的异常给中断了,但是,查看网络抓包,发现“DISCARD”命令还是发出去了的。
4.总结
从上面可以看出来,不管捕获异常没,事务都能得到保证。只是不捕获异常,会导致主线程中断。
不保证所有版本如此,在我这,spring-data-redis 2.1.3是这样的。
我跟了n趟代码,发现:
1、在执行sessionCallBack中的代码时,我们一般会先执行multi命令。
multi命令的代码如下:
public void multi() {
execute(connection -> {
connection.multi();
return null;
}, true);
}
即调用了当前线程绑定的connection的multi方法。
进入JedisConnection的multi方法,可以看到:
private @Nullable Transaction transaction;
public void multi() {
if (isQueueing()) {
return;
}
try {
if (isPipelined()) {
getRequiredPipeline().multi();
return;
}
//赋值给了connection的实例变量
this.transaction = jedis.multi();
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}
2、在有异常抛出时,直接进入finally块,会去关闭connection,当然,这里的关闭只是还回到连接池。
大概的逻辑如下:

3.在没有异常抛出时,执行exec,在exec中会先将状态变量修改,后边进入finally的时候,就不会发送discard命令了。

最后的结论就是:
所有这一切的前提是,共有同一个连接。(使用SessionCallBack的方式就能保证,总是共用同一个连接),否则multi用到的连接1里transcation是有值的,但是后面获取到的其他连接2,3,4,里面的transaction是空的,
还怎么保证事务呢?
五、思考
在不开启redisTemplate的enableTransactionSupport选项时,每执行一次redis操作,就会向服务器发送相应的命令。
但是,在开启了redisTemplate的enableTransactionSupport选项,或者使用SessionCallback方式时,会像下面这样发送命令:

后来,我在《redis实战》这本书里的4.4节,Redis事务这一节里,找到了答案:

归根到底呢,因为重用同一个连接,所以可以延迟发;如果每次都不一样的连接,只能马上发了。
这里另外说一句,不是所有客户端都这样,redis自带的redis-cli是不会延迟发送的。
六、源码
https://github.com/cctvckl/work_util/tree/master/spring-redis-template-2.1.3
spring-data-redis的事务操作深度解析--原来客户端库还可以攒够了事务命令再发?的更多相关文章
- spring boot通过Spring Data Redis集成redis
在spring boot中,默认集成的redis是Spring Data Redis,Spring Data Redis针对redis提供了非常方便的操作模版RedisTemplate idea中新建 ...
- spring data redis RedisTemplate操作redis相关用法
http://blog.mkfree.com/posts/515835d1975a30cc561dc35d spring-data-redis API:http://docs.spring.io/sp ...
- Spring Data Redis入门示例:字符串操作(六)
Spring Data Redis对字符串的操作,封装在了ValueOperations和BoundValueOperations中,在集成好了SPD之后,在需要的地方引入: // 注入模板操作实例 ...
- Spring Boot使用Spring Data Redis操作Redis(单机/集群)
说明:Spring Boot简化了Spring Data Redis的引入,只要引入spring-boot-starter-data-redis之后会自动下载相应的Spring Data Redis和 ...
- 使用Spring Data Redis操作Redis(集群版)
说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点.并且会与一些低版本的Sp ...
- 使用Spring Data Redis操作Redis(单机版)
说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点.并且会与一些低版本的Sp ...
- Spring Data Redis入门示例:Hash操作(七)
将对象存为Redis中的hash类型,可以有两种方式,将每个对象实例作为一个hash进行存储,则实例的每个属性作为hash的field:同种类型的对象实例存储为一个hash,每个实例分配一个field ...
- spring mvc Spring Data Redis RedisTemplate [转]
http://maven.springframework.org/release/org/springframework/data/spring-data-redis/(spring-data包下载) ...
- Spring Data Redis简介以及项目Demo,RedisTemplate和 Serializer详解
一.概念简介: Redis: Redis是一款开源的Key-Value数据库,运行在内存中,由ANSI C编写,详细的信息在Redis官网上面有,因为我自己通过google等各种渠道去学习Redis, ...
随机推荐
- MYSQL的索引类型:PRIMARY, INDEX,UNIQUE,FULLTEXT,SPAIAL 有什么区别?各适用于什么场合?
一.介绍一下索引的类型 Mysql常见索引有:主键索引.唯一索引.普通索引.全文索引.组合索引PRIMARY KEY(主键索引) ALTER TABLE `table_name` ADD PRIMAR ...
- iOS:第三方库使用非ARC编译
iOS: 解决某些第三方库因为ARC不能使用的问题 1.在target下面的build phases下有一个compile source,下面有很多待编译文件.可以看到一个compile flag,可 ...
- Java标准I/O流编程一览笔录
I/O是什么 I/O 是Input/Output(输入.输出)的简称,输入流可以理解为向内存输入,输出流是从内存输出. 流 流是一个连续的数据流,可以从流中读取数据,也可以往流中写数据.流与数据源,或 ...
- MacOS下MySQL配置
先去官网下载一个 MySQL for mac http://www.cnblogs.com/xiaobo-Linux/ 命令行运行终端,运行下面两条命令: 1 2 alias mysql=/usr/l ...
- UNIX环境编程学习笔记(24)——信号处理进阶学习之信号集和进程信号屏蔽字
lienhua342014-11-03 1 信号传递过程 信号源为目标进程产生了一个信号,然后由内核来决定是否要将该信号传递给目标进程.从信号产生到传递给目标进程的流程图如图 1 所示, 图 1: 信 ...
- selenium 如何处理table
qi_ling2005 http://jarvi.iteye.com/blog/1477837 andyguo http://blog.csdn.net/gzh0222/article/detai ...
- AdoConnect-获取连接字符串 (工具)
使用ADO访问数据库时需要设置正确的连接字符串,为此特提供一个获取连接字符串的小工具,方便编程使用. 使用方法: 1.点击“连接字符串”,弹出数据链接属性对话框 2.可以使用“提供程序”新建数据源,也 ...
- 通过phoenix在hbase上创建二级索引,Secondary Indexing
环境描述: 操作系统版本:CentOS release 6.5 (Final) 内核版本:2.6.32-431.el6.x86_64 phoenix版本:phoenix-4.10.0 hbase版本: ...
- go 类型转换
https://studygolang.com/articles/3400 https://studygolang.com/articles/6633
- PHP缓存机制详解
一,PHP缓存机制详解 我们可以使用PHP自带的缓存机制来完成页面静态化,但是仅靠PHP自身的缓存机制并不能完美的解决页面静态化,往往需要和其他静态化技术(通常是伪静态技术)结合使用. output ...