一、本地消息表原理
1、本地消息表方案介绍
本地消息表的最终一致方案
采用BASE原理,保证事务最终一致
在一致性方面,允许一段时间内的不一致,但最终会一致。
在实际系统中,要根据具体情况,判断是否采用。(有些场景对一致性要求较高,谨慎使用)
2、本地消息表的使用场景
基于本地消息表的方案中,将本事务外操作,记录在消息表中
其他事务,提供操作接口
定时任务轮询本地消息表,将未执行的消息发送给操作接口。
操作接口处理成功,返回成功标识,处理失败,返回失败标识。
定时任务接到标识,更新消息的状态
定时任务按照一定的周期反复执行
对于屡次失败的消息,可以设置最大失败次数
超过最大失败次数的消息,不进行接口调用
等待人工处理
例如使用支付宝的支付场景,系统生成订单,支付宝系统支付成功后,调用我们系统提供的回调接口,回调接口更新订单状态为已支付。回调通知执行失败,支付宝会过一段时间再次调用。
3、本地消息表架构图
4、优缺点
优点: 避免了分布式事务,实现了最终一致性
缺点: 注意重试时的幂等性操作
二、本地消息表数据库设计
整体工程复用前面的my-tcc-demo
1、两台数据库 134和129。user_134 创建支付消息表payment_msg, user_129数据库创建订单表t_order
2、使用MyBatis-generator 生成数据库映射文件,生成后的结构如下图所示
三、支付接口
1、创建支付服务PaymentService
@Service public class PaymentService { @Resource private AccountAMapper accountAMapper; @Resource private PaymentMsgMapper paymentMsgMapper; /** * 支付接口 * @param userId 用户Id * @param orderId 订单Id * @param amount 支付金额 * @return 0: 成功; 1:用户不存在 2:余额不足 */ @Transactional(transactionManager = "tm134") 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.setFailCnt(0); //失败次数 paymentMsg.setCreateTime(new Date()); paymentMsg.setCreateUser(userId); paymentMsg.setUpdateTime(new Date()); paymentMsg.setUpdateUser(userId); paymentMsgMapper.insertSelective(paymentMsg); return 0; } }
2、创建Controller层
@RestController public class PaymentController { @Autowired private PaymentService paymentService; //localhost:8080/payment?userId=1&orderId=10010&amount=200 @RequestMapping("payment") public String payment(int userId, int orderId, BigDecimal amount){ int result = paymentService.payment(userId, orderId,amount); return "支付结果:" + result; } }
3、调用接口
localhost:8080/payment?userId=1&orderId=10010&amount=200
查看表。账号表account_a 扣掉了200元, 支付消息表插入了一条支付记录。
四、订单操作接口
1、创建订单服务
@Service public class OrderService { @Resource OrderMapper orderMapper; /** * 订单回调接口 * @param orderId * @return 0:成功 1:订单不存在 */ 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; } }
2、创建Controller
@RestController public class OrderController { @Autowired private OrderService orderService; //localhost:8080/handlerOrder?orderId=10010 @RequestMapping("handlerOrder") public String handlerOrder( int orderId){ try { int result = orderService.handleOrder(orderId); if(result == 0){ return "success"; } return "fail"; }catch (Exception e){ return "fail"; } } }
调用方式: localhost:8080/handlerOrder?orderId=10010
五、定时任务
1、增加注解EnableScheduling
@SpringBootApplication @EnableScheduling //表明项目中可以使用定时任务 public class MyTccDemoApplication { public static void main(String[] args) { SpringApplication.run(MyTccDemoApplication.class, args); } }
2、创建服务OrderSchedule
@Service public class OrderSchedule { @Resource private PaymentMsgMapper paymentMsgMapper; //给订单处理接口发送通知 @Scheduled(cron = "0/10 * * * * ?") public void orderNotify() throws IOException { List<PaymentMsg> list = paymentMsgMapper.selectUnSendMsgList(); if (list == null || list.size() == 0) { return; } for (PaymentMsg paymentMsg : list) { int orderId = paymentMsg.getOrderId(); CloseableHttpClient httpClient = HttpClientBuilder.create().build(); HttpPost httpPost = new HttpPost("http://localhost:8080/handlerOrder"); NameValuePair orderIdPair = new BasicNameValuePair("orderId", orderId + ""); List<NameValuePair> nvlist = new ArrayList<>(); nvlist.add(orderIdPair); HttpEntity httpEntity = new UrlEncodedFormEntity(nvlist); 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 { int failCnt = paymentMsg.getFailCnt(); failCnt ++; paymentMsg.setFailCnt(failCnt); if(failCnt > 5){ paymentMsg.setStatus(2); //超过5次,改成失败 } paymentMsg.setUpdateUser(0); //系统更新 paymentMsg.setUpdateTime(new Date()); paymentMsgMapper.updateByPrimaryKey(paymentMsg); } } } }
3、模拟
1) 将订单表的状态改成0: 未支付
2) 清空消息表
3) 将UserID为1的用户金额改成1000
4) 调用支付接口
http://localhost:8080/payment?userId=1&orderId=10010&amount=200
支付成功后,用户A的金额变成了800,并在支付消息表中生成了一条支付记录。
定时任务查询支付消息表,查找未支付的支付消息记录,然后调用订单操作接口。订单操作接口调用后,将订单状态改成1:成功。订单操作接口返回成功后,则将支付消息的状态改成已支付。
5、处理失败模拟
在handleOrder方法中抛出异常。
作者:Work Hard Work Smart
出处:http://www.cnblogs.com/linlf03/
欢迎任何形式的转载,未经作者同意,请保留此段声明!