原理图

分布式事务 消息队列实现Java代码 消息队列做分布式事务_spring

 

任务目标

当用户下单时,会增加与支付金额数相等的积分,在订单模块中完成下单,远程调用用户模块中的增加积分的操作,这里连个模块用rabbimq完成事务管理

环境准备

数据库中准备三张表

changgou_order中有两张,tb_task用于储存等待处理的任务,tb_task_his用于存储已经处理完的任务,日后做数据分析

这两张表用于记录和order订单有关的事务

changgou_user中有一张tb_point_log,用于记录待执行的添加积分的任务

这张表用于记录和积分有关的事务

三张表的创建sql为

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;
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;

 为三张数据表准备对应的实体类

在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略
}

 在changgou_service_user_api中添加

@Table(name="tb_point_log")
public class PointLog {
    private String orderId;
    private String userId;
    private Integer point;
    //getter,setter略
}

 添加rabbitmq配置类

在changgou_service_order和changgou_service_user两个模块中添加配置类

@Configuration
public class RabbitMQConfig {
    //添加积分任务交换机
    public static final String EX_BUYING_ADDPOINTUSER = "ex_buying_addpointuser";
    //添加积分消息队列
    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_ADDPOINTUSER)
    public Exchange EX_BUYING_ADDPOINTUSER() {
        return ExchangeBuilder.directExchange(EX_BUYING_ADDPOINTUSER).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_ADDPOINTUSER) 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_ADDPOINTUSER) Exchange exchange) {
       return BindingBuilder.bind(queue).to(exchange).with(CG_BUYING_ADDPOINT_KEY).noargs();
    }
}

  开始操作

修改新增订单方法,将增加积分作为一个任务放到任务数据表

taskMapper为

import tk.mybatis.mapper.common.Mapper;

import java.util.Date;
import java.util.List;

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);
}

 在订单模块的新增订单方法中,添加如下,将数据写入task数据表

//增加任务表记录
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);//这是MyBatis提供的内置查询方法

 之后由定时任务自动扫描数据表,取出任务然后放到消息队列,再由用户服务中的监听类获取

开启定时扫描Task表,获取待处理的任务,并放到待处理任务的消息队列

在订单模块的引导类上加注解开启定时任务

@EnableScheduling

 在订单模块下新建一个task包,新建一个类,执行定时查询,将tb_task表中的任务数据取出,放到待处理消息队列,供用户服务消费

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.xml.crypto.Data;
import java.util.Date;
import java.util.List;

@Component
public class QueryPointTask {
@Autowired
    private TaskMapper taskMapper;
@Autowired
    private RabbitTemplate rabbitTemplate;

@Scheduled(cron = "0/2 * * * * *")//每两秒执行一次,具体语法参照以前的笔记
    public void queryTask(){//每隔一段时间,执行一次查询任务表
        //获取小于系统当前时间的数据
        List<Task> taskList = taskMapper.findTaskLessTanCurrentTime(new Date());
if(taskList!= null && taskList.size()>0){
for (Task task : taskList) {
//将任务发送到消息队列
                rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER,RabbitMQConfig.CG_BUYING_ADDPOINT_KEY, JSON.toJSONString(taskList));
            }
        }
    }
}

 在用户模块中定于消息监听类,监听待处理的任务,并调用用户服务的service完成操作,并在操作成功后将任务放到已完成任务的消息队列中,供订单服务消费

package com.changgou.user.listener;

import com.alibaba.fastjson.JSON;
import com.changgou.order.pojo.Task;
import com.changgou.user.config.RabbitMQConfig;
import com.changgou.user.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class AddPointListener {
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private UserService userService;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RabbitListener(queues = RabbitMQConfig.CG_BUYING_ADDPOINT)
    public void receiveAddPointMessage(String message){
        System.out.println("用户服务接收到了任务消息");
        //转换消息
        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){
            //说明这个任务已经在redis中存在我们不需要再处理
            return;
        }
        //更新用户积分
        int result = userService.updateUserPoint(task);
        if(result == 0){
            return;//更新积分的过程中出现错误,直接返回
        }
        //向订单服务返回通知消息  将原任务放到已完成的任务队列
        rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER,RabbitMQConfig.CG_BUYING_FINISHADDPOINT_KEY,JSON.toJSONString(task));

    }
}

 用户serviceImpl里增加积分的方法

//用户提交订单后增加积分,基于rabbitmq实现分布式事务管理
    //由AddPointListener从队列中取出task后,调用本方法来执行增加积分的操作
    @Override
    @Transactional  //注意这里加了本地事务管理
    public int updateUserPoint(Task task) {
        System.out.println("用户服务开始处理增加积分任务");
        //从task中获取相关数据
        Map map = JSON.parseObject(task.getRequestBody(), Map.class);
        String username = map.get("username").toString();
        String orderId = map.get("orderId").toString();
        int point = (int)map.get("point");
        //判断当前的任务是否操作过
        //从pointLog表里通过orderId查询
        PointLog pointLog = pointLogMapper.findPointLogByOrderId(orderId);
        if(pointLog != null){
            return 0; //tb_point_log这个数据表里记录了所有的已经增加过积分的订单的id,如果查到了说明这个订单已经增加过积分了,一个订单只能增加1次订单
        }
        //将任务存到 redis中
        redisTemplate.boundValueOps(task.getId()).set("exist",30, TimeUnit.SECONDS);//值是什么无所谓,但一定要优质,因为AddPointListener中通过这个值是否为空判断是不是已经加入了目前订单的任务 ,30S后自动过期
        //修改用户积分
        int result = userMapper.addPoints(point, username);
        if(result <= 0){ //如果返回的是0,说明修改积分的操作出了异常
            return 0;
        }
        //记录积分日志信息
        pointLog = new PointLog();
        pointLog.setUserId(username);
        pointLog.setOrderId(orderId);
        pointLog.setPoint(point);
        result = pointLogMapper.insertSelective(pointLog);
        if(result <= 0){
            return 0;//已经修改用户积分,但是未能写入记录 已经增加过积分的订单 的表,返回操作失败
        }
        //删除redis中的任务信息
        redisTemplate.delete(task.getId());

        return 1;//返回1代表操作成功
    }

 订单服务中的监听类,监听已完成任务队列的消息,并调用taskSevice删除待处理任务表中的相关数据,向历史任务表中添加记录

package com.changgou.order.listener;

import com.alibaba.fastjson.JSON;
import com.changgou.order.config.RabbitMQConfig;
import com.changgou.order.pojo.Task;
import com.changgou.order.service.TaskService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class DelTaskListener {
    @Autowired
    private TaskService taskService;

    @RabbitListener(queues = RabbitMQConfig.CG_BUYING_FINISHADDPOINT)
    public void receiveDelTaskMessage(String message){
        System.out.println("订单服务接受到了完成添加积分的消息,开始删除已添加的任务");
        Task task = JSON.parseObject(message, Task.class);
        //从待处理任务表中删除任务数据,并向历史任务表中添加记录
        taskService.delTask(task);

    }
}

 taskService如下

package com.changgou.order.service.impl;

import com.changgou.order.dao.TaskHisMapper;
import com.changgou.order.dao.TaskMapper;
import com.changgou.order.pojo.Task;
import com.changgou.order.pojo.TaskHis;
import com.changgou.order.service.TaskService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

@Service
public class TaskServiceImpl implements TaskService {
    @Autowired
    private TaskMapper taskMapper;
    @Autowired
    private TaskHisMapper taskHisMapper;

    @Override
    @Transactional
    public void delTask(Task task) {
        //记录删除的时间
        task.setDeleteTime(new Date());
        Long taskId = task.getId();
        task.setId(null);

        //bean拷贝 由于task和taskHis的属性是完全相同的,所以我们可以直接进行Bean拷贝
        TaskHis taskHis = new TaskHis();
        BeanUtils.copyProperties(task,taskHis);
        //记录历史任务数据
        taskHisMapper.insertSelective(taskHis);
        //删除原有的任务数据
        task.setId(taskId);
        taskMapper.deleteByPrimaryKey(task);

        System.out.println("订单服务完成了添加历史任务并删除原有任务的操作");
    }
}

过程总结

1由订单服务向待处理任务数据表中写入待处理任务

2由定时任务自动读取待处理任务表中的数据,取出任务,放到待处理任务消息队列

3由用户模块中的监听类监听待处理的任务,调用服务类处理任务增加积分(注意其中还涉及到redis和另一张数据表来保证一个订单只会处理一次),处理完成后将任务放到已处理的消息队列

4由订单模块的监听类监听已处理的消息列表,调用服务类移除待处理数据表的记录,并向历史任务数据表中添加记录

 

整个过程中,各个服务都有自己的本地事务管理,如果执行失败,回滚自己的本地服务,并等待下一次定时任务放置新的消息到队列,再执行一次,直到成功

在增加积分的操作中引入了redis和另一张数据表tb_point_log,用于保证一个订单只会增加一次积分,redis是为了防止重复从待处理任务队列中获取消息,tb_point_log是为了防止重复增加积分

注redis中必须设置过期时间以防止无法重新获取任务