假设支付宝转账1000元到余额宝, 通过RabbitMQ对转账过程进行解耦,
支付宝将转账的消息投递到RabbitMQ, 余额宝通过监听RibbitMQ的消息队列获得消息, 然后通过应答队列告诉支付宝消息已经消费
1. 当余额宝获取到消息之后, 可能转账失败, 消息队列不会关心余额宝是否操作成功, 这就是消息丢失的问题
2. 如果余额宝成功转账, 但响应队列迟迟没有将消费成功的消息告诉支付宝, 导致支付宝重复发送消息, 这就是消息重复发送的问题.
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
这里通过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
配置文件
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);
}
}