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。

下次有空了周末在家集成一下然后发布出去..

 

posted on 2019-08-12 08:59  冰龙之剑  阅读(424)  评论(0编辑  收藏  举报

导航