seata redis模式重构之全局事务更新

关于重构全局事务信息存储重构过程中一个问题的思考。

1.watch的必要性

jedis.hmset命令的语义:
如果这个map存在,就更新这个多个值;
如果这个map不存在,则新建map,然后设置键值对;

同时将多个 field-value (域-值)对设置到哈希表 key 中。 此命令会覆盖哈希表中已存在的域。
如果 key不存在,一个空哈希表被创建并执行 HMSET 操作。
如果命令执行成功,返回 OK 。
当 key 不是哈希表(hash)类型时,返回一个错误。

在更新全局事务session的map时,如果多tc情况下,事务前不watch这个全局事务的key,那么,当其他tc和当前tc都来更新这个全局事务时,或者由于某种情况,一个tc把这个global session删除了,那么,这个hmset是会出问题的,他会新建一个map,就这状态和时间两个值,这个map会成为游离状态,也不会被删除了。

所以这个地方,要做好防范,watch全局key,确保这个hmset过程中,map一定存在,让他update,而不会add。

private boolean updateGlobalTransactionDO(GlobalTransactionDO globalTransactionDO) {
        String xid = globalTransactionDO.getXid();
        String globalKey = buildGlobalKeyByTransactionId(globalTransactionDO.getTransactionId());
        try (Jedis jedis = JedisPooledFactory.getJedisInstance()) {
            String previousStatus = jedis.hget(globalKey, REDIS_KEY_GLOBAL_STATUS);
            if (StringUtils.isEmpty(previousStatus)) {
                throw new StoreException("Global transaction is not exist, update global transaction failed.");
            }
            //Defensive watch to prevent other TC server operating concurrently,Fail fast
            //jedis.watch(globalKey);
            String previousGmtModified = jedis.hget(globalKey, REDIS_KEY_GLOBAL_GMT_MODIFIED);
            Transaction multi = jedis.multi();
            Map<String,String> map = new HashMap<>(2);
            map.put(REDIS_KEY_GLOBAL_STATUS,String.valueOf(globalTransactionDO.getStatus()));
            map.put(REDIS_KEY_GLOBAL_GMT_MODIFIED,String.valueOf((new Date()).getTime()));
            multi.hmset(globalKey,map);
            
            multi.lrem(buildGlobalStatus(Integer.valueOf(previousStatus)),0, xid);
            multi.rpush(buildGlobalStatus(globalTransactionDO.getStatus()), xid);
            List<Object> exec = multi.exec();
            String hmset = exec.get(0).toString();
            long lrem  = (long)exec.get(1);
            long rpush = (long)exec.get(2);
            if (OK.equalsIgnoreCase(hmset) && lrem > 0 && rpush > 0) {
                return true;
            } else {
                // If someone failed, the succeed operations need rollback
                if (OK.equalsIgnoreCase(hmset)) {
                    Map<String,String> mapPrevious = new HashMap<>(2);
                    mapPrevious.put(REDIS_KEY_GLOBAL_STATUS,previousStatus);
                    mapPrevious.put(REDIS_KEY_GLOBAL_GMT_MODIFIED,previousGmtModified);
                    jedis.hmset(globalKey,mapPrevious);
                }
                if (lrem > 0) {
                    jedis.rpush(buildGlobalStatus(Integer.valueOf(previousStatus)),xid);
                }
                if (rpush > 0) {
                    jedis.lrem(buildGlobalStatus(globalTransactionDO.getStatus()),0,xid);
                }
                return false;
            }
        } catch (Exception ex) {
            throw new StoreException(ex);
        }
    }

2.hmset hset hsetnx

在重构的过程中,发现jedismock无法mock watch命令,那测试时,只能拿掉watch,就在想,如果没有watch命令,那这个地方如何来确保事务也是正确的。
我们对比下三个set命令的区别:

hmset

同时将多个 field-value (域-值)对设置到哈希表 key 中。 此命令会覆盖哈希表中已存在的域。
如果 key不存在,一个空哈希表被创建并执行 HMSET 操作。
如果命令执行成功,返回 OK 。
当 key 不是哈希表(hash)类型时,返回一个错误。

这个命令,看起来不行,如果global session被删除了,这个命令会新建一个map.

hsetnx

当且仅当域 field 尚未存在于哈希表的情况下, 将它的值设置为 value 。 如果给定域已经存在于哈希表当中, 那么命令将放弃执行设置操作。
如果哈希表 hash 不存在, 那么一个新的哈希表将被创建并执行 HSETNX 命令。
HSETNX命令在设置成功时返回 1 , 在给定域已经存在而放弃执行设置操作时返回 0 。

这个命令,看起来不行,如果global session被删除了,这个命令会新建一个map.

hset

将哈希表 hash 中域 field 的值设置为 value 。

如果给定的哈希表并不存在, 那么一个新的哈希表将被创建并执行 HSET 操作。

如果域 field 已经存在于哈希表中, 那么它的旧值将被新值 value 覆盖。

当 HSET 命令在哈希表中新创建 field 域并成功为它设置值时, 命令返回 1 ;
如果域 field 已经存在于哈希表, 并且HSET 命令成功使用新值覆盖了它的旧值, 那么命令返回 0

这个命令,我们看下实际情况,当map123不存在时,和map123存在但是key-value的key不存在时,返回值都是1,我们无法取分这个命令的操作成功究竟是不是新建了一个map.所以看起来也不行。

阿里云redis:0>hset map123 key va
"1"

阿里云redis:0>hset map123 key1 va1
"1"

阿里云redis:0>hset map123 key1 va2
"0"

所以,暂时还是没有想到,什么好方法。watch是目前能想到的唯一方式。

暂时记录:

2020-09-06 21:59:57.396  INFO --- [Rollbacking_1_1] i.s.s.coordinator.DefaultCoordinator     : Exception retry rollbacking ... 
==>
io.seata.common.exception.StoreException: Resource is returned to the pool as broken
	at io.seata.server.storage.redis.store.RedisTransactionStoreManager.updateGlobalTransactionDO(RedisTransactionStoreManager.java:299)
	at io.seata.server.storage.redis.store.RedisTransactionStoreManager.writeSession(RedisTransactionStoreManager.java:113)
	at io.seata.server.storage.redis.session.RedisSessionManager.updateGlobalSessionStatus(RedisSessionManager.java:101)
	at io.seata.server.session.AbstractSessionManager.onStatusChange(AbstractSessionManager.java:122)
	at io.seata.server.session.GlobalSession.changeStatus(GlobalSession.java:165)
	at io.seata.server.session.SessionHelper.endRollbacked(SessionHelper.java:115)
	at io.seata.server.coordinator.DefaultCore.doGlobalRollback(DefaultCore.java:338)
	at io.seata.server.coordinator.DefaultCoordinator.handleRetryRollbacking(DefaultCoordinator.java:287)
	at io.seata.server.coordinator.DefaultCoordinator.lambda$init$20(DefaultCoordinator.java:380)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.runAndReset$$$capture(FutureTask.java:308)
	at java.util.concurrent.FutureTask.runAndReset(FutureTask.java)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:745)
Caused by: redis.clients.jedis.exceptions.JedisException: Resource is returned to the pool as broken
	at redis.clients.jedis.JedisPool.returnResource(JedisPool.java:254)
	at redis.clients.jedis.Jedis.close(Jedis.java:3505)
	at io.seata.server.storage.redis.store.RedisTransactionStoreManager.updateGlobalTransactionDO(RedisTransactionStoreManager.java:298)
	... 17 common frames omitted
Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR DISCARD without MULTI
	at redis.clients.jedis.Protocol.processError(Protocol.java:132)
	at redis.clients.jedis.Protocol.process(Protocol.java:166)
	at redis.clients.jedis.Protocol.read(Protocol.java:220)
	at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:318)
	at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:236)
	at redis.clients.jedis.Transaction.discard(Transaction.java:83)
	at redis.clients.jedis.Transaction.clear(Transaction.java:36)
	at redis.clients.jedis.Transaction.close(Transaction.java:92)
	at redis.clients.jedis.BinaryJedis.resetState(BinaryJedis.java:1904)
	at redis.clients.jedis.JedisPool.returnResource(JedisPool.java:250)
	... 19 common frames omitted

你可能感兴趣的:(Seata分布式事务框架,redis)