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
, anddiscard
commands. These operations are available onRedisTemplate
. However,RedisTemplate
is not guaranteed to execute all operations in the transaction with the same connection.
意思是redis服务器通过multi,exec,discard提供事务支持。这些操作在RedisTemplate中已经实现。然而,RedisTemplate不保证在同一个连接中执行所有的这些一个事务中的操作。
另外一句话:
Spring Data Redis provides the
SessionCallback
interface for use when multiple operations need to be performed with the sameconnection
, such as when using Redis transactions. The following example uses themulti
method:
意思是: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, ...
随机推荐
- git fetch , git pull
要讲清楚git fetch,git pull,必须要附加讲清楚git remote,git merge .远程repo, branch . commit-id 以及 FETCH_HEAD. 1. [g ...
- 阿里云centos7安装桌面环境
centos7. 1.安装X11.yum groupinstall "X Window System". 2.安装gnome. 全安装:yum groupinstall -y &q ...
- SVN自动生成版本号信息
在平时的多版本开发过程中,需要通过版本号来定位到源码版本,便于定位问题.常规工程实践是设置版本号为X.Y.Z.N,一般X表示主版本号,Y表示子版本号,我一般将Z设为0,N为本次提交的SVN版本 ...
- Thinkphp5笔记六:公共模块common的使用
common模块属于公共模块,Thinkphp框架,默认就能调用. 实际用处:任何模块都可能用到的模型.控制.事件提取出来放到公共模块下. 一.公共事件 apps\common\common.php ...
- Java多线程学习篇——线程的开启
随着开发项目中业务功能的增加,必然某些功能会涉及到线程以及并发编程的知识点.笔者就在现在的公司接触到了很多软硬件结合和socket通讯的项目了,很多的功能运用到了串口通讯编程,串口通讯编程的安卓端就是 ...
- QT编译错误:invalid application of 'sizeof' to incomplete type 'Qt3DRender::QPickEvent'
执行3D常将中实体的pick操作,结果出现了编译错误:invalid application of 'sizeof' to incomplete type 'Qt3DRender::QPickEven ...
- Python打包-Pyinstaller
我们知道,Python很优雅,很值得学习.但是Python是解释性语言,代码需要有Python解释器才能执行,相比较我们平时直接运行exe等可执行文件多了一步的麻烦. 于是,希望能将Python程序打 ...
- 史上最强大的python selenium webdriver的包装
1.之前已经发过两次使用单浏览器了,但是这个最完美,此篇并没有使用任何单例模式的设计模式,用了实例属性结果缓存到类属性. 2.最简单的控制单浏览器是只实例化一次类,然后一直使用这个对象,但每个地方运行 ...
- [Scikit-learn] 2.5 Dimensionality reduction - ICA
理论学习: 独立成分分析ICA历史 Ref: Lecture 15 | Machine Learning (Stanford) - NG From: https://wenku.baidu.com/v ...
- 标准JSON格式定义与解析注意点
标准JSON格式定义与解析注意点 在JS.IOS.Android中都内置了JSON的序列化.反序列化SDK.JEE中也可以使用第三方的JSON解析库,如GSON.虽然在JSON格式被定义出来的时候并没 ...