RedisTemplate先在事務(wù)中使用,然后在非事務(wù)中使用唁盏,導(dǎo)致讀操作不能立即讀出數(shù)據(jù)

在Spring Data Redis提供了RedisTemplate對redis進(jìn)行讀寫操作并且支持事務(wù)蚕苇。

如果在同一線程(比如Web環(huán)境的一次請求中)中存在下面操作將會造成讀操作無法直接讀取出數(shù)據(jù)

  1.先在非事務(wù)環(huán)境下執(zhí)行reids操作(調(diào)用沒有加@Transactional注解)

  2.然后在事務(wù)環(huán)境下執(zhí)行redis操作(調(diào)用添加了@Transactional注解的方法)

可以從RedisTemplate源碼中找到原因

RedisTemplate中對Redis的各種數(shù)據(jù)類型的操作都抽象出了相對于的操作類 如 ValueOperations哩掺,ListOperations,SetOperations等涩笤,而這些類在執(zhí)行操作時最終還是會調(diào)用RedisTemplate的public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline)嚼吞,這個方法是RedisTemplate的操作Reids的核心方法

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 = getConnectionFactory();
        RedisConnection conn = null;
        try {
            if (enableTransactionSupport) {
                // only bind resources in case of potential transaction synchronization
                                //如果設(shè)置了啟用事務(wù),則調(diào)用bindConnection
                conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
            } else {
                conn = RedisConnectionUtils.getConnection(factory);
            }
            boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
                        //預(yù)留鉤子函數(shù)可在執(zhí)行具體操作前對connection做一些處理
            RedisConnection connToUse = preProcessConnection(conn, existingConnection);
            boolean pipelineStatus = connToUse.isPipelined();
            if (pipeline && !pipelineStatus) {
                connToUse.openPipeline();
            }
            RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
            T result = action.doInRedis(connToExpose);
            // close pipeline
            if (pipeline && !pipelineStatus) {
                connToUse.closePipeline();
            }
            // TODO: any other connection processing?
                        //預(yù)留鉤子函數(shù)可在執(zhí)行具體操作后對connection做一些處理
            return postProcessResult(result, connToUse, existingConnection);
        } finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
    }

可以看出這個方法是個模板方法蹬碧,實(shí)現(xiàn)了整個操作的流程

RedisConnectionUtils是獲取連接的工具類舱禽,在配置RedisTemplate是如果設(shè)置了enableTransactionSupport=true時,則會通過bindConnection方法獲取連接

//bindConnection調(diào)用了doGetConnection
public static RedisConnection bindConnection(RedisConnectionFactory factory, boolean enableTranactionSupport) {
        return doGetConnection(factory, true, true, enableTranactionSupport);
    }
public static RedisConnection doGetConnection(RedisConnectionFactory factory, boolean allowCreate, boolean bind,
            boolean enableTransactionSupport) {
        Assert.notNull(factory, "No RedisConnectionFactory specified");
        //從當(dāng)前線程中獲取連接
        RedisConnectionHolder connHolder = (RedisConnectionHolder) TransactionSynchronizationManager.getResource(factory);
        if (connHolder != null) {
            if (enableTransactionSupport) {
                //開啟reids事務(wù)
                potentiallyRegisterTransactionSynchronisation(connHolder, factory);
            }
            return connHolder.getConnection();
        }
        if (!allowCreate) {
            throw new IllegalArgumentException("No connection found and allowCreate = false");
        }
        if (log.isDebugEnabled()) {
            log.debug("Opening RedisConnection");
        }
        //如果當(dāng)前線程中不存在連接則創(chuàng)建連接
        RedisConnection conn = factory.getConnection();
        if (bind) {
            RedisConnection connectionToBind = conn;
            //如果開啟的事務(wù)且調(diào)用添加了@Transactional的方法恩沽,這里會創(chuàng)建一個連接的代理對象
            if (enableTransactionSupport && isActualNonReadonlyTransactionActive()) {
                connectionToBind = createConnectionProxy(conn, factory);
            }
            connHolder = new RedisConnectionHolder(connectionToBind);
            //綁定連接到當(dāng)前線程中
            TransactionSynchronizationManager.bindResource(factory, connHolder);
            if (enableTransactionSupport) {
                //開啟reids事務(wù)
                potentiallyRegisterTransactionSynchronisation(connHolder, factory);
            }
            return connHolder.getConnection();
        }
        return conn;
    }

//開啟reids事務(wù)
private static void potentiallyRegisterTransactionSynchronisation(RedisConnectionHolder connHolder,
            final RedisConnectionFactory factory) {

        if (isActualNonReadonlyTransactionActive()) {

            if (!connHolder.isTransactionSyncronisationActive()) {
                connHolder.setTransactionSyncronisationActive(true);

                RedisConnection conn = connHolder.getConnection();
                conn.multi();
                //注冊一個事務(wù)完成時的回調(diào)誊稚,用于提交或回滾redis事務(wù)
                TransactionSynchronizationManager.registerSynchronization(new RedisTransactionSynchronizer(connHolder, conn,
                        factory));
            }
        }
    }

上面代碼可以看出獲取連接的整個流程

  1. TransactionSynchronizationManager.getResource(factory)(從當(dāng)前線程中獲取連接,TransactionSynchronizationManager使用ThreadLocal把連接綁定到當(dāng)前線程上罗心。
  2. 如果獲取到連接則開啟事務(wù)里伯,返回連接,如果沒有獲取到則創(chuàng)建連接
  3. 創(chuàng)建完連接后會判斷當(dāng)前操作是否在事務(wù)中isActualNonReadonlyTransactionActive (是否添加了@Transactional注解渤闷,并且事務(wù)不是ReadOnly的)
  4. 如果操作實(shí)在事務(wù)中疾瓮,則會創(chuàng)建一個連接的代理對象
  5. TransactionSynchronizationManager.bindResource(factory, connHolder); 綁定事務(wù)到當(dāng)前線程中
  6. potentiallyRegisterTransactionSynchronisation(connHolder, factory); 開啟redis事務(wù)
  7. 返回連接

從上面流程可以看出在事務(wù)中執(zhí)行和不在事務(wù)中執(zhí)行的關(guān)鍵區(qū)別在于,是否創(chuàng)建了一個連接的代理對象肤晓,下面看一下createConnectionProxy的代碼

//創(chuàng)建了一個ConnectionSplittingInterceptor類用于攔截RedisConnection所有方法
private static RedisConnection createConnectionProxy(RedisConnection connection, RedisConnectionFactory factory) {
        ProxyFactory proxyFactory = new ProxyFactory(connection);
        proxyFactory.addAdvice(new ConnectionSplittingInterceptor(factory));
        return RedisConnection.class.cast(proxyFactory.getProxy());
    }
image.gif
image.gif

上面代碼中創(chuàng)建了一個ConnectionSplittingInterceptor類用于攔截RedisConnection中的所有方法爷贫,ConnectionSplittingInterceptor中的核心代碼是intecepter方法

@Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            RedisCommand commandToExecute = RedisCommand.failsafeCommandLookup(method.getName());
                        //判斷命令是否為只讀命令,如果是則新開一個連接執(zhí)行度操作补憾,如果是寫命令則放在事務(wù)中執(zhí)行
            if (isPotentiallyThreadBoundCommand(commandToExecute)) {
                if (log.isDebugEnabled()) {
                    log.debug(String.format("Invoke '%s' on bound conneciton", method.getName()));
                }
                return invoke(method, obj, args);
            }
            if (log.isDebugEnabled()) {
                log.debug(String.format("Invoke '%s' on unbound conneciton", method.getName()));
            }
            RedisConnection connection = factory.getConnection();
            try {
                return invoke(method, connection, args);
            } finally {
                // properly close the unbound connection after executing command
                if (!connection.isClosed()) {
                    connection.close();
                }
            }
        }
image.gif
image.gif

intecepter方法中會判斷這次執(zhí)行的命令是否是讀命令漫萄。如果不是,會用當(dāng)前線程中的連接執(zhí)行也就是放在事務(wù)中執(zhí)行盈匾,如果是讀操作腾务,會創(chuàng)建一個新的連接執(zhí)行,這樣就能立即獲得讀取的數(shù)據(jù)削饵。

通過代碼可以看出出錯的大致流程:

  1. 調(diào)用沒有使用事務(wù)的reids操作
  2. 創(chuàng)建一個連接并綁定到當(dāng)前線程中(由于沒有使用事務(wù)岩瘦,不會創(chuàng)建連接的代理對象)
  3. 執(zhí)行reids操作 (操作完成后并沒有把當(dāng)前線程中的連接清除)
  4. 調(diào)用使用事務(wù)的redis操作(方法上添加了@Transactional注解)
  5. 獲取連接方向當(dāng)前線程中已經(jīng)存在了連接不再重新創(chuàng)建(獲取到的是沒有使用事務(wù)時創(chuàng)建的連接,此連接對象不是代理對象)
  6. 開啟事務(wù)
  7. 執(zhí)行操作(如果執(zhí)行的是讀操作窿撬,由于連接對象不是代理對象启昧,讀操作并不會重新創(chuàng)建一個連接,而是使用當(dāng)前連接劈伴,并且放在事務(wù)中運(yùn)行密末,因此讀操作并不會立即執(zhí)行而是等到事務(wù)提交時才能執(zhí)行,導(dǎo)致讀操作讀取的結(jié)果為null)

解決方案:

此問題關(guān)鍵在于如果執(zhí)行了為使用事務(wù)的reids操作跛璧,在操作完成后要將當(dāng)前線程中綁定的連接對象給清除掉严里,或者在使用的事務(wù)的reids操作之前,判斷獲取到的連接是否是代理對象追城,如果不是則清除掉刹碾,重新獲取連接。在RedisTemplate的execute方法中我們看到了 reids為我們預(yù)留了兩個鉤子函數(shù)座柱,

preProcessConnection(conn, existingConnection) 和 postProcessResult(result, connToUse, existingConnection) 因此我們可以繼承RedisTemplate來對連接進(jìn)行處理

public class CustomRedisTemplate<K, V> extends RedisTemplate<K, V> {
    private boolean enableTransactionSupport = false;
    private static boolean isActualNonReadonlyTransactionActive() {
        return TransactionSynchronizationManager.isActualTransactionActive()
                && !TransactionSynchronizationManager.isCurrentTransactionReadOnly();
    }
    /**
     * 解決 redis先非事務(wù)中運(yùn)行迷帜,然后又在事務(wù)中運(yùn)行,出現(xiàn)取到的連接還是非事務(wù)連接的問題
     * 在事務(wù)環(huán)境中用非事務(wù)連接色洞,讀取操作無法馬上讀出數(shù)據(jù)
     *
     * @param connection
     * @param existingConnection
     * @return
     */
    @Override
    protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
        if (existingConnection && !Proxy.isProxyClass(connection.getClass()) && isActualNonReadonlyTransactionActive()) {
            RedisConnectionUtils.unbindConnection(getConnectionFactory());
            List<TransactionSynchronization> list = new ArrayList<>(TransactionSynchronizationManager.getSynchronizations());
            TransactionSynchronizationManager.clearSynchronization();
            TransactionSynchronizationManager.initSynchronization();
            //移除最后一個回調(diào)(由于之前回去連接是會注冊一個事務(wù)回調(diào)瞬矩,下面如果再獲取連接會導(dǎo)致注冊兩個事務(wù)回調(diào)。事務(wù)完成后會執(zhí)行兩次回調(diào)锋玲,
            // 回調(diào)中會清除資源景用,第一次已經(jīng)清除,第二次再清的時候回拋出異常)
            list.remove(list.size() - 1);
            list.forEach(TransactionSynchronizationManager::registerSynchronization);
            connection = RedisConnectionUtils.bindConnection(getConnectionFactory(), enableTransactionSupport);
        }
        return connection;
    }
    @Override
    public void setEnableTransactionSupport(boolean enableTransactionSupport) {
        super.setEnableTransactionSupport(enableTransactionSupport);
        this.enableTransactionSupport = enableTransactionSupport;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惭蹂,一起剝皮案震驚了整個濱河市伞插,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盾碗,老刑警劉巖媚污,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異廷雅,居然都是意外死亡耗美,警方通過查閱死者的電腦和手機(jī)京髓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來商架,“玉大人堰怨,你說我怎么就攤上這事∩呙” “怎么了备图?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赶袄。 經(jīng)常有香客問我揽涮,道長,這世上最難降的妖魔是什么饿肺? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任蒋困,我火速辦了婚禮,結(jié)果婚禮上敬辣,老公的妹妹穿的比我還像新娘家破。我一直安慰自己,他們只是感情好购岗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布汰聋。 她就那樣靜靜地躺著,像睡著了一般喊积。 火紅的嫁衣襯著肌膚如雪烹困。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天乾吻,我揣著相機(jī)與錄音髓梅,去河邊找鬼。 笑死绎签,一個胖子當(dāng)著我的面吹牛枯饿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诡必,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼奢方,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了爸舒?” 一聲冷哼從身側(cè)響起蟋字,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扭勉,沒想到半個月后鹊奖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涂炎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年忠聚,在試婚紗的時候發(fā)現(xiàn)自己被綠了设哗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡两蟀,死狀恐怖网梢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垫竞,我是刑警寧澤澎粟,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布蛀序,位于F島的核電站欢瞪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏徐裸。R本人自食惡果不足惜遣鼓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望重贺。 院中可真熱鬧骑祟,春花似錦、人聲如沸气笙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽潜圃。三九已至缸棵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谭期,已是汗流浹背堵第。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隧出,地道東北人踏志。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像胀瞪,于是被迫代替她去往敵國和親针余。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容