问题描述:扣减库存与保存订单是在两个服务中存在的,如果扣减库存后订单保存失败了是不会回滚的,这样就会造成数据不一致的情况,这其实就是我们所说的分布式事务的问题,接下来我们来学习分布式事务的解决方案。
spring cloud alibaba 架构图 | ProcessOn免费在线作图,在线流程图,在线思维导图 |
1.分布式事务解决方案
1.1 本地事务与分布式事务
1.1.1 事务
数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
事务拥有以下四个特性,习惯上被称为ACID特性:
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原子性)。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个操作在被数据库所执行一样。
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。
1.1.2 本地事务
起初,事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
1.1.3 分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
1.2 分布式事务相关理论
1.2.1 CAP定理
C一致性:写操作后读操作返回值,A可用性:收到用户请求,服务器返回消息,
P分区容错性:分区通信失败认为是总是成立的.
1.2.2 BASE理论
1.3 分布式事务解决方案
1.3.1 基于XA协议的两阶段提交 2PC
首先我们来简要看下分布式事务处理的XA规范 :
二阶段协议:
第一阶段TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作内容进行持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经完成的工作后,就可以丢弃这个事务分支信息了。
第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己的事务分支。
也就是TM与RM之间是通过两阶段提交协议进行交互的.
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
1.3.2 TCC补偿机制
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
-
Try 阶段主要是对业务系统做检测及资源预留
-
Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
-
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
1.3.3 消息最终一致性
消息最终一致性应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
2. 基于Seata实现分布式事务
2.1 Seata简介
2.2 实现原理
2.3 Fescar模式
2.3.1 AT模式
业务逻辑不需要关注事务机制,分支与全局事务的交互过程自动进行。
AT模式:主要关注多 DB 访问的数据一致性,实现起来比较简单,对业务的侵入较小。
AT模式部分代码如下:不需要关注执行状态,对业务代码侵入较小。类似代码如下,只需要为方法添加@GlobalTransactional
注解即可。
2.3.2 MT模式
2.3.3 混合模式
因为 AT 和 MT 模式的分支从根本上行为模式是一致的,所以可以完全兼容,即,一个全局事务中,可以同时存在 AT 和 MT 的分支。这样就可以达到全面覆盖业务场景的目的:AT 模式可以支持的,使用 AT 模式;AT 模式暂时支持不了的,用 MT 模式来替代。另外,自然的,MT 模式管理的非事务性资源也可以和支持事务的关系型数据库资源一起,纳入同一个分布式事务的管理中。
2.3.4 补充
https://seata.io/zh-cn/docs/overview/what-is-seata.html
(1) AT模式
(2) 写隔离
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
写隔离:其实就是谁先注册谁就获得锁,然后做事务,提交了才会释放锁,如果注册发现有锁了,就只能不断的尝试,直到能注册位置,这样就进行了写隔离,同一时刻就只能有一个事务对同一个数据进行修改。
(3) 读隔离
读隔离:默认的是读未提交,如果要进行读已提交的话也可以用全局锁:
在sql
中有SELECT FOR UPDATE
语句,执行器就是SelectForUpdateExecutor
,可以看到查询的时候会检查全局锁,不能获得就会重试。 其实就一个简单的例子,比如你想查询一个数据,但是你没在任何全局事务中,但是怕有其他全局事务在修改,你查出来的数据有问题,那怎么办呢,你就可以在查询的方法上用@GlobalLock
,其实就是一个读隔离。
(4) TCC模式
(5) Saga 模式
(6) AT 分支的工作过程,工作机制
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式
https://blog.csdn.net/weixin_44792186/article/details/122898611
二阶段提交协议2PC
https://www.cnblogs.com/balfish/p/8658691.html
https://blog.51cto.com/u_15082402/2644336
2.4 代码实现
2.4.1 分布式事务公共模块
(1)创建工程 changgou_common_fescar,引入依赖
<properties>
<fescar.version>0.4.2</fescar.version>
</properties>
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-tm</artifactId>
<version>${fescar.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-spring</artifactId>
<version>${fescar.version}</version>
</dependency>
</dependencies>
(3)创建FescarRMRequestFilter,给每个线程绑定一个XID
public class FescarRMRequestFilter extends OncePerRequestFilter {
private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger( FescarRMRequestFilter.class);
/**
* 给每次线程请求绑定一个XID
* @param request
* @param response
* @param filterChain
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String currentXID = request.getHeader( FescarAutoConfiguration.FESCAR_XID);
if(!StringUtils.isEmpty(currentXID)){
RootContext.bind(currentXID);
LOGGER.info("当前线程绑定的XID :" + currentXID);
}
try{
filterChain.doFilter(request, response);
} finally {
String unbindXID = RootContext.unbind();
if(unbindXID != null){
LOGGER.info("当前线程从指定XID中解绑 XID :" + unbindXID);
if(!currentXID.equals(unbindXID)){
LOGGER.info("当前线程的XID发生变更");
}
}
if(currentXID != null){
LOGGER.info("当前线程的XID发生变更");
}
}
}
}
(4)创建FescarRestInterceptor过滤器,每次请求其他微服务的时候,都将XID携带过去。(资源提供)
public class FescarRestInterceptor implements RequestInterceptor, ClientHttpRequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String xid = RootContext.getXID();
if(!StringUtils.isEmpty(xid)){
requestTemplate.header( FescarAutoConfiguration.FESCAR_XID, xid);
}
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String xid = RootContext.getXID();
if(!StringUtils.isEmpty(xid)){
HttpHeaders headers = request.getHeaders();
headers.put( FescarAutoConfiguration.FESCAR_XID, Collections.singletonList(xid));
}
return execution.execute(request, body);
}
}
(5)创建FescarAutoConfiguration类 (资源提供)
/**
* * 创建数据源
* * 定义全局事务管理器扫描对象
* * 给所有RestTemplate添加头信息防止微服务之间调用问题
*/
@Configuration
public class FescarAutoConfiguration {
public static final String FESCAR_XID = "fescarXID";
/***
* 创建代理数据库
* @param environment
* @return
*/
@Bean
public DataSource dataSource(Environment environment){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(environment.getProperty("spring.datasource.url"));
try {
dataSource.setDriver(DriverManager.getDriver(environment.getProperty("spring.datasource.url")));
} catch (SQLException e) {
throw new RuntimeException("can't recognize dataSource Driver");
}
dataSource.setUsername(environment.getProperty("spring.datasource.username"));
dataSource.setPassword(environment.getProperty("spring.datasource.password"));
return new DataSourceProxy(dataSource);
}
/***
* 全局事务扫描器
* 用来解析带有@GlobalTransactional注解的方法,然后采用AOP的机制控制事务
* @param environment
* @return
*/
@Bean
public GlobalTransactionScanner globalTransactionScanner(Environment environment){
String applicationName = environment.getProperty("spring.application.name");
String groupName = environment.getProperty("fescar.group.name");
if(applicationName == null){
return new GlobalTransactionScanner(groupName == null ? "my_test_tx_group" : groupName);
}else{
return new GlobalTransactionScanner(applicationName, groupName == null ? "my_test_tx_group" : groupName);
}
}
/***
* 每次微服务和微服务之间相互调用
* 要想控制全局事务,每次TM都会请求TC生成一个XID,每次执行下一个事务,也就是调用其他微服务的时候都需要将该XID传递过去
* 所以我们可以每次请求的时候,都获取头中的XID,并将XID传递到下一个微服务
* @param restTemplates
* @return
*/
@ConditionalOnBean({RestTemplate.class})
@Bean
public Object addFescarInterceptor(Collection<RestTemplate> restTemplates){
restTemplates.stream()
.forEach(restTemplate -> {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if(interceptors != null){
interceptors.add(fescarRestInterceptor());
}
});
return new Object();
}
@Bean
public FescarRMRequestFilter fescarRMRequestFilter(){
return new FescarRMRequestFilter();
}
@Bean
public FescarRestInterceptor fescarRestInterceptor(){
return new FescarRestInterceptor();
}
}
2.4.2 分布式事务的实现
(1)涉及到分布式事务的数据库添加表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_unionkey` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8;
核心在于对业务sql进行解析,转换成undolog,所以只要支持Fescar分布式事务的微服务数据都需要导入该表结构
(2)需要添加分布式事务的微服务(商品微服务、订单微服务)添加对 changgou_transaction_fescar的依赖
<!--fescar依赖-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common_fescar</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(3)在订单微服务的OrderServiceImpl的add方法上增加@GlobalTransactional(name = "order_add")注解
/**
* 增加 Order
*
* @param order
* @return
*/
@Override
@GlobalTransactional(name = "order_add")
public Order add(Order order) {
//为了确保线程安全,加了synchronized关键字
// 主键 ID
order.setId(String.valueOf(idWorker.nextId()));
// 获取订单明细->购物车集合,循环订单明细,每个商品的购买数量叠加
List<OrderItem> orderItems = new ArrayList<>();
// 获取勾选的商品 ID,需要下单的商品,将要下单的商品的ID信息从购物车中移除
//这个是在页面选中的商品的id的集合。因为下单的时候,可以选择购物车中的部分商品,并不是所有的商品,用这个就可以判断哪些是选中的。
for (Long skuId : order.getSkuIds()) {
orderItems.add((OrderItem) redisTemplate.boundHashOps("Cart_" + order.getUsername()).get(skuId));
redisTemplate.boundHashOps("Cart_" + order.getUsername()).delete(skuId);
}
int totalNum = 0;
int totalMoney = 0;
// 封装Map<Long, Integer> 封装递减数据
Map<String, Integer> decrMap = new HashMap<>();
for (OrderItem orderItem : orderItems) {
totalNum += orderItem.getNum();
totalMoney += orderItem.getMoney();
// 订单明细的 Id
orderItem.setId(String.valueOf(idWorker.nextId()));
// 订单明细所属的订单
orderItem.setOrderId(order.getId());
// 是否退货
orderItem.setIsReturn("0");
// 封装递减数据
decrMap.put(orderItem.getSkuId().toString(), orderItem.getNum());
}
// 订单购买商品总数量 = 每个商品总数量之和
order.setTotalNum(totalNum);
// 订单总金额
order.setTotalMoney(totalMoney);
// 实付金额
order.setPayMoney(totalMoney);
// 创建时间
order.setCreateTime(new Date());
// 修改时间
order.setUpdateTime(order.getCreateTime());
// 订单来源,1:web
order.setSourceType("1");
// 是否完成订单,0:未完成
order.setOrderStatus("0");
// 是否支付,0:未支付
order.setPayStatus("0");
// 是否删除, 0:未删除
order.setIsDelete("0");
// 添加订单信息
orderMapper.insertSelective(order);
// 循环添加订单明细信息
for (OrderItem orderItem : orderItems) {
orderItemMapper.insertSelective(orderItem);
}
// 库存递减
skuFeign.decrCount(decrMap);
// 添加用户积分活跃度
userFeign.addPoints(1);
//线上支付,记录订单,存到redis中,避免每次访问数据库
if (order.getPayType().equalsIgnoreCase("1")) {
//将支付记录存入到Reids namespace key value
redisTemplate.boundHashOps("Order").put(order.getId(), order);
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("创建订单时间:" + simpleDateFormat.format(new Date()));
// 添加订单
rabbitTemplate.convertAndSend("orderDelayQueue", (Object) order.getId().toString(), new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置延时读取
message.getMessageProperties().setExpiration("10000");
return message;
}
});
return order;
}
(4)启动Fescar-server,打开seata包/fescar-server-0.4.2/bin,双击fescar-server.bat启动fescar-server,如下:
Releases · seata/seata · GitHub
2.4.3 测试
(1)功能测试,看功能能否正常执行。
(2)异常测试,我们在方法中添加int x=1/0
,看库存信息是否能够回滚。
3.基于消息队列实现分布式事务
3.1 准备工作
3.1.1 changgou_order库新增数据表
DROP TABLE IF EXISTS `tb_task`;
CREATE TABLE `tb_task` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
`task_type` varchar(32) DEFAULT NULL COMMENT '任务类型',
`mq_exchange` varchar(64) DEFAULT NULL COMMENT '交换机名称',
`mq_routingkey` varchar(64) DEFAULT NULL COMMENT 'routingkey',
`request_body` varchar(512) DEFAULT NULL COMMENT '任务请求的内容',
`status` varchar(32) DEFAULT NULL COMMENT '任务状态',
`errormsg` varchar(512) DEFAULT NULL COMMENT '任务错误信息',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tb_task_his`;
CREATE TABLE `tb_task_his` (
`id` bigint(32) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
`task_type` varchar(32) DEFAULT NULL COMMENT '任务类型',
`mq_exchange` varchar(64) DEFAULT NULL COMMENT '交换机名称',
`mq_routingkey` varchar(64) DEFAULT NULL COMMENT 'routingkey',
`request_body` varchar(512) DEFAULT NULL COMMENT '任务请求的内容',
`status` varchar(32) DEFAULT NULL COMMENT '任务状态',
`errormsg` varchar(512) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
3.1.2 changgou_service_order_api添加相关实体类
@Table(name="tb_task")
public class Task {
@Id
private Long id;
@Column(name = "create_time")
private Date createTime;
@Column(name = "update_time")
private Date updateTime;
@Column(name = "delete_time")
private Date deleteTime;
@Column(name = "task_type")
private String taskType;
@Column(name = "mq_exchange")
private String mqExchange;
@Column(name = "mq_routingkey")
private String mqRoutingkey;
@Column(name = "request_body")
private String requestBody;
@Column(name = "status")
private String status;
@Column(name = "errormsg")
private String errormsg;
//getter,setter略
}
@Table(name="tb_task_his")
public class TaskHis {
@Id
private Long id;
@Column(name = "create_time")
private Date createTime;
@Column(name = "update_time")
private Date updateTime;
@Column(name = "delete_time")
private Date deleteTime;
@Column(name = "task_type")
private String taskType;
@Column(name = "mq_exchange")
private String mqExchange;
@Column(name = "mq_routingkey")
private String mqRoutingkey;
@Column(name = "request_body")
private String requestBody;
@Column(name = "status")
private String status;
@Column(name = "errormsg")
private String errormsg;
//getter,setter略
}
3.1.3 changgou_user新增积分日志表
DROP TABLE IF EXISTS `tb_point_log`;
CREATE TABLE `tb_point_log` (
`order_id` varchar(200) NOT NULL,
`user_id` varchar(200) NOT NULL,
`point` int(11) NOT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3.1.4 changgou_service_user_api添加实体类 PointLog
@Table(name="tb_point_log")
public class PointLog {
private String orderId;
private String userId;
private Integer point;
//getter,setter略
}
3.1.5 changgou_service_order添加rabbitMQ配置类
@Configuration
public class RabbitMQConfig {
//添加积分任务交换机
public static final String EX_BUYING_ADDPOINTURSE = "ex_buying_addpointurse";
//添加积分消息队列
public static final String CG_BUYING_ADDPOINT = "cg_buying_addpoint";
//完成添加积分消息队列
public static final String CG_BUYING_FINISHADDPOINT = "cg_buying_finishaddpoint";
//添加积分路由key
public static final String CG_BUYING_ADDPOINT_KEY = "addpoint";
//完成添加积分路由key
public static final String CG_BUYING_FINISHADDPOINT_KEY = "finishaddpoint";
/**
* 交换机配置
* @return the exchange
*/
@Bean(EX_BUYING_ADDPOINTURSE)
public Exchange EX_DECLARE() {
return ExchangeBuilder.directExchange(EX_BUYING_ADDPOINTURSE).durable(true).build();
}
//声明队列
@Bean(CG_BUYING_FINISHADDPOINT)
public Queue QUEUE_CG_BUYING_FINISHADDPOINT() {
Queue queue = new Queue(CG_BUYING_FINISHADDPOINT);
return queue;
}
//声明队列
@Bean(CG_BUYING_ADDPOINT)
public Queue QUEUE_CG_BUYING_ADDPOINT() {
Queue queue = new Queue(CG_BUYING_ADDPOINT);
return queue;
}
/**
* 绑定队列到交换机 .
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding BINDING_QUEUE_FINISHADDPOINT(@Qualifier(CG_BUYING_FINISHADDPOINT) Queue queue, @Qualifier(EX_BUYING_ADDPOINTURSE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_FINISHADDPOINT_KEY).noargs();
}
@Bean
public Binding BINDING_QUEUE_ADDPOINT(@Qualifier(CG_BUYING_ADDPOINT) Queue queue, @Qualifier(EX_BUYING_ADDPOINTURSE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_ADDPOINT_KEY).noargs();
}
}
3.2 订单服务添加任务并发送
3.2.1 修改添加订单方法
当添加订单的时候,添加任务表中相关数据, 局部代码如下:
//增加任务表记录
Task task = new Task();
task.setCreateTime(new Date());
task.setUpdateTime(new Date());
task.setMqExchange(RabbitMQConfig.EX_BUYING_ADDPOINTURSE);
task.setMqRoutingkey(RabbitMQConfig.CG_BUYING_ADDPOINT_KEY);
Map map = new HashMap();
map.put("userName",order.getUsername());
map.put("orderId",order.getId());
map.put("point",order.getPayMoney());
task.setRequestBody(JSON.toJSONString(map));
taskMapper.insertSelective(task);
3.2.2 定时扫描任务表最新数据
订单服务新增定时任务类,获取小于系统当前时间的所有任务数据
修改订单服务启动类,添加开启定时任务注解定义定时任务类,查询最新数据
@EnableScheduling
更新taskMapper新增方法,查询所有小于系统当前时间的数据
public interface TaskMapper extends Mapper<Task> {
@Select("SELECT * from tb_task WHERE update_time<#{currentTime}")
@Results({@Result(column = "create_time",property = "createTime"),
@Result(column = "update_time",property = "updateTime"),
@Result(column = "delete_time",property = "deleteTime"),
@Result(column = "task_type",property = "taskType"),
@Result(column = "mq_exchange",property = "mqExchange"),
@Result(column = "mq_routingkey",property = "mqRoutingkey"),
@Result(column = "request_body",property = "requestBody"),
@Result(column = "status",property = "status"),
@Result(column = "errormsg",property = "errormsg")})
List<Task> findTaskLessTanCurrentTime(Date currentTime);
}
任务类实现
@Component
public class QueryPointTask {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private TaskMapper taskMapper;
@Scheduled(cron = "0 0/2 * * * ?")
public void queryTask(){
//1.获取小于系统当前时间数据
List<Task> taskList = taskMapper.findTaskLessTanCurrentTime(new Date());
if (taskList!=null && taskList.size()>0){
//将任务数据发送到消息队列
for (Task task : taskList) {
rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTURSE,RabbitMQConfig.CG_BUYING_ADDPOINT_KEY, JSON.toJSONString(task));
}
}
}
}
3.3 用户服务更改积分
3.3.1 添加rabbitmq配置类(与订单服务相同)
@Configuration
public class RabbitMQConfig {
//添加积分任务交换机
public static final String EX_BUYING_ADDPOINTURSE = "ex_buying_addpointurse";
//添加积分消息队列
public static final String CG_BUYING_ADDPOINT = "cg_buying_addpoint";
//完成添加积分消息队列
public static final String CG_BUYING_FINISHADDPOINT = "cg_buying_finishaddpoint";
//添加积分路由key
public static final String CG_BUYING_ADDPOINT_KEY = "addpoint";
//完成添加积分路由key
public static final String CG_BUYING_FINISHADDPOINT_KEY = "finishaddpoint";
/**
* 交换机配置
* @return the exchange
*/
@Bean(EX_BUYING_ADDPOINTURSE)
public Exchange EX_DECLARE() {
return ExchangeBuilder.directExchange(EX_BUYING_ADDPOINTURSE).durable(true).build();
}
//声明队列
@Bean(CG_BUYING_FINISHADDPOINT)
public Queue QUEUE_CG_BUYING_FINISHADDPOINT() {
Queue queue = new Queue(CG_BUYING_FINISHADDPOINT);
return queue;
}
//声明队列
@Bean(CG_BUYING_ADDPOINT)
public Queue QUEUE_CG_BUYING_ADDPOINT() {
Queue queue = new Queue(CG_BUYING_ADDPOINT);
return queue;
}
/**
* 绑定队列到交换机 .
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding BINDING_QUEUE_FINISHADDPOINT(@Qualifier(CG_BUYING_FINISHADDPOINT) Queue queue, @Qualifier(EX_BUYING_ADDPOINTURSE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_FINISHADDPOINT_KEY).noargs();
}
@Bean
public Binding BINDING_QUEUE_ADDPOINT(@Qualifier(CG_BUYING_ADDPOINT) Queue queue, @Qualifier(EX_BUYING_ADDPOINTURSE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_ADDPOINT_KEY).noargs();
}
}
3.3.2 定义消息监听类
@Component
public class AddPointListener {
@Autowired
private UserService userService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT)
public void receiveMessage(String message){
Task task = JSON.parseObject(message, Task.class);
if (task == null || StringUtils.isEmpty(task.getRequestBody())){
return;
}
//判断redis中是否存在内容
Object value = redisTemplate.boundValueOps(task.getId()).get();
if (value != null){
return;
}
//更新用户积分
int result = userService.updateUserPoints(task);
if (result<=0){
return;
}
//返回通知
rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTURSE,RabbitMQConfig.CG_BUYING_FINISHADDPOINT_KEY,JSON.toJSONString(task));
}
}
3.3.3 定义修改用户积分实现
实现思路:
1)判断当前订单是否操作过
2)将任务存入redis
3)修改用户积分
4)添加积分日志表记录
5)删除redis中记录
@Autowired
private PointLogMapper pointLogMapper;
/**
* 修改用户积分
* @param task
* @return
*/
@Override
@Transactional
public int updateUserPoints(Task task) {
Map info = JSON.parseObject(task.getRequestBody(), Map.class);
String userName = info.get("userName").toString();
String orderId = info.get("orderId").toString();
int point = (int) info.get("point");
//判断当前订单是否操作过
PointLog pointLog = pointLogMapper.findLogInfoByOrderId(orderId);
if (pointLog != null){
return 0;
}
//将任务存入redis
redisTemplate.boundValueOps(task.getId()).set("exist",1,TimeUnit.MINUTES);
//修改用户积分
int result = userMapper.updateUserPoint(userName, point);
if (result<=0){
return result;
}
//添加积分日志表记录
pointLog = new PointLog();
pointLog.setOrderId(orderId);
pointLog.setPoint(point);
pointLog.setUserId(userName);
result = pointLogMapper.insertSelective(pointLog);
if (result<=0){
return result;
}
//删除redis中的记录
redisTemplate.delete(task.getId());
return 1;
}
3.3.4 定义根据订单id查询积分日志表
定义PointLogMapper,实现根据订单id查询
public interface PointLogMapper extends Mapper<PointLog> {
@Select("select * from tb_point_log where order_id=#{orderId}")
PointLog findLogInfoByOrderId(@Param("orderId") String orderId);
}
3.4 订单服务删除原任务
3.4.1 定义监听类
在订单服务中定义监听类,用于监听队列,如果队列中有消息,则删除原任务防止消息重复发送,并对任务信息进行记录
@Component
public class DelTaskListener {
@Autowired
private TaskService taskService;
@RabbitListener(queues = RabbitMQConfig.CG_BUYING_FINISHADDPOINT)
public void receiveMessage(String message){
Task task = JSON.parseObject(message, Task.class);
taskService.delTask(task);
}
}
3.4.2 定义任务service
public interface TaskService {
void delTask(Task task);
}
@Service
@Transactional
public class TaskServiceImpl implements TaskService {
@Autowired
private TaskMapper taskMapper;
@Autowired
private TaskHisMapper taskHisMapper;
@Override
public void delTask(Task task) {
//1. 设置删除时间
task.setDeleteTime(new Date());
Long id = task.getId();
task.setId(null);
//bean复制
TaskHis taskHis = new TaskHis();
BeanUtils.copyProperties(task,taskHis);
//记录任务信息
taskHisMapper.insertSelective(taskHis);
//删除原任务
task.setId(id);
taskMapper.deleteByPrimaryKey(task);
}
}