作者: | 来源:互联网 | 2023-09-12 13:54
一、本地消息表原理
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 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 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方法中抛出异常。