Spring Data Redis学习笔记

一、关于@Transactional支持

在阅读Spring Data Redis V2.5.1官方文档的时候,在10.12.1. @Transactional Support这一小节介绍了如何配置Spring Data Redis,使其能够支持@Transactional。让我感到奇怪的是以下这句话:
Spring Data Redis distinguishes between read-only and write commands in an ongoing transaction. Read-only commands, such as KEYS, are piped to a fresh (non-thread-bound) RedisConnection to allow reads. Write commands are queued by RedisTemplate and applied upon commit.
大概是说Spring Data Redis能够区分事务中的读写操作,写命令由专门的事务连接(每个事务都会新建一个连接并且绑定线程,并且带有MULTI、EXEC)向Redis服务发出请求,读命令由非事务连接(一个多线程共享的连接,不支持事务和blocking操作,一般操作都由它向Redis服务发送请求)向Redis服务发出请求。但是阅读源码的时候并没有发现用于区分读写操作的代码,最终通过DEBUG走单步的方式查找到了相关代码,原来它是通过代理的方式拦截了相关命令的执行,在命令发送前对连接进行了切换。

RedisConnectionUtils.class
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind, boolean transactionSupport) {
    ...
    //在@Transaction事务模式下会运行以下代码,为connection创建一个代理
    if (bindSynchronization && isActualNonReadonlyTransactionActive()) {
        connection = createConnectionSplittingProxy(connection, factory);
    }
    ...
}

private static RedisConnection createConnectionSplittingProxy(RedisConnection connection, RedisConnectionFactory factory) {

    ProxyFactory proxyFactory = new ProxyFactory(connection);
    proxyFactory.addAdvice(new ConnectionSplittingInterceptor(factory));
    proxyFactory.addInterface(RedisConnectionProxy.class);

    return RedisConnection.class.cast(proxyFactory.getProxy());
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ConnectionSplittingInterceptor.class
public Object intercept(Object obj, Method method, Object[] args) throws Throwable {

    if (method.getName().equals("getTargetConnection")) {
       // Handle getTargetConnection method: return underlying RedisConnection.
       return obj;
    }

    RedisCommand commandToExecute = RedisCommand.failsafeCommandLookup(method.getName());
    //这段代码会判断是否是写命令
    if (isPotentiallyThreadBoundCommand(commandToExecute)) {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Invoke '%s' on bound connection", method.getName()));
       }
        return invoke(method, obj, args);
    }

    if (log.isDebugEnabled()) {
        log.debug(String.format("Invoke '%s' on unbound connection", method.getName()));
    }

    //如果是读命令会获取一个新RedisConnection,这个RedisConnection 是一个相对高级的对象它包含了真正的Redis命令连接,执行读命令的时候它会根据上下文关系返回一个共享连接还是专用连接
    RedisConnection connection = factory.getConnection();

    try {
        return invoke(method, connection, args);
    } finally {
        // properly close the unbound connection after executing command
       if (!connection.isClosed()) {
           doCloseConnection(connection);
        }
    }
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//我使用的是Lettuce客户端库,以下只是一个读命令的例子
LettuceKeyCommands.class
@Override
public Set<byte[]> keys(byte[] pattern) {

    Assert.notNull(pattern, "Pattern must not be null!");

    try {
        if (isPipelined()) {
            pipeline(connection.newLettuceResult(getAsyncConnection().keys(pattern), LettuceConverters.bytesListToBytesSet()));
            return null;
        }
        if (isQueueing()) {
            transaction(connection.newLettuceResult(getAsyncConnection().keys(pattern), LettuceConverters.bytesListToBytesSet()));
            return null;
        }
        //最终会运行到这里,获取共享连接
        return LettuceConverters.toBytesSet(getConnection().keys(pattern));
    } catch (Exception ex) {
        throw convertLettuceAccessException(ex);
    }
}

第二个疑惑是关于事务中读操作的,因为根据相关知识了解到,在Redis事务中的命令会先缓存在客户端,等到EXEC命令调用后才会向Redis服务发出请求。但是根据上面的分析读命令并不会缓存而是直接向Redis服务发出请求(使用了一个共享连接),DEBUG后发现的确是如此。奇怪的是如果你用System.out.println(template.opsForValue().get("asd"));这样的代码它的结果是NULL或者空对象,但是DEBUG的时候它明明已经获取到了数据。

//一个包含RedisConnection、DeserializingConverter、SerializingConverter等类的上层类
DefaultStringRedisConnection.class
@Override
public byte[] get(byte[] key) {
    //这里的delegate就是前面的RedisConnection的代理对象
    return convertAndReturn(delegate.get(key), identityConverter);
}

// DEBUG到这里value的返回值依然是正确的
@SuppressWarnings("unchecked")
@Nullable
private <T> T convertAndReturn(@Nullable Object value, Converter converter) {

    //这个就是关键代码,这个条件会判断为true,最终返回null
    if (isFutureConversion()) {

        addResultConverter(converter);
        return null;
    }

    if (!(converter instanceof ListConverter) && value instanceof List) {
        return (T) new ListConverter<>(converter).convert((List) value);
    }

    return value == null ? null
            : ObjectUtils.nullSafeEquals(converter, identityConverter) ? (T) value : (T) converter.convert(value);
}

//判断是否在事务中或者在流水模式中
//虽然读命令使用了非事务的连接,但是这个用于判断的依然是事务连接,也就是整个上下文依然属于事务连接中
private boolean isFutureConversion() {
    return isPipelined() || isQueueing();
}

从上面的代码可以看出,最终读命令的返回值被重置为了null,在正常事务模式下这是正常的,因为事务模式下任何操作只是缓存在客户端,这时候还没有值。既然这里的读写连接模式是分开的,而且读操作确实已经从Redis服务那读取了数据,那么为什么Spring Data Redis没有对这些读操作返回值作兼容性处理而是沿用正常的事务操作处理,所以不太明白读写分离模式具体有什么作用。
当然官方文档是不建议我们在事务中进行读取操作的,以下是一些官方说明:

// must be performed on thread-bound connection
template.opsForValue().set("thing1", "thing2");

// read operation must be run on a free (not transaction-aware) connection
template.keys("*");

// returns null as values set within a transaction are not visible
template.opsForValue().get("thing1");

个人觉得在实际开发中没必要使用@Transactional来管理Redis事务,主要是Redis的事务跟关系数据库的事务是有区别的,而且读写分离模式貌似没有作用,最后使用template.execute(new SessionCallback<Object>() {})更符合Redis事务语义(读写不区分)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容