前言:关于分布式事务话题一直是颇有争议的话题,在本文中通过ActiveMq 实现分布式事务做一个简单的demo;同时也让自己能在实践中可以获取经验和对分布式事务自己的一些思考。
1.本地事务
我们通常只需借助开发平台中特有数据访问技术和框架(例如Spring、JDBC、ADO.NET),结合关系型数据库自带的事务管理机制来实现事务性的需求。例如A给B转账100元并发送100代金券,不管是服务器挂掉还是转账失败抛出异常,我们最终都要保证这个流程要么都成功要么都失败,否则会出现数据异常。
2.分布式事务
余额表和代金券表分布在不同的节点的数据库,转账和发放代金券是不同的应用,它们之间通信可能通过rpc,httpclient,mq;假设这时候A服务给B转账成功,但是发放代金券失败,我们应该如何处理呢?笔者在现在公司项目里就有很多这样的问题,我们是和第三方经常有数据交互,那么调用第三方的接口进行划拨操作,有可能在第三方划拨成功但是消息丢失(网络异常、服务器挂掉、某些人新加的不合理代码导致异常回滚等等)
3.使用消息队列ActiveMq实现事务一致性
- 以下 demo简单模拟用户注册后发放代金券这一过程;流程首先是用户注册成功后推送用户信息到Active mq,代金券应用中也配置好了Active Mq,但是它是充当消费者的角色,实现代金券消息监听,当监听到消息后会拉取Active Mq的消息发执行派发金券动作; 其中用户注册是一个应用,发放代金券是另外一个应用,它们之间是通过activemq实现消息收发。
- 首先创建2个maven项目,分别叫account和voucher,在这里我用的是springmvc+jdbc作为项目骨架。
- 在account项目中我新首先建了一个UserController.java作为注册的控制层,并提供注册的方法,如下代码示例,其中注意的是增加了一张消息表,关于为什么需要消息表下面会详细解答。
-
1 package com.zdd.mvc; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.jms.core.JmsTemplate; 5 import org.springframework.jms.core.MessageCreator; 6 import org.springframework.stereotype.Controller; 7 import org.springframework.ui.ModelMap; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 import org.springframework.web.bind.annotation.RequestMethod;10 import org.springframework.web.bind.annotation.ResponseBody;11 import utils.ActiveMQutil;12 import utils.JdbcUtil;13 import utils.Result;14 15 import javax.jms.JMSException;16 import javax.jms.Message;17 import javax.jms.Session;18 19 /**20 * Created by dada on 2017/8/25.21 */22 @Controller23 @RequestMapping("/register")24 public class UserAccountController {25 26 @Autowired27 private JmsTemplate jmsTemplate;28 29 30 @RequestMapping(method = RequestMethod.GET)31 public String register() {32 return "register";33 }34 35 36 @RequestMapping(method = RequestMethod.POST,value = "/doReg")37 @ResponseBody38 public Result doReg(final String phone) {39 JdbcUtil jdbcUtil = null;40 try{41 jdbcUtil = new JdbcUtil();42 jdbcUtil.getConnection();43 44 jdbcUtil.setAutoCommit(false); //往账户表添加一条数据45 String sql = "insert into account(phone) values ('"+phone+"')";46 int row = jdbcUtil.insert(sql);47 if(row == 1){48 //插入到消息记录表49 sql = "insert into message(phone,status) values ('"+phone+"',0)";50 int m_row = jdbcUtil.insert(sql);51 if(m_row == 1){52 //成功后发送队列53 jmsTemplate.send("voucher_message", new MessageCreator() {54 @Override55 public Message createMessage(Session session) throws JMSException {56 return session.createTextMessage(phone);57 }58 });59 }60 }61 jdbcUtil.Commit();62 63 }catch (RuntimeException e){64 e.printStackTrace();65 jdbcUtil.rollback();//出现异常事务回滚66 }finally {67 if(null != jdbcUtil){68 jdbcUtil.releaseConn();69 }70 }71 Result result = new Result();72 return result;73 }74 75 }
消息表主要用处是:
-
假如我们消息投递到消息中间件后,消费者那边出现异常,虽然信息已经被消费者消费了,但由于代码或宕机导致消费端数据事务没有成功提交,如果没有消息表,我们将会丢失这一条数据。有了消息表后我们可以查询到有哪些是属于未成功派发的数据,这时候可以通过轮询或者是其他方式再次把这批未成功消费的数据重新派发出去。
-
根据上述代码及注释,我们来分析下可能的情况:
-
操作数据库成功,向MQ中投递消息也成功,皆大欢喜。
-
操作数据库失败,不会向MQ中投递消息了。
-
操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚。
- 操作数据库成功,投递MQ消息成功,消费异常,数据未更新,通过扫描消息表再次把数据取出进行消费。
从上面分析的几种情况来看,貌似问题都不大的。那么我们来分析下消费者端面临的问题:
-
消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致。
-
尽量避免消息重复消费,消费前先查询一下是否消费成功,一定要有一个标识标明,如果重复消费,也不能因此影响业务结果,保证幂等性。
-
时序图: