可靠消息:张三一定保证消息发出去了。
最终一致性:李四失败张三不能回滚,李四你无论如何要把钱加上。
重复消息要解决幂等性。
要解决的问题:
1.本地事务与消息发送的原子性问题
2.事务参与方接受消息的可靠性
3.消息重复消费问题
------------------------------------------------01-----------------------------------------------------------------
方案:本地消息表方案。
第一步:解决本地事务和消息的原子性,发送成功才删除积分日志。否则等待下一个周期重试。只要没发送就一直发送。
第二步:积分服务监听消息队列,成功消费给mq一个回应,mq有消息确认机制确认就不推送了,不回的话会一直发送。
------------------------------------------------------------------------------02------------------------------------------------------------------
RocketMQ事务消息方案。
逻辑请求:
1.张三是发送方,发送数据给mq
2.半消息,mq返回发送成功。
3.执行本地事务,发送commit,此时mq把自己的消息更改为可消费。
4.mq发给消费者李四账户加钱。
5.发送端扣款失败则发一个rollback的ack,此时回滚。
6.mq会定时回查事务是不是提交了,没提交可以提交的。
7.检查本地事务状态是不是有问题了,有问题就自己处理。
------------------------------------------------------------------------------03------------------------------------------------------------------
开发案例:
1.创建数据库导入脚本
2.
3.启动mq注意这个mq直接在我们的本地运行就可以。
win启动rockerMQ,在我的电脑路径:D:\softWork\rocketmq4.5.0
4.导入项目
------------------------------------------------------------------------------04------------------------------------------------------------------
bank1就是mq的发起方,bank2就是mq的消费方。
bank1:张三扣减金额,本地事务提交,发消息。
第一步:
手动创建事务:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AccountChangeEvent implements Serializable {
/**
* 账号
*/
private String accountNo;
/**
* 变动金额
*/
private double amount;
/**
* 事务号
*/
private String txNo;
}
@RestController
@Slf4j
public class AccountInfoController {
@Autowired
private AccountInfoService accountInfoService;
@GetMapping(value = "/transfer")
public String transfer(@RequestParam("accountNo")String accountNo, @RequestParam("amount") Double amount){
//创建一个事务id,作为消息内容发到mq
String tx_no = UUID.randomUUID().toString();
AccountChangeEvent accountChangeEvent = new AccountChangeEvent(accountNo,amount,tx_no);
//发送消息
accountInfoService.sendUpdateAccountBalance(accountChangeEvent);
return "转账成功";
}
}
//将accountChangeEvent转成json
JSONObject jsonObject =new JSONObject();
jsonObject.put("accountChange",accountChangeEvent);
String jsonString = jsonObject.toJSONString();
//生成message类型
Message<String> message = MessageBuilder.withPayload(jsonString).build();
//发送一条事务消息
/**
* String txProducerGroup 生产组
* String destination topic,
* Message<?> message, 消息内容
* Object arg 参数
*/
rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1","topic_txmsg",message,null);
注意这里是有问题的,老师也说了
第二步:
//更新账户,扣减金额
@Override
@Transactional
public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
//幂等判断
if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
return ;
}
//扣减金额
accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount() * -1);
//添加事务日志 判断幂等
accountInfoDao.addTx(accountChangeEvent.getTxNo());
if(accountChangeEvent.getAmount() == 3){
throw new RuntimeException("人为制造异常");
}
}
实现了发送消息和更新本地事务。
------------------------------------------------------------------------------05------------------------------------------------------------------
消息发送成功回调。
回查状态。
我们写一个接口的实现类。
执行本地事务:
//事务消息发送后的回调方法,当消息发送给mq成功,此方法被回调
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try {
//解析message,转成AccountChangeEvent
String messageString = new String((byte[]) message.getPayload());
JSONObject jsonObject = JSONObject.parseObject(messageString);
String accountChangeString = jsonObject.getString("accountChange");
//将accountChange(json)转成AccountChangeEvent
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
//执行本地事务,扣减金额
accountInfoService.doUpdateAccountBalance(accountChangeEvent);
//当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
}
这里是rollback则mq就删掉消息了。
发送commit则队列的消息就是可以消费了。
-------------------------------------------------------------------------------------
什么时候事务回查?就是没有收到4的确认的时候。
事务的回查:张三是不是扣减金额了。
//事务状态回查,查询是否扣减金额
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
//解析message,转成AccountChangeEvent
String messageString = new String((byte[]) message.getPayload());
JSONObject jsonObject = JSONObject.parseObject(messageString);
String accountChangeString = jsonObject.getString("accountChange");
//将accountChange(json)转成AccountChangeEvent
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
//事务id
String txNo = accountChangeEvent.getTxNo();
int existTx = accountInfoDao.isExistTx(txNo);
if(existTx>0){
return RocketMQLocalTransactionState.COMMIT;
}else{
// 这个就是等网络好了 还要继续发送的
return RocketMQLocalTransactionState.UNKNOWN;
}
}
注意:在controller要有事务号:
------------------------------------------------------------------------------06------------------------------------------------------------------
李四的微服务:接受消息,监听mq,增加金额。
//更新账户,增加金额
@Override
@Transactional
public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
log.info("bank2更新本地账号,账号:{},金额:{}",accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
return ;
}
//增加金额
accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
//添加事务记录,用于幂等
accountInfoDao.addTx(accountChangeEvent.getTxNo());
if(accountChangeEvent.getAmount() == 4){
throw new RuntimeException("人为制造异常");
}
}
监听mq:
@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2",topic = "topic_txmsg")
public class TxmsgConsumer implements RocketMQListener<String> {
@Autowired
AccountInfoService accountInfoService;
//接收消息
@Override
public void onMessage(String message) {
log.info("开始消费消息:{}",message);
//解析消息
JSONObject jsonObject = JSONObject.parseObject(message);
String accountChangeString = jsonObject.getString("accountChange");
//转成AccountChangeEvent
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
//设置账号为李四的
accountChangeEvent.setAccountNo("2");
//更新本地账户,增加金额
accountInfoService.addAccountInfoBalance(accountChangeEvent);
}
}
注意上面的代码和配置文件的消息组是不一样的。
rocketmq.producer.group = producer_bank2
rocketmq.name-server = 127.0.0.1:9876
------------------------------------------------------------------------------07------------------------------------------------------------------
测试:
李四接到消息必须一直消费,直到成功。
张三的异常,在执行本地事务时候制造异常:
李四的异常:出现异常就重复的消费。也是执行本地事务制造异常。
onMessage抛出异常就循环接收。循环消费。不能回滚,要自己处理,可以打印日志什么的。消费多了。
------------------------------------------------------------------------------08----------------------------------------------------------------------------
最大努力通知:
可靠消息:保证消息发送成功我就提交事务,发送方发出去不会回滚。
最大努力通知:可靠性由接收方去保证。
最大努力不能用于交易,用于交易后的通知。
------------------------------------------------------------------------------09------------------------------------------------------------------
解决方案:最大努力基于ACK机制。
------------------------------------------------------------------------------10------------------------------------------------------------------
我们实现方案1:
第一步:数据库:
还需要bank1。
第二步:启动rockermq。
第三步:-n 127.0.0.1:9876 autoCreateTopicEnable=true
第三步:导入工程
第四步:注意
生产组的码字要和程序里面的生产组的名字是不一样的。
第五步:写代码
@Service
@Slf4j
public class AccountPayServiceImpl implements AccountPayService {
@Autowired
AccountPayDao accountPayDao;
@Autowired
RocketMQTemplate rocketMQTemplate;
//插入充值记录
@Override
public AccountPay insertAccountPay(AccountPay accountPay) {
int success = accountPayDao.insertAccountPay(accountPay.getId(), accountPay.getAccountNo(), accountPay.getPayAmount(), "success");
if(success>0){
//发送通知,使用普通消息发送通知
accountPay.setResult("success");
rocketMQTemplate.convertAndSend("topic_notifymsg",accountPay);
return accountPay;
}
return null;
}
//查询充值记录,接收通知方调用此方法来查询充值结果
@Override
public AccountPay getAccountPay(String txNo) {
AccountPay accountPay = accountPayDao.findByIdTxNo(txNo);
return accountPay;
}
}
第六步:写controller生成事务的编号
第七步:账户系统,监听mq通知,主动查询充值系统,账户系统会重复接受通知,要做好幂等。
@Autowired
AccountInfoDao accountInfoDao;
@Autowired
PayClient payClient;
//更新账户金额 监听mq有消息会调用这个方法
@Override
@Transactional
public void updateAccountBalance(AccountChangeEvent accountChange) {
//幂等校验
if(accountInfoDao.isExistTx(accountChange.getTxNo())>0){
return ;
}
int i = accountInfoDao.updateAccountBalance(accountChange.getAccountNo(), accountChange.getAmount());
//插入事务记录,用于幂等控制
accountInfoDao.addTx(accountChange.getTxNo());
}
//远程调用查询充值结果
@Override
public AccountPay queryPayResult(String tx_no) {
//远程调用
AccountPay payresult = payClient.payresult(tx_no);
if("success".equals(payresult.getResult())){
//更新账户金额
AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
accountChangeEvent.setAccountNo(payresult.getAccountNo());//账号
accountChangeEvent.setAmount(payresult.getPayAmount());//金额
accountChangeEvent.setTxNo(payresult.getId());//充值事务号
updateAccountBalance(accountChangeEvent);
}
return payresult;
}
第八步:写mq的监听
@Component
@Slf4j
@RocketMQMessageListener(topic = "topic_notifymsg",consumerGroup = "consumer_group_notifymsg_bank1")
public class NotifyMsgListener implements RocketMQListener<AccountPay> {
@Autowired
AccountInfoService accountInfoService;
//接收消息
@Override
public void onMessage(AccountPay accountPay) {
log.info("接收到消息:{}", JSON.toJSONString(accountPay));
if("success".equals(accountPay.getResult())){
//更新账户金额
AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
accountChangeEvent.setAccountNo(accountPay.getAccountNo());
accountChangeEvent.setAmount(accountPay.getPayAmount());
accountChangeEvent.setTxNo(accountPay.getId());
accountInfoService.updateAccountBalance(accountChangeEvent);
}
log.info("处理消息完成:{}", JSON.toJSONString(accountPay));
}
}
------------------------------------------------------------11------------------12---------------13---------------------------------------------------
测试:
1.屏蔽发送结果到mq,账户系统主动查询。
充值成功
张三主动查询更新自己:
2.发送消息,账户去监听
-----------------------------------------------------------------------14--------------------------------------------------------------