SpringCloud商城day12 分布式解决方案-2021-10-24

一. 本地事务和分布式事务

1. ACID: 操作执行原子性(atomicity), 数据库状态一致性(consistency),多线程并发隔离性(isolation),durability(修改数据持久性)

2.  @Transaction: 只在单体服务中有效, 即本地事务

3. 分布式事务: 事务参与者, 服务器,事务管理器在不同节点的不同应用-> 数据一致性

        (1) 早期单体服务: 不涉及服务间的调用, 而是服务内操作到访问多个数据库资源

        (2) 一个服务调用另外一个服务-> 跨服务的事务策略

        (3) 跨服务事务中, 事务的发起者和提交是同一个(整个调用客户端=客户端最先调用的服务

二. CAP和BASE理论

1. CAP: 记忆 漫威哪里有事(事务), 哪里就有cap(美队)

        (1) 一致性(consistency): 主从备份-> 机器越多时间越长-> 一致性效率越低

        (2) 可用性(availability): 服务器的备用机-> 多台-> 高可用

        (3) 分区容错性(partition tolerance): 分布式-> 多个子网(区)-> 区间通信可能失败-> 必然存在-> 一直成立

        (4) 单体服务中可以保证CA: 一致性和高可用

2. BASE理论: ebay理论, 权衡CA

        (1) basically available基本可用: 响应时间损失(时间延长)/功能损失 (页面降级)

        (2) soft state软状态: 多个节点数据存在数据延迟

        (3) eventually consistent最终一致性: 软状态最终达到数据一致

三. 分布式解决方案

1. 数据库XA协议-> 2PC两阶段提交2 phase commit: 

(1) AP应用程序application program: 自己编写的代码-> 事务边界(开始和结束)

(2) RM资源管理器resource manager: 计算机的共享资源(数据库, 文件系统,打印服务器)

(3) TM事务管理器transaction manager: 分配id, 监控进度, 事务提交, 回滚, 恢复

2. 二阶段协议: 

        (1) 第一阶段: TM询问RM提交分支-> RM判断能否提交-> 可以则持久化返回OK-> 不行则返回NO-> RM回复NO且完成工作-> 丢弃事务分支

        (2) 第二阶段: TM根据结果-> 决定提交或回滚-> RM全prepare成功则提交-> RM全回执NO则回滚

        (3) 优点: 数据强一致性;           缺点: 实现复杂, 牺牲可用性, 不适合高并发

3. TCC补偿机制: 每个操作-> 注册确认和撤销操作

        (1) Try: 业务系统检测和资源预留(转账前冻结钱)

        (2) Confirm: 确认提交(转账成功则解冻, 失败则冻结接口)

        (3) cancel: 执行出错(解冻方法)

        (4) 优点: 可用性强-> 实现最终一致性   缺点: 每个微服务对三个阶段大量代码

4. 消息队列分布式事务-> 最终一致性

        (1) 拆分成本地事务-> 通过队列不断发送消息-> 直到成功

        (2) 优点: 最终一致性, 实现相对简单;     缺点: 消息表会耦合到业务系统-> 很多杂活

四. Seata: 原名fescar

1. 基于2PC, 解决XA缺点:

(1)单点问题: TM协调者在二阶段的提交中至关重要, 
    TM出现故障会导致其他参与者处于事务资源的锁定状态;
(2)同步阻塞: Fescar的一阶段本地事务就已经异步提交释放资源了, 
    不会向XA一样在prepare和commit阶段锁住资源; 
(3)数据不一致: commit时数据库的数据已经一致
(4)AT和MT: 混合模式
(5)Fescar代码零侵入

2. 原理: 比2PC多了一个TC(transaction coordinator)事务协调器

分布式事务(全局事务)-> 本地事务分支
(1) TC: 维护协调全局事务运行
(2) TM: 开启全局事务, 发起全局提交/回滚的决议
(3) RM: 事务分支的注册状态汇报

(4) 流程:
TM向TC申请开启全局事务-> 生成唯一XID-> XID在微服务间传播
-> RM向TC注册分支(本地事务)-> 纳入XID对应事务-> TM向TC发起决议

3. 工作模式: 

(1) AT模式: automatic transaction
    @GlobalTransactional 业务无侵入
    第一阶段: 解析SQL-> 执行SQL-> 保存到undo log表-> RM注册到TM-> 提交-> 本地事务提交
    第二阶段: 如果全局提交-> 分支事务已完成提交-> 无需同步-> 异步清理undo log
             如果全局回滚-> RM收到TC的回滚请求-> 通过XID和分支ID找到undo log-> 反向更新SQL
    
(2) MT模式: manual transaction 不支持XA协议
    MT分支: prepare/commit/rollback
    RM注册TC-> 执行prepare-> RM检查资源是否可用-> 分支向TC汇报状态-> TC决定commit/rollback-> 通知RM

(3) 混合AT&MT模式
    

4. 代码实现:

(1) 导入模块changgou_common_fescar:

(1)模块导入: changgou_common_fescar

(2)fescar/config/FescarAutoConfiguration.java
三个bean:
    dataSource: 数据库代理类创建, 读取配置文件,获取数据库信息;
    globalTransactionScanner: 扫描@GlobalTransactional, 基于AOP控制事务
    addFescarInterceptor: TM向TC申请XID, 每调用下一个微服务, 获取请求中的XID

(3)过滤器: fescar/filter/FescarRMRequestFilter.java
一个方法:
    doFilterInternal: 给每个线程请求绑定XID

(4)拦截器:fescar/interceptor/FescarRestInterceptor.java
FescarRequestInterceptor implements RequestInterceptor: 实现请求拦截器(feignInterceptor), 判断请求头是否有XID, 有则传递到下一个微服务

 (2) 日志表: undo log表 -> brand_id 和xid

 (3) 启动fescar服务: fescar-server

 (4) 给微服务配置fescar分布式事务

(1) 导入依赖: changgou_common_fescar
(2) 在方法中添加: @GlobalTransactional(name="order_ad") //给不同的全局事务起名字

五. RabbitMQ实现分布式事务

1. 生成订单-> 任务表中添加数据-> 定时任务扫描任务表-> 获取最新订单信息-> 发送到MQ-> 用户服务接收消息-> 判断在redis是否存在-> 否则判断在DB中是否存在-> 否则存入redis-> 设置生存期防止本地事务回滚redis数据一直存在-> 修改用户积分-> 增加积分表日志-> 删除redis的任务信息-> 通知订单

2. 表:

(1)需要的表: 
order_task表用来记录订单添加积分任务(用户名, 订单号, 积分); 
order_task_history以后可用来统计一些订单数, 积分数据 //创建时间/删除时间/错误信息/交换机

user_point_log表: 记录增加积分日志

3. 实现代码:

   (1) Orderf服务中的代码实现:

(1) order/config/RabbitMQConfig.java
绑定增加积分和完成增加积分的消息队列到同一个交换机, 定义路由键routingkey

(2) order/service/impl/OrderServiceImpl.java //添加任务数据
 //在订单数据库中的任务表tb_task添加任务数据
        Task task = new Task();
        task.setCreateTime(new Date());
        task.setUpdateTime(new Date());
        task.setMqExchange(RabbitMQConfig.EX_BUYING_ADDPOINTUSER);
        task.setMqRoutingkey(RabbitMQConfig.CG_BUYING_ADDPOINT_KEY);

        Map map = new HashMap();
        map.put("username", order.getUsername());
        map.put("orderId", orderId);
        map.put("point", order.getPayMoney());
        task.setRequestBody(JSON.toJSONString(map));

        taskMapper.insertSelective(task); //转换成jason

(3) order/dao/TaskMapper.java
public interface TaskMapper extends Mapper<Task> {
    //定义查询方法, 获取小于当前时间的最新数据
    @Select("select * from tb_task where update_time<#{currentTime}")
    //tb_task表中列名 和 task实体类的属性不对应, 需要映射 - 传递映射关系
    @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> findTaskLessThanCurrentTime(Date currentTime);
}

(4) 开启定时任务: OrderApplication.java 添加 @EnableScheduling
(5)order/task/QueryPointTask.java
@Component
public class QueryPointTask {
    @Autowired
    private TaskMapper taskMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    //查询任务表的最新数据
    @Scheduled(cron = "0/2 * * * * ?")
    public void queryTask() {
        //1.获取小于系统当前时间的数据
        List<Task> taskList = taskMapper.findTaskLessThanCurrentTime(new Date());
        if (taskList != null&&taskList.size()>0){
            //2. 发送任务到消息队列
            for (Task task : taskList) {
rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER,
RabbitMQConfig.CG_BUYING_ADDPOINT_KEY, JSON.toJSONString(task));
                System.out.println("订单服务向添加积分队列发送了一条消息");
            }
        }
    }
}

    (2) User用户微服务

(1)user/listener/AddPointListener.java
@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){
            return;
        }
        //更新用户积分
        int result = userService.updateUserPoint(task);
        if (result <=0){
            return;
        }
        //返回通知消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.EX_BUYING_ADDPOINTUSER, RabbitMQConfig.CG_BUYING_FINISHADDPOINT_KEY, JSON.toJSONString(task));

    }
}


(2)user/service/impl/UserServiceImpl.java
    /**
     * 更新用户积分
     * @param task
     * @return
     */
    @Override
    @Transactional
    public int updateUserPoint(Task task) {
        System.out.println("用户服务开始对任务进行处理");
        //1. 从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");
        //2. 判断当前任务是否操作过
        PointLog pointLog = pointLogMapper.findPointLogByOrderId(orderId);
        if (pointLog != null){
            return 0;
        }
        //3. 将任务存入redis中
redisTemplate.boundValueOps(task.getId()).set("exist",30,TimeUnit.SECONDS);
        //4. 修改用户积分
        int result = userMapper.updateUserPoint(username,point);
        if (result<=0){
            return 0;
        }
        //5. 记录积分日志信息
        pointLog = new PointLog();
        pointLog.setUserId(username);
        pointLog.setOrderId(orderId);
        pointLog.setPoint(point);
        result = pointLogMapper.insertSelective(pointLog);
        if (result <= 0){
            return 0;
        }
        //6. 删除redis中的任务信息
        redisTemplate.delete(task.getId());
        System.out.println("用户服务完成了更改用户积分的操作");
        return 1;
    }

(3) user/dao/PointLogMapper.java
public interface PointLogMapper extends Mapper<PointLog> {
    @Select("select * from tb_point_log where order_id=#{orderId}")
    PointLog findPointLogByOrderId(@Param("orderId")String orderId);
}

(4)user/dao/UserMapper.java
public interface UserMapper extends Mapper<User> {
    /**
     *增加用户积分
     */
    @Update("UPDATE tb_user SET points=points+#{point} WHERE username=#{username}")
    int addUserPoints(@Param("username")String username, @Param("point")Integer point);
    //修改用户积分
    @Update("update tb_user set points=points+#{point} where username=#{username}")
    int updateUserPoint(@Param("username")String username, @Param("point")int point);
}


(4) order/task/DelTaskListener.java
@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);
    }
}

(5) order/service/impl/TaskServiceImpl.java
@Service
public class TaskServiceImpl implements TaskService {
    @Autowired
    private TaskHisMapper taskHisMapper;
    @Autowired
    private TaskMapper taskMapper;
    @Override
    @Transactional
    public void delTask(Task task) {
        //1.记录删除的时间
        task.setDeleteTime(new Date());
        Long taskId = task.getId();
        task.setId(null);
        //2. bean拷贝 tb_task表 和 tb_task_his的属性一样
        TaskHis taskHis = new TaskHis();
        BeanUtils.copyProperties(task, taskHis);
        //3. 记录历史任务数据
        taskHisMapper.insertSelective(taskHis);
        //4.删除原有任务数据
        task.setId(taskId);
        taskMapper.deleteByPrimaryKey(task);
        System.out.println("当前的订单服务完成了添加历史任务并删除任务为的操作");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值