MQ消息丢失和消息重复的解决方案和实战

场景

假设支付宝转账1000元到余额宝, 通过RabbitMQ对转账过程进行解耦,

支付宝将转账的消息投递到RabbitMQ, 余额宝通过监听RibbitMQ的消息队列获得消息, 然后通过应答队列告诉支付宝消息已经消费

MQ消息丢失和消息重复的解决方案和实战_第1张图片

遇到的问题

1. 当余额宝获取到消息之后, 可能转账失败, 消息队列不会关心余额宝是否操作成功, 这就是消息丢失的问题

MQ消息丢失和消息重复的解决方案和实战_第2张图片

2. 如果余额宝成功转账, 但响应队列迟迟没有将消费成功的消息告诉支付宝, 导致支付宝重复发送消息, 这就是消息重复发送的问题.

MQ消息丢失和消息重复的解决方案和实战_第3张图片

解决方案

1. 第一种是引入ZK, 顺序消费

2. 本地消息表

在支付宝端和余额宝端同时建立消息表

1. 当在支付宝端扣款成功的同时, 在消息表中建立一条记录, 状态标识为unconfirm, 将消息投递到消息队列

2. 余额宝从消息队列中获取消息后, 在余额宝中扣款成功后, 同时在消息表中建立一条消息, 状态标识为confirmed.

3. 余额宝将响应消息投递到响应队列, 支付宝获得响应后, 查询余额宝的消息表, 如果其中没有消费记录, 则插入新的消息。如果查询有消费的消息, 就停止插入, 并返回已经消费的消息。这样可以避免消息重复消费的问题。

4. 支付宝端会有一个定时任务, 相隔一段时间就从消息表中将unconfirm的消息拉取并重新发送, 这样可以避免消息丢失的问题

代码

数据库脚本

DROP DATABASE IF EXISTS `rabbit_taobao_consumer`;
CREATE DATABASE IF NOT EXISTS `rabbit_taobao_consumer` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `rabbit_taobao_consumer`;



DROP TABLE IF EXISTS `tb_account`;
CREATE TABLE IF NOT EXISTS `tb_account` (
  `user_id` varchar(10) NOT NULL,
  `amount` int(11) NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


INSERT INTO `tb_account` (`user_id`, `amount`, `update_time`) VALUES
	('SAM0000001', 0, '2019-09-22 14:30:49'),
	('SAM0000002', 0, '2019-09-22 14:02:59'),
	('SAM0000003', 0, '2019-09-22 14:03:09');

DROP TABLE IF EXISTS `tb_message`;
CREATE TABLE IF NOT EXISTS `tb_message` (
  `message_id` varchar(100) NOT NULL,
  `user_id` varchar(10) NOT NULL,
  `amount` int(11) NOT NULL,
  `state` varchar(10) NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


DROP DATABASE IF EXISTS `rabbit_taobao_provider`;
CREATE DATABASE IF NOT EXISTS `rabbit_taobao_provider` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `rabbit_taobao_provider`;


DROP TABLE IF EXISTS `tb_account`;
CREATE TABLE IF NOT EXISTS `tb_account` (
  `user_id` varchar(10) NOT NULL,
  `amount` int(11) NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


INSERT INTO `tb_account` (`user_id`, `amount`, `update_time`) VALUES
	('SAM0000001', 64000, '2019-09-22 14:30:48'),
	('SAM0000002', 80000, '2019-09-21 18:10:44'),
	('SAM0000003', 70000, '2019-09-21 18:10:57');


DROP TABLE IF EXISTS `tb_message`;
CREATE TABLE IF NOT EXISTS `tb_message` (
  `message_id` varchar(100) NOT NULL,
  `user_id` varchar(10) NOT NULL,
  `amount` int(11) NOT NULL,
  `state` varchar(10) NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建立alipay-server工程, 下面是pom.xml



   4.0.0
   
      org.springframework.boot
      spring-boot-starter-parent
      2.1.8.RELEASE
       
   
   com.teddy
   alipay-server
   0.0.1-SNAPSHOT
   alipay-server
   Demo project for Spring Boot

   
      1.8
   

   
      
         org.springframework.boot
         spring-boot-starter-amqp
      

      
         org.springframework.boot
         spring-boot-starter-web
         
      

      
         com.alibaba
         druid-spring-boot-starter
         1.1.10
      
      
         tk.mybatis
         mapper-spring-boot-starter
         2.0.2
      
      
         mysql
         mysql-connector-java
         runtime
      

      
         org.springframework.boot
         spring-boot-starter-test
         test
      
      
         org.springframework.amqp
         spring-rabbit-test
         test
      

        
            com.alibaba
            fastjson
            1.2.28
        
        
            org.springframework.boot
            spring-boot-starter-web
        
    

   
      
         
            org.springframework.boot
            spring-boot-maven-plugin
         
      
   


支付宝端的Dao文件

package com.teddy.alipayserver.dao;

import com.teddy.alipayserver.bean.Account;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Update;

public interface AccountMapper {
    @Update("update tb_account set amount=amount-#{amount}, update_time=now() where user_id=#{userId}")
    int updateAccount(Account account);
    @Insert("insert tb_account(user_id, amount, update_time) values(#{userId}, #{amount}, now())")
    int addAccount(Account account);
}
package com.teddy.alipayserver.dao;

import com.teddy.alipayserver.bean.Message;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Insert;

import java.util.List;

public interface MessageMapper {
    @Update("update tb_message set state=#{state} where message_id=#{message_id}")
    int updateMessage(Message message);

    @Insert("insert into tb_message(user_id, message_id, amount, state, update_time) values (#{user_id}, #{message_id}, #{amount}, 'unconfirm', now())")
    int addMessage(Message message);

    @Select("select * from tb_message where state=#{state}")
    List queryMessageByState(String state);
}

service接口

package com.teddy.alipayserver.service;

public interface AlipayService {
    //修改支付宝账户余额的接口
    public void updateAmount(int amount, String userId);
    //回调接口 修改本地消息表消息的状态
    public void updateMessage(String param);
}

service接口实现

package com.teddy.alipayserver.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.teddy.alipayserver.bean.Account;
import com.teddy.alipayserver.bean.Message;
import com.teddy.alipayserver.config.RabbitmqSender;
import com.teddy.alipayserver.dao.AccountMapper;
import com.teddy.alipayserver.dao.MessageMapper;
import com.teddy.alipayserver.service.AlipayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.Random;
@Service
public class AlipayServiceImpl implements AlipayService{
    @Autowired
    TransactionTemplate transactionTemplate;
    @Autowired
    AccountMapper accountMapper;
    @Autowired
    MessageMapper messageMapper;
    @Autowired
    RabbitmqSender rabbitmqSender;

    private static String SUCCESS="OK";

    /**
     * 1. 修改支付宝账户余额信息
     * 2. 插入本地消息表
     * 3. 往mq中插入消息, 供余额宝业务消息
     * @param amount
     * @param userId
     */
    @Override
    public void updateAmount(int amount, String userId) {
        String messageId=(String)transactionTemplate.execute(new TransactionCallback() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                //1. 修改余额宝账户余额信息
                Account account = new Account();
                account.setUserId(userId);
                account.setAmount(amount);
                int count=accountMapper.updateAccount(account);
                if(count==1){
                    //构建一个本地消息对账表
                    String messageId="Sam"+System.currentTimeMillis()+new Random().nextInt(Integer.MAX_VALUE);
                    Message message = new Message();
                    message.setMessage_id(messageId);
                    message.setUser_id(userId);
                    message.setAmount(amount);
                    //dao
                    int result = 0;
                    result = messageMapper.addMessage(message);

                    if(result==1){
                        return messageId;
                    }else{
                        System.out.println("update local message table failure");
                        return null;
                    }
                }
                //2. 插入本地消息表
                return null;
            }
        });

        if(messageId!=null && messageId.trim().length()>1){
            // 构建待确认消息给MQ
            Message message = new Message();
            message.setMessage_id(messageId);
            message.setUser_id(userId);
            message.setAmount(amount);
            message.setState("unconfirm");
            // 消息发送模板发送这个消息
            rabbitmqSender.sendMessage("exchange.message", "teddy.message.routeKey", message);
        }
    }

    @Override
    public void updateMessage(String param) {
        JSONObject jsonObject= JSONObject.parseObject(param);
        String respCode=jsonObject.getString("respCode");
        String messageId=jsonObject.getString("messageId");
        if(SUCCESS.equals(respCode)){
            Message message=new Message();
            message.setState("confirm");
            message.setMessage_id(messageId);
            messageMapper.updateMessage(message);
        }
    }
}
 
  

这里通过Spring的TransactionTemplate引入了编程式事务, 因为本地操作要成为一个事务, 远程的操作不可能和本地一个事务, 但是所有操作需要在一个方法里, 所以引入了编程式的事务。

实体

package com.teddy.alipayserver.bean;

import java.util.Date;

public class Account {
    private String userId;
    private Integer amount;
    private Date updateTime;

    public Date getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }

    public Integer getAmount() {
        return amount;
    }

    public void setAmount(Integer amount) {
        this.amount = amount;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}
package com.teddy.alipayserver.bean;

import java.io.Serializable;

public class Message implements Serializable{

    private static final long serialVersionUID=1L;

    private String message_id;
    private String user_id;
    private Integer amount;
    private String state;

    public String getMessage_id() {
        return message_id;
    }

    public void setMessage_id(String message_id) {
        this.message_id = message_id;
    }

    public String getUser_id() {
        return user_id;
    }

    public void setUser_id(String user_id) {
        this.user_id = user_id;
    }

    public Integer getAmount() {
        return amount;
    }

    public void setAmount(Integer amount) {
        this.amount = amount;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

RabbitMQ的配置文件

package com.teddy.alipayserver.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {

    @Bean(name="message")
    public Queue queueMessage(){
        return new Queue("teddy.message");
    }

    @Bean
    public TopicExchange exchange(){
        return new TopicExchange("exchange.message");
    }

    @Bean
    Binding bindingExchangeMessage(@Qualifier("message") Queue queueMessage, TopicExchange exchange){
        return BindingBuilder.bind(queueMessage).to(exchange).with("teddy.message.routeKey");
    }
}

RabbitMQ的服务

package com.teddy.alipayserver.config;

import com.alibaba.fastjson.JSONObject;
import com.teddy.alipayserver.bean.Message;
import org.springframework.amqp.core.AmqpTemplate;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class RabbitmqSender {
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMessage(String exchange, String routeKey, Message content){
        String message= JSONObject.toJSONString(content);
        System.out.println("send message to MQ, waiting for alipay consuming:"+message);
        amqpTemplate.convertAndSend(exchange, routeKey, message);
    }
}

Controller

package com.teddy.alipayserver.controller;

import com.teddy.alipayserver.service.AlipayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AlipayController {
    @Autowired
    AlipayService alipayService;

    @RequestMapping("/transfer")
    @ResponseBody
    public String transferAmount(String userId, int amount){
        try {
            alipayService.updateAmount(amount, userId);
        } catch (Exception e) {
            e.printStackTrace();
            return "fail";
        }
        return"OK";
    }
}

配置文件

spring:
  datasource:
    druid:
      url: jdbc:mysql://192.168.25.132:3306/rabbit_taobao_provider?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: 123456
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.jdbc.Driver
  application:
    name: aplipay-server
  rabbitmq:
    host: 192.168.25.137
    port: 5672
    username: rabbit
    password: 123456
server:
  port: 8090

定时器, 用来重发消息

package com.teddy.alipayserver.timer;

import com.alibaba.fastjson.JSONObject;
import com.teddy.alipayserver.bean.Message;
import com.teddy.alipayserver.config.RabbitmqSender;
import com.teddy.alipayserver.dao.MessageMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@Component
public class ScheduledService {
    private static SimpleDateFormat sdf=new SimpleDateFormat("YYYY-MM-dd");
    @Autowired
    MessageMapper messageMapper;
    @Autowired
    RabbitmqSender rabbitmqSender;

    @Scheduled(cron="0/60 0/1 * * * ?")
    public void scheduledProcess(){
        System.out.println("============>>>>>>>>>use cron "+sdf.format(new Date())+" start scan ......");
        List unconfirmMessages=messageMapper.queryMessageByState("unconfirm");
        if(unconfirmMessages!=null&&unconfirmMessages.size()>0){
            System.out.println("query unconfirmed message:"+JSONObject.toJSONString(unconfirmMessages));
            for(Message message:unconfirmMessages){
                System.out.println("============timer send unconfirm message to mq"+ JSONObject.toJSONString(message));
                rabbitmqSender.sendMessage("exchange.message", "teddy.message.routeKey", message);
            }
        }
    }
}

监听器, 用来监听响应队列中的消息

package com.teddy.alipayserver.listener;

import com.teddy.alipayserver.service.AlipayService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class listener {
    @Autowired
    AlipayService alipayService;

    @RabbitListener(queues="teddy.message.response")
    public void process(final String result){
        System.out.println("=====================receive balance transaction successul response message========"+result);
        alipayService.updateMessage(result);
    }
}

启动文件

package com.teddy.alipayserver;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * http://localhost:8090/transfer?userId=SAM0000001&amount=3000
 */
@SpringBootApplication(scanBasePackages = "com.teddy.alipayserver")
@MapperScan(basePackages = {"com.teddy.alipayserver.dao"})
@EnableScheduling
public class AlipayServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(AlipayServerApplication.class, args);
   }

}

下面是余额宝的工程, pom.xml和支付宝是一样的

dao

package com.teddy.balanceserver.dao;


import com.teddy.balanceserver.bean.Account;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface AccountMapper {
    @Update("update tb_account set amount=amount-#{amount}, update_time=now() where user_id=#{userId}")
    int updateAccount(@Param("amount")int amount, @Param("userId") String userId);
    @Insert("insert tb_account(user_id, amount, update_time) values(#{userId}, #{amount}, now())")
    int addAccount(Account account);
}
package com.teddy.balanceserver.dao;


import com.teddy.balanceserver.bean.Message;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface MessageMapper {
    @Update("update tb_message set state=#{state} where message_id=#{message_id}")
    int updateMessage(Message message);

    @Insert("insert into tb_message(user_id, message_id, amount, state, update_time) values (#{userId}, #{messageId}, #{amount}, 'confirm', now())")
    int addMessage(@Param("userId")String userId, @Param("messageId")String messageId, @Param("amount")int amount);

    @Select("select * from tb_message where state=#{state}")
    List queryMessageByState(@Param("state") String state);

    @Select("select * from tb_message where message_id=#{messageId}")
    List queryMessaegCountByMessageId(@Param("messageId") String messageId);
}

service

package com.teddy.balanceserver.service;

import com.teddy.balanceserver.bean.Account;

public interface BalanceService {
    public int queryMessaegCountByMessageId(String messageId);

    public void updateAmount(int amount, String userId);

    public void addMessage(String userId, String messageId, int amount);
}

service的实现

package com.teddy.balanceserver.service.impl;

import com.teddy.balanceserver.bean.Account;
import com.teddy.balanceserver.bean.Message;
import com.teddy.balanceserver.dao.AccountMapper;
import com.teddy.balanceserver.dao.MessageMapper;
import com.teddy.balanceserver.service.BalanceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BalanceServiceImpl implements BalanceService{

    @Autowired
    AccountMapper accountMapper;
    @Autowired
    MessageMapper messageMapper;

    @Override
    public int queryMessaegCountByMessageId(String messageId) {
        List messages = messageMapper.queryMessaegCountByMessageId(messageId);
        return messages.size();
    }

    @Override
    public void updateAmount(int amount, String userId) {
        accountMapper.updateAccount(amount, userId);
    }

    @Override
    public void addMessage(String userId, String messageId, int amount) {
        messageMapper.addMessage(userId, messageId, amount);
    }
}

队列的配置

package com.teddy.balanceserver.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {

    @Bean(name="message")
    public Queue queueMessage(){
        return new Queue("teddy.message.response");
    }

    @Bean
    public TopicExchange exchange(){
        return new TopicExchange("exchange.message.response");
    }

    @Bean
    Binding bindingExchangeMessage(@Qualifier("message") Queue queueMessage, TopicExchange exchange){
        return BindingBuilder.bind(queueMessage).to(exchange).with("teddy.message.routeKey.response");
    }
}

发送服务

package com.teddy.balanceserver.config;

import com.alibaba.fastjson.JSONObject;
import com.teddy.balanceserver.bean.Message;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class RabbitmqSender {
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMessage(String exchange, String routeKey, String content){
        System.out.println("send message to MQ, waiting for alipay consuming:"+content);
        amqpTemplate.convertAndSend(exchange, routeKey, content);
    }
}

监听器

package com.teddy.balanceserver.listener;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.teddy.balanceserver.bean.Message;
import com.teddy.balanceserver.config.RabbitmqSender;
import com.teddy.balanceserver.service.BalanceService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;

@Component
public class ReceiveListener {

    private static String SUCCESS="OK";

    @Autowired
    BalanceService balanceService;
    @Autowired
    TransactionTemplate transactionTemplate;
    @Autowired
    RabbitmqSender rabbitmqSender;

    @RabbitListener(queues="teddy.message")
    public void process(String jsonStr){
        final Message message=JSONObject.parseObject(jsonStr, Message.class);
        System.out.println("========balance start to consume MQ's message, message is: "+jsonStr);

        boolean isSuccess = (Boolean) transactionTemplate.execute(new TransactionCallback() {
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                //1. 修改本地余额宝的账户余额
                //2. 修改本地余额成功消息插入本地消息对账表
                //编程式事务
                String message_id=message.getMessage_id();
                int count=balanceService.queryMessaegCountByMessageId(message_id);
                // count==0代表余额宝没有处理过该流水号的记录
                if(count==0){
                    balanceService.updateAmount(message.getAmount(), message.getUser_id());
                    balanceService.addMessage(message.getUser_id(), message_id, message.getAmount());
                    return true;
                }else{
                    System.out.println("this message id:"+message_id+"already consumed. transaction stopped");
                    return false;
                }
            }
        });
        //3. 修改本地余额成功消息通过MQ的应答队列发送给支付宝进行确认
        if(isSuccess){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("messageId", message.getMessage_id());
            jsonObject.put("respCode", SUCCESS);
            System.out.println("===========send transaction successful response message to alipay======"+jsonObject.toJSONString());
            rabbitmqSender.sendMessage("exchange.message.response", "teddy.message.routeKey.response", jsonObject.toJSONString());
        }else{

        }
    }
}
 
  

配置文件

spring:
  datasource:
    druid:
      url: jdbc:mysql://192.168.25.132:3306/rabbit_taobao_consumer?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: 123456
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.jdbc.Driver
  application:
    name: balance-server
  rabbitmq:
    host: 192.168.25.137
    port: 5672
    username: rabbit
    password: 123456
server:
  port: 8080

启动类

package com.teddy.balanceserver;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(scanBasePackages = "com.teddy.balanceserver")
@MapperScan(basePackages = {"com.teddy.balanceserver.dao"})
@EnableScheduling
public class BalanceServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(BalanceServerApplication.class, args);
   }

}

 

你可能感兴趣的:(中间件)