随着springcloud使用的越来越普遍,微服务也趋向于成熟,既然都分成微服务了,势必也会是分库的设方式,既然分库了,肯定会遇到分布式事务的问题,这是任何一个微服务架构设计当中逃不掉的拦路虎。关于分布式事务,网上有很多讨论,也有很多解决方案,但他们都有一个共同的缺点,就是侵入式开发,而且使用起来,也过于复杂,和业务不解偶。
本方案使用起来简单易懂,和业务解耦、业务层面不需要关心分布式事务的问题(本方案已经生产系统当中大规模使用,如有不懂之处,可以v:hekf520)
1、先来看两张图,(1)在发起事务阶段,订单系统调用库存系统,先就行预减库存,预减成功,订单就往下走,如果预减失败,就中止订单生成。库存系统处调用pushMessage方法,通知【事务控制系统】介入。针对业务层面来说,工作已做完,后面的事由【事务控制系统】定时调用【订单系统】获取订单生成是否成功,
(a)如果订单生成功,就通知【库存系统】确认减掉库存。
(b)如果订单生失败或未生成,就通知【库存系统】取消预减。
(c)如果订单未支付,就过一段时间查询订单状态,根据状态,再通知【库存系统】
(d)定时查询,我们用到了RabbitMQ的延时队列,下一次等待时间为等比数列公式计算而来,公式为:5(秒)x((2的n次方)-1),比如订单有效期为900秒(15分钟),那么等待查询的时间为5、15、35、75、155、315、635、900
2、创建一个test的表,用于测试,我们只模拟上面图片带红色字部分(为了测试方便,将三个系统的相关接口放在了事务控制系统里暂做简单测试):flag1字段代表【订单系统】生成订单的状态,flag2代表【库存系统】减库存的状态(flag1和flag2具体怎么样,在项目中根据自己的业务来,这里只是模拟)
CREATE TABLE `test` (
`id` char(32) NOT NULL COMMENT '记录id',
`flag1` int(1) NOT NULL COMMENT 'url1的flag 0初始 1成功 2重试 3重试失败,取消',
`flag2` int(32) NOT NULL COMMENT 'url2的flag 根据flag1的结果来',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='测试记录';
另外两个表用于记录分布式事务相关的日志
CREATE TABLE `record` (
`record_id` char(32) NOT NULL COMMENT '记录id',
`pk1` varchar(32) NOT NULL COMMENT 'url1的主键',
`pk2` varchar(32) NOT NULL COMMENT 'url2的主键',
`type` int(1) NOT NULL COMMENT '类型',
`flag` int(1) NOT NULL DEFAULT '0' COMMENT '处理标志 0初始 1成功 2重试 3重试失败,取消',
`times` int(1) NOT NULL DEFAULT '0' COMMENT '处理次数',
`max_expiration` int(1) NOT NULL COMMENT '最大超时秒',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`url1` varchar(1024) NOT NULL COMMENT '请求地址一',
`url2` varchar(1024) NOT NULL COMMENT '请求地址二',
`creater` char(16) NOT NULL COMMENT '创建人',
`updater` char(16) NOT NULL COMMENT '更新人',
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`record_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='事务记录';
CREATE TABLE `record_log` (
`log_id` char(32) NOT NULL COMMENT '日志id',
`record_id` char(32) NOT NULL COMMENT '记录id',
`state` int(1) NOT NULL DEFAULT '0' COMMENT '处理标志 0初始 1成功 2重试 3重试失败,取消',
`url1` varchar(1024) NOT NULL COMMENT '请求地址一',
`url2` varchar(1024) NOT NULL COMMENT '请求地址二',
`content` varchar(512) DEFAULT NULL COMMENT '内容',
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`log_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='事务记录日志';
3、运行transation项目的TransApplication启动起来,访问 http://localhost:8001/trans/swagger-ui.html,模拟上面图1红色字部分
4、这时我们来看RabbitMQ上面的情况
4、我们看一下关键的几个类,RabbitMq的
@Configuration
public class DelayedRabbitMQConfig {
private static final String active="my";
// 延迟队列 TTL 名称
private static final String DELAY_QUEUE = active+".delay.queue";
// DLX,dead letter发送到的 exchange
// 延时消息就是发送到该交换机的
public static final String DELAY_EXCHANGE = active+".delay.exchange";
// routing key 名称
// 具体消息发送在该 routingKey 的
public static final String DELAY_ROUTING_KEY = active+".delay.routing.key";
//立即消费的队列名称
public static final String FLASH_QUEUE = active+".flash.queue";
// 立即消费的exchange
public static final String FLASH_EXCHANGE = active+".flash.exchange";
//立即消费 routing key 名称
public static final String FLASH_ROUTING_KEY = active+".flash.routing.key";
/**
* 创建一个延时队列
*/
@Bean
public Queue delayQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", FLASH_EXCHANGE);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", FLASH_ROUTING_KEY);
return new Queue(DELAY_QUEUE, true, false, false, params);
}
/**
* 延迟交换机
*/
@Bean
public DirectExchange toDirectExchange() {
// 一共有三种构造方法,可以只传exchange的名字, 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
// 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
// new DirectExchange(ORDER_DELAY_EXCHANGE,true,false);
return new DirectExchange(DELAY_EXCHANGE);
}
/**
* 把延时队列和 订单延迟交换的exchange进行绑定
* @return
*/
@Bean
public Binding delayBinding() {
return BindingBuilder.bind(delayQueue()).to(toDirectExchange()).with(DELAY_ROUTING_KEY);
}
/**
* 创建一个立即消费队列
*/
@Bean
public Queue flashQueue() {
// 第一个参数为queue的名字,第二个参数为是否支持持久化
return new Queue(FLASH_QUEUE, true);
}
/**
* 立即消费交换机
*/
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(FLASH_EXCHANGE);
}
/**
* 把立即队列和 立即交换的exchange进行绑定
* @return
*/
@Bean
public Binding flashBinding() {
// TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键
return BindingBuilder.bind(flashQueue()).to(topicExchange()).with(FLASH_ROUTING_KEY);
}
}
@Component
@Slf4j
public class MQReceiverListener {
private static Logger logger = LoggerFactory.getLogger(MQReceiverListener.class);
@Autowired
private RecordService recordService;
//监听消息队列
@RabbitListener(queues = {DelayedRabbitMQConfig.FLASH_QUEUE})
public void consumeMessage(RecordMqVo mqVo, Message message, Channel channel) throws IOException {
log.info("处理订阅消息开始.......");
System.out.println(mqVo);
boolean boo1=true;
boolean boo2=true;
Gson gs = new Gson();
String errMsg=null;
Integer flag = null;
LinkedTreeMap transmitMap=null;
try {
logger.info("请求url1:"+mqVo.getUrl1());
String str1 = HttpUtil.doPost(mqVo.getUrl1(),new HashMap());
ResultInfoVo resultInfoVo = gs.fromJson(str1,ResultInfoVo.class);
transmitMap = (LinkedTreeMap)resultInfoVo.getData();
Integer data = Double.valueOf(transmitMap.get("flag").toString()).intValue();
if(TanServerConstants.RECORD_FLAG.str1.equals(data)){
//成功
flag=data;
}else if(TanServerConstants.RECORD_FLAG.str0.equals(data)||TanServerConstants.RECORD_FLAG.str2.equals(data)){
//等待
//再次发送消息
mqVo.setContent("等待订单失效中...");
messageReTry(mqVo,channel,message);
return;
}else if(TanServerConstants.RECORD_FLAG.str3.equals(data)){
//取消
flag=data;
}
}catch (Exception e){
boo1=false;
mqVo.setContent("url1请求异常:"+e.getMessage());
messageReTry(mqVo,channel,message);
e.printStackTrace();
return;
}
if(boo1) {
try {
String url2=mqVo.getUrl2();
logger.info("请求url2:"+url2);
String str2 = HttpUtil.doPostJson(url2, new Gson().toJson(transmitMap));
ResultInfoVo resultInfoVo = gs.fromJson(str2, ResultInfoVo.class);
if(!ResultInfoVo.SUCCESS.equals(resultInfoVo.getCode())){
boo2=false;
mqVo.setContent("请求url2出错:"+resultInfoVo.getMessage());
messageReTry(mqVo,channel,message);
return;
}
} catch (Exception e) {
boo2 = false;
mqVo.setContent("url2请求异常:" + e.getMessage());
messageReTry(mqVo,channel,message);
e.printStackTrace();
}
}
try {
if(boo1&boo2){
recordService.updateRecord(mqVo.getRecordId(), flag,1,errMsg);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
mqVo.setContent("确信信息步骤出错:"+e.getMessage());
messageReTry(mqVo,channel,message);
e.printStackTrace();
}
}
/**
* 信息重试
* @param mqVo
* @param channel
* @param message
*/
public void messageReTry(RecordMqVo mqVo,Channel channel,Message message){
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
reTry(mqVo);
logger.info(mqVo.getContent());
}
/**
* 重试
* @param mqVo
*/
private void reTry(RecordMqVo mqVo){
logger.error("重试信息");
System.out.println(mqVo);
String maxExpirationStr=mqVo.getMaxExpirationStr();
int times = mqVo.getTimes()+1;
mqVo.setTimes(times);
mqVo.setFlag(TanServerConstants.RECORD_FLAG.str2);
int nextTime = recordService.getNextTime(times);
String nextExpirationStr=mqVo.setNextExpiration(nextTime);
if(maxExpirationStr.compareTo(nextExpirationStr)>=0){
recordService.pushMessage(mqVo);
}else{
Date currentDate = new Date();
String currentTimeStr = DateUtils.dateToString(currentDate,"yyyy-MM-dd HH:mm:ss");
if(currentTimeStr.compareTo(maxExpirationStr)==1){
//超时抛弃
String errMsg = "超时抛弃";
mqVo.setContent(errMsg);
recordService.updateRecord(mqVo.getRecordId(), TanServerConstants.RECORD_FLAG.str3,1,errMsg);
}else{
Date maxExpirationDate = DateUtils.stringToDate(maxExpirationStr,"yyyy-MM-dd HH:mm:ss");
System.out.println(currentDate+" "+maxExpirationDate);
Long milliSecond=DateUtils.plusDateMilliSecond(currentDate,maxExpirationDate);
System.out.println("milliSecond=="+milliSecond);
if(milliSecond.intValue()>1000) {//必须大于1秒以上
mqVo.setNextExpiration(milliSecond.intValue());//延缓3秒
recordService.pushMessage(mqVo);
}else{
String errMsg = "超时抛弃";
mqVo.setContent(errMsg);
recordService.updateRecord(mqVo.getRecordId(), TanServerConstants.RECORD_FLAG.str3,1,errMsg);
}
}
}
}
}
5、看一样业务层面的几个关键类
@RestController
@RequestMapping("/record")
@Api(tags = "RecordController", description = "分布式事务记录控制器")
@Validated
public class RecordController {
@Autowired
private RecordService recordService;
@PostMapping(value = "/pushMessage")
@ApiOperation(value = "推送消息")
public ResultInfoVo pushMessage(@RequestBody RecordMqParam param){
RecordMqVo mqVo = new RecordMqVo();
CommonUtils.copyPropertiesToMap(param,mqVo,true);
Integer nextExpiration = recordService.getNextTime(1);
mqVo.setNextExpiration(nextExpiration);
mqVo = recordService.pushMessage(mqVo);
return new ResultInfoVo(mqVo);
}
@PostMapping(value = "/getTestFlag1")
@ApiOperation(value = "(微服务1)获取使用状态FlagTarget,其中flag获取状态,返回值为 1成功 2重试 3重试失败,取消")
public ResultInfoVo getTestFlag1(String pk1){
System.out.println("获取使用状态:pk1="+pk1);
Test test = recordService.findTest();
FlagTarget target = new FlagTarget();//FlagTarget会自动传给confirmFlag1方法
target.setFlag(test.getFlag1());
target.setPk1(pk1);
target.setPk2("123456");
target.setDesc("测试,这个字段为业务新加的字段");
return new ResultInfoVo(target);
}
@PostMapping(value = "/confirmFlag1")
@ApiOperation(value = "(微服务2)确认预减:FlagTarget由刷新系统自动传递过来")
public ResultInfoVo confirmFlag1(@RequestBody FlagTarget target){
System.out.println("确认预减:"+new Gson().toJson(target));
recordService.updateTestFlag2(target.getFlag());
return new ResultInfoVo();
}
}
@Service
public class RecordServiceImpl implements RecordService {
private static Logger logger = LoggerFactory.getLogger(RecordServiceImpl.class);
@Autowired
private RecordDao recordDao;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private CommonDao commonDao;
/**
* 推送消息
* @param mqVo
*/
@Transactional(Transactional.TxType.REQUIRES_NEW)
public RecordMqVo pushMessage(RecordMqVo mqVo){
System.out.println(new Gson().toJson(mqVo));
Record record = null;
if(StringUtils.isNotEmpty(mqVo.getRecordId())){
record = recordDao.findTById(Record.class,mqVo.getRecordId());
if(record!=null){
record.setTimes(mqVo.getTimes());
record.setFlag(mqVo.getFlag());
RecordLog recordLog = new RecordLog();
String id = Utils.createPrimaryKey("L",32);
CommonUtils.copyPropertiesToMap(mqVo,recordLog,true);
CommonUtils.copyPropertiesToMap(record,recordLog,true);
recordLog.setLogId(id);
if(TanServerConstants.RECORD_FLAG.str1.equals(mqVo.getFlag())){
recordLog.setState(TanServerConstants.YES);
}else {
recordLog.setState(TanServerConstants.NO);
}
commonDao.saveT(recordLog);
}else{
logger.error("事务记录未找到,记录id为:"+mqVo.getRecordId());
}
}else{
record = new Record();
CommonUtils.copyProperties(mqVo,record,true);
String id = Utils.createPrimaryKey("R",32);
record.setRecordId(id);
record.setTimes(0);
record.setFlag(TanServerConstants.RECORD_FLAG.str0);
record.setCreater("system");
record.setUpdater("system");
recordDao.saveT(record);
CommonUtils.copyProperties(record,mqVo,true);
mqVo.setCreatedStr(DateUtils.dateToString(new Date(),"yyyy-MM-dd HH:mm:ss"));
}
try {
logger.info("发送消息:"+mqVo);
this.rabbitTemplate.convertAndSend(DelayedRabbitMQConfig.DELAY_EXCHANGE, DelayedRabbitMQConfig.DELAY_ROUTING_KEY, mqVo, message -> {
// 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间
message.getMessageProperties().setExpiration(mqVo.getNextExpiration().toString());
return message;
});
} catch (Exception e) {
logger.error("向mq中发送消息,出现异常:"+e.getMessage());
e.printStackTrace();
}
return mqVo;
}
/**
* 修改状态
*/
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void updateRecord(String recordId,Integer flag,Integer times,String content) {
Record record = null;
if (StringUtils.isNotEmpty(recordId)) {
record = recordDao.findTById(Record.class, recordId);
if (record != null) {
if(times!=null) {
record.setTimes(record.getTimes()+times);
}
if(flag!=null){
record.setFlag(flag);
}
RecordLog recordLog = new RecordLog();
String id = Utils.createPrimaryKey("L",32);
CommonUtils.copyPropertiesToMap(record,recordLog,true);
recordLog.setLogId(id);
if(TanServerConstants.RECORD_FLAG.str1.equals(flag)){
recordLog.setState(TanServerConstants.YES);
}else {
recordLog.setState(TanServerConstants.NO);
}
recordLog.setContent(content);
commonDao.saveT(recordLog);
} else {
logger.error("更新事务记录,事务记录未找到,记录id为:" + recordId);
}
}
}
/**
* 下次超时时间
* @param time
* @return
*/
public Integer getNextTime(Integer time){
Double result = 5000*(Math.pow(2.0,time)-1);
return result.intValue();
}
/**
* 下次超时运行时间
* @param millisecond
* @return
*/
public String getNextTimeString(Integer millisecond,String maxExpirationStr){
String nextTimeStr;
Date date = new Date();
System.out.println(date);
Date nextDate = DateUtils.plusSeconds(date,millisecond/1000);
String nextDateStr = DateUtils.dateToString(nextDate,"yyyy-MM-dd HH:mm:ss");
if(nextDateStr.compareTo(maxExpirationStr)==1){
nextTimeStr= maxExpirationStr;
}else{
nextTimeStr= nextDateStr;
}
System.out.println("nextTimeStr=="+nextTimeStr);
return nextTimeStr;
}
public Test findTest(){
Test test = (Test)commonDao.findTById(Test.class,"1");
return test;
}
@Transactional
public void updateTestFlag2(Integer flag2){
Test test = (Test)commonDao.findTById(Test.class,"1");
test.setFlag2(flag2);
}
}
本方案已经生产系统当中大规模使用,如有不懂之处,可以v:hekf520,可以直接使用在项目中,如需要源码,需进行付(*>.<*)费(29.8)