关于分布式事务

CAP原理

  • C:Consistent,一致性。具体是指,操作成功以后所有的节点,在同一时间内,看到的数据是完全一致的,一致性说的就是数据一致性。
  • A:Availability,可用性。值服务一致可用,在规定的时间内完成相应。
  • P: Parition tolerance, 分区容错性。指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。

XA协议

  1. XA协议是由X/Open组织提出的分布式事务的规范。
  2. 由一个事务管理器(TM)和多个资源管理器(RM)组成。
  3. 两阶段提交:提交分两个阶段:prepare和commit。
    – 保证数据的强一致性
    – commit阶段出现问题,事务出现不一致,需人工处理
    – 效率低下,性能与本地事务相差10倍
    – MySql5.7及以上均支持XA协议
    – MySql Connector/J 5.0以上支持XA协议
    – Java系统中,数据源采用Atomikos
    在这里插入图片描述
    在这里插入图片描述代码实现:
    1.pom.xml 引入 starter
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

2.采用配置类的方式配置多数据源

@Configuration
@MapperScan(value = "com.example.xademo.db128.dao",sqlSessionFactoryRef = "sqlSessionFactoryBean128")
public class ConfigDb128 {

    @Bean("db128")
    public DataSource db131(){
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUser("imooc");
        xaDataSource.setPassword("Imooc@123456");
        xaDataSource.setUrl("jdbc:mysql://192.168.163.128:3306/xa_128");
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        return atomikosDataSourceBean;
    }

    @Bean("sqlSessionFactoryBean128")
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db128") DataSource dataSource) throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db128/*.xml"));
        return sqlSessionFactoryBean;
    }

    @Bean("xaTransaction")
    public JtaTransactionManager jtaTransactionManager(){
        UserTransaction userTransaction = new UserTransactionImp();
        UserTransactionManager userTransactionManager = new UserTransactionManager();

        return new JtaTransactionManager(userTransaction,userTransactionManager);
    }

}

@Configuration
@MapperScan(value = "com.example.xademo.db129.dao",sqlSessionFactoryRef = "sqlSessionFactoryBean129")
public class ConfigDb129 {

    @Bean("db129")
    public DataSource db131(){
        MysqlXADataSource xaDataSource = new MysqlXADataSource();
        xaDataSource.setUser("imooc");
        xaDataSource.setPassword("Imooc@123456");
        xaDataSource.setUrl("jdbc:mysql://192.168.163.129:3306/xa_129");
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        return atomikosDataSourceBean;
    }

    @Bean("sqlSessionFactoryBean129")
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db129") DataSource dataSource) throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db129/*.xml"));
        return sqlSessionFactoryBean;
    }

}

  • 注意:DataSource都要归AtomikosDataSourceBean统一管理
  • 注意:新建事务Jta的事物管理器JtaTransactionManager
  • 注意:JtaTransactionManager属于TM,两个DataSource属于RM

3.测试

@Service
public class XAService {

    @Resource
    private XA129Mapper xa129Mapper;

    @Resource
    private XA128Mapper xa128Mapper;


    @Transactional(transactionManager = "xaTransaction")
    public void testXA(){

        XA129 xa129 = new XA129();
        xa129.setId(1);
        xa129.setName("xa_129");
        xa129Mapper.insert(xa129);

        XA128 xa128 = new XA128();
        xa128.setId(1);
        xa128.setName("xa_128");
        xa128Mapper.insert(xa128);

    }

}
@SpringBootTest
@RunWith(value = SpringRunner.class)
public class XaDemoApplicationTests {

    @Autowired
    private XAService xaService;

    @Test
    public void contextLoads() {
    }

    @Test
    public void testXA(){
        xaService.testXA();
    }

}

Mycat

mycat 版本 1.6 以后是支持事务的,主要体现在server.xml里面:

<!--分布式事务开关,0为不过滤分布式事务,1为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤),2为不过滤分布式事务,但是记录分布式事务日志-->
<property name="handleDistributedTransactions">0</property>
  • 0: 表示开启分布式事务
  • 1: 表示不开启分布式事务

1.在Navicat中执行插入语句需:

SET autocommit = 0;
set xa = on;

INSERT INTO `user`(id,username) VALUES(1,'奇数'),(2,'偶数123456');

commit;

2.在项目中执行直接连接mycat即可,和直接连接mysql是一样的,配置数据源,执行程序。

spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://192.168.163.129:8066/user?serverTimezone=Asia/Shanghai&useSSL=false
 @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void testUser(){
        User user1 = new User();
        user1.setId(1);
        user1.setUsername("奇数");
        userMapper.insert(user1);

        User user2 = new User();
        user2.setId(2);
        user2.setUsername("偶数123456");
        userMapper.insert(user2);
    }

Sharding JDBC

sharding JDBC 默认开启了分布式事务

事务补偿机制

什么是事务补偿机制?

针对每个操作都要注册一个与其对应的补偿(撤销)操作。
在执行失败的时候,调用补偿操作,撤销之前的操作。

基于本地消息表的最终一致性方案

  • 采用Base原理,保证事务的最终一致性方案。
  • 在一致性方面,允许一段时间内的不一致,但最终会一致。
  • 在实际的系统和当中,要根据具体情况,判断是否采用。

原理:
1.基于本地消息表的方案中,将本事务外操作,记录在消息表中。
2. 其他事务提供操作接口。
3. 定时任务轮询本地消息表,将未执行的消息发送给操作接口。
4. 操作接口处理成功返回成功标识,处理失败返回失败标识。
5. 定时任务接到标识,更新消息的状态。
6. 定时任务按照一定的周期反复执行。
7. 对于屡次失败的消息,可以设置最大失败次数。
8. 超过最大的失败次数的消息,不在进行接口调用。
9. 等待人工补偿。
在这里插入图片描述

@RestController
public class PaymentController {

    @Autowired
    private PaymentService paymentService;

    @RequestMapping("payment")
    public String payment(int userId , int orderId, BigDecimal amount){
        int payment = paymentService.payment(userId, orderId, amount);
        return "支付结果 :" + payment;
    }
}
@Service
public class PaymentService {

    @Resource
    private AccountAMapper accountAMapper;

    @Resource
    private PaymentMsgMapper paymentMsgMapper;

    /**
     * 支付接口
     * @param userId
     * @param orderId
     * @param amount
     * @return 1:用户不存在;2: 余额不足;0:代表成功。
     *
     */
    @Transactional(transactionManager = "tm128")
    public int payment(int userId , int orderId, BigDecimal amount){
        //支付操作
        AccountA accountA = accountAMapper.selectByPrimaryKey(userId);
        if(accountA == null){
            return 1;
        }
        if(accountA.getBalance().compareTo(amount) < 0){
            return 2;
        }
        accountA.setBalance(accountA.getBalance().subtract(amount));
        accountAMapper.updateByPrimaryKey(accountA);

        PaymentMsg paymentMsg = new PaymentMsg();
        paymentMsg.setOrderId(orderId);
        paymentMsg.setStatus(0);//未发送
        paymentMsg.setFalureCnt(0);
        paymentMsg.setCreateTime(new Date());
        paymentMsg.setCreateUser(userId);
        paymentMsg.setUpdateTime(new Date());
        paymentMsg.setUpdateUser(userId);
        paymentMsgMapper.insertSelective(paymentMsg);
        return 0;
    }
}
@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @RequestMapping("handleOrder")
    public String handleOrder(int orderId){
        try{
            int result = orderService.handleOrder(orderId);
            if(result == 0){
                return "success";
            }
            return "fail";
        }catch (Exception e){
            return "fail";
        }
    }
}
@Service
public class OrderService {
    @Resource
    private OrderMapper orderMapper;
    /**
     * 订单回调接口
     * @param orderId
     * @return 1: 订单不存在;0: 成功。
     */
    @Transactional(transactionManager = "tm129")
    public int handleOrder(int orderId){
        Order order = orderMapper.selectByPrimaryKey(orderId);
        if(order == null){
            return 1;
        }
        order.setOrderStatus(1);//已支付
        order.setUpdateTime(new Date());
        order.setUpdateUser(0); //系统更新
        orderMapper.updateByPrimaryKey(order);
        return 0;
    }
}
@Service
public class OrderScheduler {

    @Resource
    private PaymentMsgMapper paymentMsgMapper;

    @Scheduled(cron = "0/10 * * * * ?")
    public void orderNotify() throws IOException {
        System.out.println("================================");
        PaymentMsgExample paymentMsgExample = new PaymentMsgExample();
        paymentMsgExample.createCriteria().andStatusEqualTo(0);//未发送
        List<PaymentMsg> paymentMsgs = paymentMsgMapper.selectByExample(paymentMsgExample);
        if(paymentMsgs == null || paymentMsgs.isEmpty()){
            return;
        }
        for (PaymentMsg paymentMsg : paymentMsgs) {
            Integer orderId = paymentMsg.getOrderId();

            CloseableHttpClient httpClient = HttpClientBuilder.create().build();
            HttpPost httpPost = new HttpPost("http://localhost:8080/handleOrder");
            NameValuePair orderPair = new BasicNameValuePair("orderId",orderId+"");
            List<NameValuePair> list = new ArrayList<>();
            list.add(orderPair);
            HttpEntity httpEntity = new UrlEncodedFormEntity(list,"utf-8");
            httpPost.setEntity(httpEntity);
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String s = EntityUtils.toString(response.getEntity());
            if("success".equals(s)){
                paymentMsg.setStatus(1);//发送成功
                paymentMsg.setUpdateTime(new Date());
                paymentMsg.setUpdateUser(0);//系统更新
                paymentMsgMapper.updateByPrimaryKey(paymentMsg);
            }else{
                Integer falureCnt = paymentMsg.getFalureCnt();
                falureCnt++;
                paymentMsg.setFalureCnt(falureCnt);
                if(falureCnt > 5){
                    paymentMsg.setStatus(2);//失败
                    paymentMsg.setUpdateTime(new Date());
                    paymentMsg.setUpdateUser(0);//系统更新
                    paymentMsgMapper.updateByPrimaryKey(paymentMsg);
                }
                paymentMsg.setUpdateTime(new Date());
                paymentMsg.setUpdateUser(0);//系统更新
                paymentMsgMapper.updateByPrimaryKey(paymentMsg);
            }
        }

    }

}

基于MQ的最终一致性方案

  • 原理与本地消息表类似。
  • 不同点:本地消息表改为MQ。
  • 定时任务改为MQ的消费者。
  • 优点:不依赖定时任务,基于MQ更高效,更可靠。
  • 适合公司内的系统,不同公司之间无法基于MQ,本地消息表更合适。
@Configuration
public class RocketMQConfig {

    @Bean(initMethod = "start", destroyMethod = "shutdown")
    public DefaultMQProducer producer(){
        DefaultMQProducer producer = new
                DefaultMQProducer("paymentGroup");
        // Specify name server addresses.
        producer.setNamesrvAddr("localhost:9876");
        return producer;
    }


    @Bean(initMethod = "start",destroyMethod = "shutdown")
    public DefaultMQPushConsumer consumer(@Qualifier("messageListener") MessageListenerConcurrently messageListener) throws MQClientException {
        // Instantiate with specified consumer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("paymentConsumerGroup");

        // Specify name server addresses.
        consumer.setNamesrvAddr("localhost:9876");

        // Subscribe one more more topics to consume.
        consumer.subscribe("payment", "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener(messageListener);
        return consumer;
    }
}
@Component("messageListener")
public class ChangeOrderStatus implements MessageListenerConcurrently {

    @Resource
    private OrderMapper orderMapper;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list,
                                                    ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        if(list == null || list.isEmpty()){
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        for (MessageExt messageExt : list) {
            String orderId = messageExt.getKeys();
            byte[] body = messageExt.getBody();
            String msg = new String(body);
            System.out.println("msg = " + msg);
            Order order = orderMapper.selectByPrimaryKey(Integer.parseInt(orderId));
            if(order == null){
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
            try{
                order.setOrderStatus(1);//已支付
                order.setUpdateTime(new Date());
                order.setUpdateUser(0); //系统更新
                orderMapper.updateByPrimaryKey(order);
            }catch (Exception e){
                e.printStackTrace();
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}
    /**
     * 支付接口(消息队列)
     * @param userId
     * @param orderId
     * @param amount
     * @return 1:用户不存在;2: 余额不足;0:代表成功。
     *
     */
    @Transactional(transactionManager = "tm128",rollbackFor = Exception.class)
    public int paymentMQ(int userId , int orderId, BigDecimal amount) throws Exception {
        //支付操作
        AccountA accountA = accountAMapper.selectByPrimaryKey(userId);
        if(accountA == null){
            return 1;
        }
        if(accountA.getBalance().compareTo(amount) < 0){
            return 2;
        }
        accountA.setBalance(accountA.getBalance().subtract(amount));
        accountAMapper.updateByPrimaryKey(accountA);

        Message message = new Message();
        message.setTopic("payment");
        message.setKeys(orderId+"");
        message.setBody("订单已支付".getBytes());
        try {
            SendResult result = producer.send(message);
            if(result.getSendStatus() == SendStatus.SEND_OK){
                return 0;
            }else{
                throw new Exception("消息发送失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值