一、官方文档

简单介绍下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. iOS: 动画更换 UIImageView 的 Image

    #import <QuartzCore/QuartzCore.h> ... imageView.image = [UIImage imageNamed:(i % ) ? @"3. ...

  2. gtest运行小析

    Gtest是google推出的C++测试框架,本篇文档,从整体上对Gtest的运行过程中的关键路径进行分析和梳理. 分析入口 新建一个最简单的测试工程,取名为source_analyse_proj,建 ...

  3. Linux系统下wetty安装和使用说明

    1. Wetty简介 Wetty是使用Node.js和websockets开发的一个开源Web-based SSH.关于Web-based SSH的更多资料请参考https://en.wikipedi ...

  4. level 6 - unit3 -- 非限制性定语从句

    非限制性定语从句 例子1 he has a son who is a fireman who 引导一个定语从句. who 是修饰前面的son. 翻译的意思:他有一个消防员的儿子 he has a so ...

  5. MySQL的sql_mode解析与设置,sql文件导入报错解决

    在往MySQL数据库中插入一组数据时,出错了!数据库无情了给我报了个错误:ERROR 1365(22012):Division by 0:意思是说:你不可以往数据库中插入一个 除数为0的运算的结果.于 ...

  6. Mongodb安全认证

    Mongodb安全认证在单实例和副本集两种情况下不太一样,单实例相对简单,只要在启动时加上 --auth参数即可,但副本集则需要keyfile. 一.单实例 1.启动服务(先不要加auth参数) 2. ...

  7. gcc和g++头文件和库路径的寻找和添加

    对所有用户有效修改/etc/profile文件 对个人有效则修改~/.bashrc文件 #在PATH中找到可执行文件程序的路径. export PATH =$PATH:$HOME/bin (可一次指定 ...

  8. numpy和Matplotlib篇---2

    原创博文,转载请标明出处--周学伟http://www.cnblogs.com/zxouxuewei/ 5.3 Python的科学计算包 - Numpy numpy(Numerical Python ...

  9. [Object Tracking] Identify and Track Specific Object

    Abstract—Augmented Reality (AR) has become increasingly popular in recent years and it has a widespr ...

  10. Tomcat------如何更改被IIS占用的80端口

    1.打开cmd,运行'netstat -ano'发现80端口被pid=4的进程占用 2.打开任务管理器,发现pid=4的进程,其实是system进程,其对应的进程描述是NT kernel & ...