一、官方文档

简单介绍下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 multiexec, and discard commands. These operations are available on RedisTemplate. 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 same connection, such as when using Redis transactions. The following example uses the multi 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的事务操作深度解析--原来客户端库还可以攒够了事务命令再发?的更多相关文章

  1. spring boot通过Spring Data Redis集成redis

    在spring boot中,默认集成的redis是Spring Data Redis,Spring Data Redis针对redis提供了非常方便的操作模版RedisTemplate idea中新建 ...

  2. spring data redis RedisTemplate操作redis相关用法

    http://blog.mkfree.com/posts/515835d1975a30cc561dc35d spring-data-redis API:http://docs.spring.io/sp ...

  3. Spring Data Redis入门示例:字符串操作(六)

    Spring Data Redis对字符串的操作,封装在了ValueOperations和BoundValueOperations中,在集成好了SPD之后,在需要的地方引入: // 注入模板操作实例 ...

  4. Spring Boot使用Spring Data Redis操作Redis(单机/集群)

    说明:Spring Boot简化了Spring Data Redis的引入,只要引入spring-boot-starter-data-redis之后会自动下载相应的Spring Data Redis和 ...

  5. 使用Spring Data Redis操作Redis(集群版)

    说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点.并且会与一些低版本的Sp ...

  6. 使用Spring Data Redis操作Redis(单机版)

    说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点.并且会与一些低版本的Sp ...

  7. Spring Data Redis入门示例:Hash操作(七)

    将对象存为Redis中的hash类型,可以有两种方式,将每个对象实例作为一个hash进行存储,则实例的每个属性作为hash的field:同种类型的对象实例存储为一个hash,每个实例分配一个field ...

  8. spring mvc Spring Data Redis RedisTemplate [转]

    http://maven.springframework.org/release/org/springframework/data/spring-data-redis/(spring-data包下载) ...

  9. Spring Data Redis简介以及项目Demo,RedisTemplate和 Serializer详解

    一.概念简介: Redis: Redis是一款开源的Key-Value数据库,运行在内存中,由ANSI C编写,详细的信息在Redis官网上面有,因为我自己通过google等各种渠道去学习Redis, ...

随机推荐

  1. git fetch , git pull

    要讲清楚git fetch,git pull,必须要附加讲清楚git remote,git merge .远程repo, branch . commit-id 以及 FETCH_HEAD. 1. [g ...

  2. 阿里云centos7安装桌面环境

    centos7. 1.安装X11.yum groupinstall "X Window System". 2.安装gnome. 全安装:yum groupinstall -y &q ...

  3. SVN自动生成版本号信息

        在平时的多版本开发过程中,需要通过版本号来定位到源码版本,便于定位问题.常规工程实践是设置版本号为X.Y.Z.N,一般X表示主版本号,Y表示子版本号,我一般将Z设为0,N为本次提交的SVN版本 ...

  4. Thinkphp5笔记六:公共模块common的使用

    common模块属于公共模块,Thinkphp框架,默认就能调用. 实际用处:任何模块都可能用到的模型.控制.事件提取出来放到公共模块下. 一.公共事件  apps\common\common.php ...

  5. Java多线程学习篇——线程的开启

    随着开发项目中业务功能的增加,必然某些功能会涉及到线程以及并发编程的知识点.笔者就在现在的公司接触到了很多软硬件结合和socket通讯的项目了,很多的功能运用到了串口通讯编程,串口通讯编程的安卓端就是 ...

  6. QT编译错误:invalid application of 'sizeof' to incomplete type 'Qt3DRender::QPickEvent'

    执行3D常将中实体的pick操作,结果出现了编译错误:invalid application of 'sizeof' to incomplete type 'Qt3DRender::QPickEven ...

  7. Python打包-Pyinstaller

    我们知道,Python很优雅,很值得学习.但是Python是解释性语言,代码需要有Python解释器才能执行,相比较我们平时直接运行exe等可执行文件多了一步的麻烦. 于是,希望能将Python程序打 ...

  8. 史上最强大的python selenium webdriver的包装

    1.之前已经发过两次使用单浏览器了,但是这个最完美,此篇并没有使用任何单例模式的设计模式,用了实例属性结果缓存到类属性. 2.最简单的控制单浏览器是只实例化一次类,然后一直使用这个对象,但每个地方运行 ...

  9. [Scikit-learn] 2.5 Dimensionality reduction - ICA

    理论学习: 独立成分分析ICA历史 Ref: Lecture 15 | Machine Learning (Stanford) - NG From: https://wenku.baidu.com/v ...

  10. 标准JSON格式定义与解析注意点

    标准JSON格式定义与解析注意点 在JS.IOS.Android中都内置了JSON的序列化.反序列化SDK.JEE中也可以使用第三方的JSON解析库,如GSON.虽然在JSON格式被定义出来的时候并没 ...