mq解决分布式事物问题【代码】
上节课简单说了一下mq是怎么保证数据一致性的。下面直接上代码了。
所需环境:1、zookeepor注册中心 2、kafka的服务端和工具客户端(工具客户端也可以不要只是为了更方便的查看消息而已) 3、springcloud的消息生产者 4、springcloud的消息消费者。
1、zk的安装和启动。百度有很多,kafka是依赖于zk的,所以zk必须要有。
2、kafka的服务端安装和启动。安装选择2进制的,不要选源码安装【我就遇到过坑,切记】,启动命令:进入kafka的安装目录后按住Shift键然后鼠标右键选择在此处打开命令窗口然后输入.\bin\windows\kafka-server-start.bat .\config\server.properties (说明:kafka的服务端下载完成后默认的配置文件中的zk是本地的localhost:2181,自己的端口默认是9092,都是可以根据实际情况进行修改的)
3、springcloud 集成 kafka的消息生产者:
pom.xml:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-kafka</artifactId> </dependency>
application.yml: 这里我写的比较简单,输出的通道是在java代码中项目启动的时候去加载的,不在配置文件中,若topic主题不多建议放在配置文件中,因为我的topic比较多,采用的是动态生成的..
cloud: stream: kafka: binder: brokers: localhost:9092 # kafka服务地址和端口 zk-nodes: localhost:2181 # ZK的集群配置地址和端口
项目启动加载topic:
@Component @EnableBinding public class KafkaTopicConfig { private static final Logger logger = LoggerFactory.getLogger(KafkaTopicConfig.class); @Autowired private BinderAwareChannelResolver resolver; @PostConstruct public void initKafkaTopic() { logger.info("初始化topic begin..");
// 这里我写死了topic,其实可以动态的去表中读取,然后循环去调用下面的方法就好了 String topicName = "order"; // 这行代码是动态去生成topic的,先检查kafka中有没有传入的topic,有就直接返回topic,没有则新建 resolver.resolveDestination(topicName); } }
控制层到发送消息的代码:
@Autowired private IntegrateService integrateService; /** * 下单操作,将个人账户充值100元,下单和充值分别属于不同库不同项目 * @param order * @return */ @RequestMapping("/createOrder") R createOrder(@RequestBody Order order) { order.setCreateTime(new Date()); order.setOrderNo(System.currentTimeMillis() + MathUtil.getFiveRandom()); integrateService.createOrder(order); return R.ok(); } @Component public class IntegrateService { /** * log */ private static final Logger logger = LoggerFactory.getLogger(IntegrateService.class); @Autowired private OrderService orderService; @Autowired private SendMessage sendMessage; @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { try{ // 本地下单操作 orderService.createOrder(order); Msg msg = Msg.getMsg("下单操作",1L, order); // 下单后将发送消息到kafka通知消费者进行账户加100 sendMessage.sendOrderMessage(msg, "order"); }catch (Exception e) { logger.error("下单失败..", e); } } }
4、springcloud集成kafka的消息消费者:
pom.xml和上面的一致。
application.yml:
cloud:
stream:
kafka:
binder:
brokers: localhost:9092 # kafka服务地址和端口
zk-nodes: localhost:2181 # ZK的集群配置地址和端口
bindings:
inboundOrgChanges: #默认为input
destination: order #此处order是输出者定义的
content-type: application/json
group: licensingGroup #消费者组保证消息只被一组服务实例处理一次
定义接收接口:
/** * @Title: CustomChannels * @Description: 定义输入通道和yml中的配置一致, * @author: sunxuesong@hztianque.com * @date: Created in 21:43 2019/8/11 * @Modifired by: */ public interface CustomChannels { /** * 接收订单消息通道 * @return */ @Input("inboundOrgChanges") SubscribableChannel receiveOrderMsg(); }
消息监听进行消费账户加100:
@EnableBinding(CustomChannels.class) public class ConsumerHandler { private static final Logger logger = LoggerFactory.getLogger(ConsumerHandler.class); @Autowired private AmountService amountService; @StreamListener("inboundOrgChanges") public void receiveOrderMsg(String msg) { logger.info("接收消息msg:{}",msg); if (StringUtils.isEmpty(msg)) { return ; } JSONObject jsonObject = JSONObject.parseObject(msg); jsonObject = jsonObject.getJSONObject("data"); Long userId = Long.parseLong(jsonObject.getString("userId")); Double amount = Double.parseDouble(jsonObject.getString("amount")); // 先查询当前账户然后和下单的金额相加 Account account1 = amountService.getAmountByUserId(userId); BigDecimal b1 = new BigDecimal(Double.toString(account1.getAmount())); BigDecimal b2 = new BigDecimal(Double.toString(amount)); Account account = new Account(); account.setAmount(b1.add(b2).doubleValue()); account.setUserId(userId); amountService.updateAmountByUserId(account); // return出去,不然会出现重复消费,后面有机会的话做全局id+日志进行控制幂等性 return; } }
下单之后在kafka的客户端中可以看到topic中的消息:
消费端一旦监听到topic中有消息就会立马进行消费。
虽然最终能保证消费者和生产者的消息最终一致性,但是难免会有一点点的延迟。这种方式不怎么好,分布式事物的控制还有其他方式:比如LCN解决。
LCN是进行分段提交的:两段提交协议或者三段提交协议,集成之后只需要在方法上加一个@TxTransactional主键就可以了。并且两边的数据是同时进行commit的,没有延迟。推荐使用LCN。
下次有空了周末在家集成一下然后发布出去..