基于MQ的2PC分布式事务

​​​​
在这里插入图片描述

上图阐释了如何基于mq实现2pc的分布式事务

  • 一阶段红线部分。
  • 二阶段为蓝线部分。

图中展示了较为复杂的调用方式,S1调用S2、S3,S3又调用了S4。
感谢seata开源社区大佬的帮助。虽然2pc本身存在很多问题,但是自己手动实现一遍还是学习到很多。
本文仅做参考,不具备生产意义。
seata社区陈建斌大佬指正的问题列表如下:
问题
第一:tm需要有事务记录表,来恢复事务,而且要考虑到rm没任何异常,只是因为tm宕机导致tm的二阶段提交没有入库,但是由于这样,rm本身应该提交的事务变成了回滚。
第二:需要把connection换为xaconnection,使用xa协议来保证rm宕机后事务数据可恢复。
第三:要保证消息队列中间件的高可用。
第四:要防止资源悬挂问题,因为没有了分支事务注册,很可能因为网络或者其它因素,先发后置了,导致了tm没感知到这个rm的存在,这个rm就可能因为用了xa协议导致死锁。

show your code

根据上图我们可以很好的实现代码如下:此处基于rocketmq方式实现。

引入以下包

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

全局事务注解此注解开启全局事务,真正事务还是交给Transactional注解去执行

package com.xxx.mq.trx.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Description TODO
 * @Author 姚仲杰
 * @Date 2021/1/2 21:36
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GlobalTransaction {

}

全局事务切面

package com.xxx.mq.trx.aspect;

import com.xxx.mq.trx.config.TransactionConst;
import com.xxx.mq.trx.core.TrxContextHolder;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.StringUtils;

/**
 * @Description TODO
 * @Author 姚仲杰
 * @Date 2021/1/2 21:38
 */
public class GlobalTrxAspect {

    @Autowired
    RocketMQTemplate rocketMQTemplate;

    @Pointcut("@annotation(com.xxx.mq.trx.config.GlobalTransaction)")
    public void pointcut(){}

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        //方法执行前需生成trx_id
        //判断是否事务发起者,如果能从线程上下文取到事务id说明是参与者,如果取不到则是事务管理者。
        String trx_id = TrxContextHolder.getTrxId();
        boolean isManager = false;
        if (StringUtils.isEmpty(trx_id)) {
            UUID uuid = UUID.randomUUID();
            TrxContextHolder.setTrxId(uuid.toString());
            isManager=true;
        }
        Map map=new HashMap(2);
        map.put(TransactionConst.TRX_ID,trx_id);
        try {
            joinPoint.proceed();
            map.put(trx_id, TransactionConst.COMMIT);
        } catch (Throwable throwable) {
            map.put(trx_id, TransactionConst.ROLLBACK);
            throw throwable;
        }finally {
            //方法执行后需发送消息告知所有事务参与者是提交还是回滚
            if(isManager) {
                Message msg = MessageBuilder.withPayload(map).build();
                rocketMQTemplate.send(TransactionConst.TRX_TOPIC, msg);
            }
        }
    }
}

事务常量定义

package com.xxx.mq.trx.config;

/**
 * @Description TODO
 * @Author 姚仲杰
 * @Date 2021/1/4 9:28
 */
public interface TransactionConst {
    int COMMIT=1;
    int ROLLBACK=0;
    String TRX_ID="trx_id";
    String TRX_TOPIC="global_trx_topic";
    String TRX_GROUP="global_trx_group";
}

package com.xxx.mq.trx.aspect;

import com.xxx.mq.trx.core.ConnectionProxy;
import com.xxx.mq.trx.core.TrxContextHolder;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description 拦截getConnection调用用于处理事务手动提交
 * @Author 姚仲杰
 * @Date 2021/01/04 11:46
 */
@Aspect
@Component
public class DataSourceAspect {
    @Autowired
    ObjectFactory<ConnectionProxy> bean;

    ReentrantLock lock = new ReentrantLock();

    @Around("execution(* javax.sql.DataSource.getConnection(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Connection conn = (Connection)joinPoint.proceed();
        String trxId=TrxContextHolder.getTrxId();
        if (StringUtils.isNotBlank(trxId)) {
            ConnectionProxy connectionProxy = bean.getObject();
            connectionProxy.setConnection(conn);
            lock.lock();
            try {
                List<ConnectionProxy> list = TrxContextHolder.getConnections(trxId);
                if (list == null) {
                    list = new ArrayList<>();
                }
                list.add(connectionProxy);
                TrxContextHolder.setConnections(trxId,list);
            } finally {
                lock.unlock();
            }
            return connectionProxy;
        }
        return conn;
    }

}

连接代理让Transactional注解的事务提交执行个寂寞,然后转交由我们自己mq通知提交。

package com.xxx.mq.trx.core;

import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

/**
 * @Description TODO
 * @Author 姚仲杰
 * @Date 2021/1/4 10:48
 */
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ConnectionProxy implements Connection {
    private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionProxy.class);

    private Connection connection;
    //mq收到事务通知之后调用此方法执行提交或回滚
    public void notify(int state) {
        try {
            if (state == 1) {
                connection.commit();
            } else {
                connection.rollback();
            }
            connection.close();
        } catch (Exception e) {
            LOGGER.error(e.getLocalizedMessage(), e);
        }
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {
        connection.setAutoCommit(false);
    }

    @Override
    public void commit() throws SQLException {
        // connection.commit();
    }

    @Override
    public void rollback() throws SQLException {
        // connection.rollback();
    }

    @Override
    public void close() throws SQLException {
        // connection.close();
    }

    @Override
    public boolean getAutoCommit() throws SQLException {
        return connection.getAutoCommit();
    }

    @Override
    public Statement createStatement() throws SQLException {
        return connection.createStatement();
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return connection.prepareStatement(sql);
    }

    @Override
    public CallableStatement prepareCall(String sql) throws SQLException {
        return connection.prepareCall(sql);
    }

    @Override
    public String nativeSQL(String sql) throws SQLException {
        return connection.nativeSQL(sql);
    }

    @Override
    public boolean isClosed() throws SQLException {
        return connection.isClosed();
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return connection.getMetaData();
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {
        connection.setReadOnly(readOnly);
    }

    @Override
    public boolean isReadOnly() throws SQLException {
        return connection.isReadOnly();
    }

    @Override
    public void setCatalog(String catalog) throws SQLException {
        connection.setCatalog(catalog);
    }

    @Override
    public String getCatalog() throws SQLException {
        return connection.getCatalog();
    }

    @Override
    public void setTransactionIsolation(int level) throws SQLException {
        connection.setTransactionIsolation(level);
    }

    @Override
    public int getTransactionIsolation() throws SQLException {
        return connection.getTransactionIsolation();
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return connection.getWarnings();
    }

    @Override
    public void clearWarnings() throws SQLException {
        connection.clearWarnings();
    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        return connection.createStatement(resultSetType, resultSetConcurrency);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
        throws SQLException {
        return connection.prepareStatement(sql, resultSetType, resultSetConcurrency);
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return connection.getTypeMap();
    }

    @Override
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
        connection.setTypeMap(map);
    }

    @Override
    public void setHoldability(int holdability) throws SQLException {
        connection.setHoldability(holdability);
    }

    @Override
    public int getHoldability() throws SQLException {
        return connection.getHoldability();
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        return connection.setSavepoint();
    }

    @Override
    public Savepoint setSavepoint(String name) throws SQLException {
        return connection.setSavepoint(name);
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {
        connection.rollback(savepoint);
    }

    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
        connection.releaseSavepoint(savepoint);
    }

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
        throws SQLException {
        return connection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
        int resultSetHoldability) throws SQLException {
        return connection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
        int resultSetHoldability) throws SQLException {
        return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        return connection.prepareStatement(sql, autoGeneratedKeys);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
        return connection.prepareStatement(sql, columnIndexes);
    }

    @Override
    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        return connection.prepareStatement(sql, columnNames);
    }

    @Override
    public Clob createClob() throws SQLException {
        return connection.createClob();
    }

    @Override
    public Blob createBlob() throws SQLException {
        return connection.createBlob();
    }

    @Override
    public NClob createNClob() throws SQLException {
        return connection.createNClob();
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        return connection.createSQLXML();
    }

    @Override
    public boolean isValid(int timeout) throws SQLException {
        return connection.isValid(timeout);
    }

    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException {
        connection.setClientInfo(name, value);
    }

    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException {
        connection.setClientInfo(properties);
    }

    @Override
    public String getClientInfo(String name) throws SQLException {
        return connection.getClientInfo(name);
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        return connection.getClientInfo();
    }

    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
        return connection.createArrayOf(typeName, elements);
    }

    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
        return connection.createStruct(typeName, attributes);
    }

    @Override
    public void setSchema(String schema) throws SQLException {
        connection.setSchema(schema);
    }

    @Override
    public String getSchema() throws SQLException {
        return connection.getSchema();
    }

    @Override
    public void abort(Executor executor) throws SQLException {
        connection.abort(executor);
    }

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
        connection.setNetworkTimeout(executor, milliseconds);
    }

    @Override
    public int getNetworkTimeout() throws SQLException {
        return connection.getNetworkTimeout();
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return connection.unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return connection.isWrapperFor(iface);
    }

    public Connection getConnection() {
        return connection;
    }

    public void setConnection(Connection connection) {
        this.connection = connection;
    }
}

事务上下文

package com.xxx.mq.trx.core;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description TODO
 * @Author 姚仲杰
 * @Date 2020/12/28 11:42
 */
public class TrxContext {

    private ThreadLocal<Map<String,String>> threadLocal=new ThreadLocal<Map<String,String>>(){
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<String, String>();
        }
    };

    public String put(String key, String value) {
        return threadLocal.get().put(key, value);
    }

    public String get(String key) {
        return threadLocal.get().get(key);
    }

    public String remove(String key) {
        return threadLocal.get().remove(key);
    }

    public Map<String, String> entries() {
        return threadLocal.get();
    }
}

事务上下文持有者缓存了trxId,以及全局事务连接列表等属性。

package com.xxx.mq.trx.core;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
 * @Description 
 * @Author 姚仲杰
 * @Date 2020/12/28 11:46
 */
public class TrxContextHolder {
    private static final Logger LOGGER = LoggerFactory.getLogger(TrxContextHolder.class);

    public static final TrxContext TRX_CONTEXT_HOLDER=new TrxContext();

    private static volatile ConcurrentHashMap<String, List<ConnectionProxy>> connectionsMap =
        new ConcurrentHashMap<>();

    public static final String TRX_ID="TRX_ID";

    public static String getTrxId(){
        return TRX_CONTEXT_HOLDER.get(TRX_ID);
    }

    public static void setTrxId(String trxId){
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("set trx_id:[{}]", trxId);
        }
        TRX_CONTEXT_HOLDER.put(TRX_ID, trxId);

    }

    public static String removeTrxId() {
        String trxId = TRX_CONTEXT_HOLDER.remove(TRX_ID);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("remove trx_id:[{}] ", trxId);
        }
        return trxId;
    }

    public static List<ConnectionProxy> getConnections(String trxId){
        if (StringUtils.isEmpty(trxId)){
            LOGGER.error("trx_id can not be empty");
            throw new IllegalArgumentException();
        }
        return connectionsMap.get(trxId);
    }

    public static void setConnections(String trxId,List<ConnectionProxy> connections){
        if (StringUtils.isEmpty(trxId)){
            LOGGER.error("trx_id can not be empty");
            throw new IllegalArgumentException();
        }
        if (CollectionUtils.isEmpty(connections)){
            LOGGER.error("connections can not be empty,require at least one connection");
            throw new IllegalArgumentException();
        }
        connectionsMap.put(trxId,connections);
    }

    public static void removeConnections(String trxId){
        if (StringUtils.isEmpty(trxId)){
            LOGGER.error("trx_id can not be empty");
            throw new IllegalArgumentException();
        }
        connectionsMap.remove(trxId);
    }
}

二阶段提交mq监听器

package com.xxx.mq.trx.core;

import com.alibaba.fastjson.JSON;
import com.xxx.mq.trx.config.TransactionConst;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.CollectionUtils;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Description TODO
 * @Author 姚仲杰
 * @Date 2021/1/4 11:18
 */
@RocketMQMessageListener(consumeMode = ConsumeMode.CONCURRENTLY,topic = TransactionConst.TRX_TOPIC,consumerGroup = TransactionConst.TRX_GROUP)
public class TransactionMassageListener implements MessageListenerConcurrently {
    public static final Logger LOGGER= LoggerFactory.getLogger(TransactionMassageListener.class);
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
        ConsumeConcurrentlyContext context) {
        LOGGER.info("receive global transaction message: {}",msgs);
        MessageExt messageExt = msgs.get(0);
        //如果本地获取不到事务等待连接直接返回消费成功,因为这是广播模式。
        try {
            String s = new String(messageExt.getBody(), "utf-8");
            Map map = JSON.parseObject(s, HashMap.class);
            String trxId= (String) map.get(TransactionConst.TRX_ID);
            int state= (int) map.get(trxId);
            List<ConnectionProxy> connections = TrxContextHolder.getConnections(trxId);
            if (!CollectionUtils.isEmpty(connections)){
                try {
                    connections.forEach(cp -> cp.notify(state));
                }finally {
                    TrxContextHolder.removeConnections(trxId);
                }
            }
        }catch (Throwable e){
           return  ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

dubbo事务id传播过滤器

package com.xxx.mq.trx.integration.dubbo;

import com.xxx.mq.trx.config.TransactionConst;
import com.xxx.mq.trx.core.TrxContextHolder;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Description 用户传递trx_id给下游服务,并将事务id绑定给本地线程变量
 * @Author 姚仲杰
 * @Date 2021/01/04 11:46
 */
@Activate(group = {"provider", "consumer"}, order = 100)
public class DubboTrxPropagationFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(DubboTrxPropagationFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String trxId = TrxContextHolder.getTrxId();
        String rpcXid = RpcContext.getContext().getAttachment(TransactionConst.TRX_ID);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("trxId in TrxContext[{}] trxId in RpcContext[{}]", trxId, rpcXid);
        }
        boolean bind = false;
        if (trxId != null) {
            RpcContext.getContext().setAttachment(TransactionConst.TRX_ID, trxId);
        } else {
            if (rpcXid != null) {
                TrxContextHolder.setTrxId(rpcXid);
                bind = true;
            }
        }
        try {
            return invoker.invoke(invocation);
        } finally {
            if (bind) {
                TrxContextHolder.removeTrxId();
            }
        }
    }

}

推荐阅读更多精彩内容

  • 1. 基础概念 CAP理论 一致性(Consistency) :在分布式系统中所有的数据备份,在同一时刻都保持一致...
    8090的大叔阅读 1,848评论 2 2
  • 3. 分布式事务解决方案之2PC(两阶段提交) 针对不同的分布式场景业界常见的解决方案有2PC、TCC、可靠消息最...
    小小一技术驿站阅读 1,042评论 2 2
  • 一、产生背景 为了应对互联网环境带来了海量的数据容量、连接数与访问量,通过数据拆分实现数据库能力的线性扩展,通过微...
    春涛的随笔阅读 1,502评论 0 3
  • 本地事务 在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事...
    知止9528阅读 111评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 3,916评论 13 21