seata分布式事务接入

一、分布式事务

为解决微服务建设后,各个业务中心之间耦合业务的数据强一致性问题,引入分布式事务。

本文档主要以seata为例说明。

分布式事务:http://seata.io/zh-cn/blog/seata-at-tcc-saga.html

GTS事务框架以及模式(偏server侧的说明):https://help.aliyun.com/document_detail/157850.html?spm=a2c4g.11186623.6.554.688b24a1NYmh7D

AT模式:https://mp.weixin.qq.com/s/Pypkm5C9aLPJHYwcM6tAtA

MT(Manual Transaction)模式(TCC):

http://seata.io/zh-cn/blog/tcc-mode-applicable-scenario-analysis.html

http://seata.io/zh-cn/blog/tcc-mode-design-principle.html

AT模式和MT模式的区别:

AT 模式基于 支持本地 ACID 事务 的 关系型数据库:

一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,MT 模式,不依赖于底层数据资源的事务支持:

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

二、依赖

接入seata需要依赖以下两个基础包

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.2.0</version>
</dependency>
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.9</version>
</dependency>

三、配置

## seata

seata.enabled=true

## 是否启用数据源代理(如果需要接入AT模式,则需要设置为true,如果不需要AT模式,则需要设置为false,默认为true)
seata.enable-auto-data-source-proxy=false

seata.application-id=${spring.application.name}
seata.client.rm.report-success-enable=true
seata.client.rm.table-meta-check-enable=false
seata.client.rm.report-retry-count=5
seata.client.rm.async-commit-buffer-limit=10000
seata.client.rm.lock.retry-interval=10
seata.client.rm.lock.retry-times=30
seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=true
seata.client.tm.commit-retry-count=3
seata.client.tm.rollback-retry-count=3
seata.client.log.exceptionRate=100

## 服务信息
## 分组
seata.tx-service-group=supplier-chain-seata-group
seata.service.vgroup-mapping.supplier-chain-seata-group=default

## seata server的地址
seata.service.grouplist.default=127.0.0.1:8091
seata.service.enable-degrade=false
seata.service.disable-global-transaction=false

seata.transport.shutdown.wait=3
seata.transport.thread-factory.boss-thread-prefix=NettyBoss
seata.transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker
seata.transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler
seata.transport.thread-factory.share-boss-worker=false
seata.transport.thread-factory.client-selector-thread-prefix=NettyClientSelector
seata.transport.thread-factory.client-selector-thread-size=1
seata.transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread
seata.transport.type=TCP
seata.transport.server=NIO
seata.transport.heartbeat=true
seata.transport.serialization=seata
seata.transport.compressor=none
seata.transport.enable-client-batch-send-request=true

## 事务注册中心,根据需要切换注册中心类型,此处为zk
seata.registry.type=zk
seata.registry.zk.cluster=default
## zk地址
seata.registry.zk.serverAddr=127.0.0.1:2181

三、事务实现

seata-srping-boot-starter包默认启动AT + MT 模式

AT模式不需要进行代码改造,直接启动即可

MT模式,需要针对业务进行接口改造,需要将现有业务拆分为三部分:prepare、commit、rollback

prepare:资源预占

commit:资源使用

rollback:资源返还

建议针对性的抽取出单独的service进行改造。

其中实现需要满足以下条件

1、需要定义接口类

2、接口类中需要标注 LocalTCC 注解

3、对应的prepare函数需要标注 TwoPhaseBusinessAction 注解,注解中需要指定 prepare、commit、rollback 三部分对应的函数名

4、prepare、commit、rollback三部分对应的函数都必须包含参数 BusinessActionContext actionContext

5、prepare 函数对于分布事务参数外,其他业务参数需要增加注解标记,保证对应的参数在commit和rollback中能够获取到。例如 @BusinessActionContextParameter(paramName = “param”)

代码示例

/**
 * 分布式事务
 *
 * @author yingchengpeng
 * @since 2020-11-11
 */
@LocalTCC
public interface TransactionalService {

    /**
     * 事务预提交
     */
    @TwoPhaseBusinessAction(name = "TccTransactionalService", commitMethod = "commit" , rollbackMethod = "rollback")
    boolean prepare(BusinessActionContext actionContext,
                    @BusinessActionContextParameter(paramName = "param") String param);

    /**
     * 事务提交,库存业务中没有具体的资源临时占用,因此提交环节直接返回true即可
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * 事务回滚
     */
    boolean rollback(BusinessActionContext actionContext);
}

四、事务流程保障

TCC模式下,分布式事务的接入需要注意以下注意事项:

1、prepare以及commit需要 保证幂等校验

2、commit需要保证执行时必定是成功的,或最终一定会执行成功

3、rollback需要支持空回滚

4、针对异常情况下先触发rollback后触发prepare的场景进行规避,防止事务悬挂

针对悬挂问题,给出以下实现思路

在prepare和rollback入口处进行行为标记,如果在rollback的时候发现正在prepare,则拒绝rollback,如果执行prepare的时候正在进行rollback或已经执行完成rollback,则拒绝prepare操作。

目前通过AOP实现,代码如下

事务状态标记管理类

@Component
public class TransactionalSafeManager {

    private final static String DISTRIBUTE_TRANSACTIONAL_LOCK = "bussiness_name:distribute_transactional_lock:";
    
    private interface DISTRIBUTE_TRANSACTIONAL_STATUS {
        String RUNNING = "running";
        String ROLLBACKING = "rollbacking";
        String ROLLBACKED = "rollbacked";
        String PRE_PARED = "pre_pared";
        String PRE_PARE_FAIL = "pre_pare_fail";
        String COMMITTED = "committed";
    }
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 标记进行中
     *
     * @param actionContext 事务上下文
     * @return 是否标记成功
     */
    public boolean marketRunning(BusinessActionContext actionContext) {
        String xid = getXid(actionContext);
        if (StringUtils.isEmpty(xid)) {
            return true;
        }
    
        Boolean result = redisTemplate.opsForValue().setIfPresent(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.RUNNING);
        if (result != null && result) {
            return true;
        }
    
        String value = redisTemplate.opsForValue().get(getDistributeTransactionalKey(xid));


        return value == null || value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.RUNNING)
                || value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.PRE_PARE_FAIL);
    }
    
    /**
     * 标记预处理完成
     *
     * @param actionContext 事务上下文
     */
    public void marketPrepared(BusinessActionContext actionContext) {
        String xid = getXid(actionContext);
        if (StringUtils.isEmpty(xid)) {
            return;
        }
        redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.PRE_PARED);
    }
    
    /**
     * 标记预处理失败
     *
     * @param actionContext 事务上下文
     */
    public void marketPreparedFail(BusinessActionContext actionContext) {
        String xid = getXid(actionContext);
        if (StringUtils.isEmpty(xid)) {
            return;
        }
        redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.PRE_PARE_FAIL);
    }
    
    /**
     * 标记回滚
     *
     * @param actionContext 事务上下文
     */
    public boolean marketRollBack(BusinessActionContext actionContext) {
        String xid = getXid(actionContext);
        if (StringUtils.isEmpty(xid)) {
            return true;
        }
    
        Boolean result = redisTemplate.opsForValue().setIfPresent(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKING);
        if (result != null && result) {
            return true;
        }
    
        String value = redisTemplate.opsForValue().get(getDistributeTransactionalKey(xid));
    
        return value == null || value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKING)
                || value.equalsIgnoreCase(DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKED);
    }
    
    /**
     * 标记回滚完成
     *
     * @param actionContext 事务上下文
     */
    public void marketRollBacked(BusinessActionContext actionContext) {
        String xid = getXid(actionContext);
        if (StringUtils.isEmpty(xid)) {
            return;
        }
        redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.ROLLBACKED);
    }
    
    /**
     * 标记已提交
     *
     * @param actionContext 事务上下文
     */
    public void marketCommitted(BusinessActionContext actionContext) {
        String xid = getXid(actionContext);
        if (StringUtils.isEmpty(xid)) {
            return;
        }
        redisTemplate.opsForValue().set(getDistributeTransactionalKey(xid), DISTRIBUTE_TRANSACTIONAL_STATUS.COMMITTED);
        redisTemplate.expire(getDistributeTransactionalKey(xid), 30, TimeUnit.MINUTES);
    }
    
    private String getXid(BusinessActionContext actionContext) {
        // 不存在,认为不需要管控分布式事务
        if (actionContext == null) {
            return null;
        }
    
        return actionContext.getXid();
    }
    
    private String getDistributeTransactionalKey(String xid) {
        return DISTRIBUTE_TRANSACTIONAL_LOCK + xid;
    }
}

切面实现

@Aspect
@Slf4j
@Component
@Order(0)
public class TccSafeAop {

    @Autowired
    private TransactionalSafeManager transactionalSafeManager;
    
    @Pointcut("execution(public * com.xxx.service.transactional.impl.*.*(..))")
    public void pointcut() {
    }
    
    private Map<String, String> prepareMethod = Maps.newConcurrentMap();
    private Map<String, String> commitMethod = Maps.newConcurrentMap();
    private Map<String, String> rollbackMethod = Maps.newConcurrentMap();
    
    @Around("pointcut()")
    public Object doAop(ProceedingJoinPoint joinPoint) throws Throwable {
    
        boolean isPrepare = false;
        BusinessActionContext actionContext = null;
        try {
            Object[] args = joinPoint.getArgs();
            if (args == null || args.length == 0) {
                return joinPoint.proceed();
            }
    
            if (args[0] instanceof BusinessActionContext) {
                Object returnObj;
                MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
                Method method = methodSignature.getMethod();
                actionContext = (BusinessActionContext) args[0];
                String actionName = (String) actionContext.getActionContext("actionName");
    
                String prepareMethodName = prepareMethod.get(actionName);
                if(StringUtils.isEmpty(prepareMethodName)) {
                    prepareMethodName = (String) actionContext.getActionContext("sys::prepare");
                    prepareMethod.put(actionName, prepareMethodName);
                }
    
                if(method.getName().equals(prepareMethodName)) {
                    boolean marketResult = transactionalSafeManager.marketRunning(actionContext);
                    if(!marketResult) {
                        throw new BizException("operate-has-rollback", "事务操作已经被回滚,操作失败");
                    }
                    isPrepare = true;
                    returnObj = joinPoint.proceed();
                    transactionalSafeManager.marketPrepared(actionContext);
                    return returnObj;
                }
    
                String commitMethodName = commitMethod.get(actionName);
                if(StringUtils.isEmpty(commitMethodName)) {
                    commitMethodName = (String) actionContext.getActionContext("sys::commit");
                    commitMethod.put(actionName, commitMethodName);
                }
    
                if(method.getName().equals(commitMethodName)) {
                    returnObj = joinPoint.proceed();
                    transactionalSafeManager.marketCommitted(actionContext);
                    return returnObj;
                }
    
                String rollbackMethodName = rollbackMethod.get(actionName);
                if(StringUtils.isEmpty(rollbackMethodName)) {
                    rollbackMethodName = (String) actionContext.getActionContext("sys::rollback");
                    rollbackMethod.put(actionName, rollbackMethodName);
                }
    
                if(method.getName().equals(rollbackMethodName)) {
                    boolean marketResult = transactionalSafeManager.marketRollBack(actionContext);
                    if(!marketResult) {
                        throw new BizException("operate-can-not-rollback", "事务正在预提交操作,操作回滚失败");
                    }
                    returnObj = joinPoint.proceed();
                    transactionalSafeManager.marketRollBacked(actionContext);
                    return returnObj;
                }
            }
    
            return joinPoint.proceed();
        } catch (BizException e) {
            if(isPrepare) {
                transactionalSafeManager.marketPreparedFail(actionContext);
            }
            throw e;
        } catch (Exception e) {
            if(isPrepare) {
                transactionalSafeManager.marketPreparedFail(actionContext);
            }
            throw new BizException("dubbo-api-invoke-fail", "dubbo接口调用失败");
        }
    }


}

五、Q & A

接入过程中遇到并解决的问题

5.1、启动加载找不到DruidDataSourceWrapper类

A: 依赖包中的dev-tools和seata的jar存在,冲突,目前主要通过去除dev-tools包解决该问题

5.2、xid组成?

xid = seata-server特定节点的ip:port:transactionId

5.3、如果发生回滚时,应用节点异常会怎么处理?

seata server在进行提交或回滚操作时会优先对同ip服务进行调用,如果找不到可用服务则会在找寻其他节点的服务进行调用。

源码:io.seata.core.rpc.netty.ChannelManager#getChannel

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值