分布式事务

分布式事务

1.CAP和Base理论

1.1 分布式事务简介

在单体项目中,事务通常采用数据库本地事务,即把若干个逻辑处理单元当成一个整体处理。它包 含ACID四大特
性。

  • A (Atomicity, 原子性):一个事务中的所有操作要不全部成功,要不全部失败,不能出现部分成功,部分失
    败的情况。
  • C(Consistency,一致性):数据库设计上这个含义比较模糊,简单可以理解为财务的对账一样,两边数据的
    加加减减必须要能保持一致。
  • I(Isolation,隔离性):主要是针对在并发访问数据时要有一定的隔离性,在MySQL中隔离性也是分等级的,
    根据不同的业务需求选择不同的隔离性,主要依靠锁+MVCC来实现,隔离性越强,数据库的吞吐就越差。
  • D(Durability,持久性):事务一旦提交,数据将会保存到数据库中,此时如果数据库发生错误,也不会造成
    数据丢失。

在传统架构中往往是一个单体架构,一个系统就对应一个war包,然后这个系统也只有一个数据库。即一个应用对
应一个数据库,此时能满足传统的数据库事务,满足ACID的强一致性。后来,由于业务需求或其他原因,此时一个应用系统操作两个数据库(虽然这个在微服务规范中是不合理的)即一个应用要操作两个资源,此时就不能用传统的事务了。此时就需要用到分布式事务,XA事务。再到后来,为了解耦或其他原因,此时一个应用系统需要拆分成两个子系统,其中一个系统对应一个库,此时也需要用到分布式事务。虽然SpringCloud Alibaba给我肯提供了Seata框架可以用来处理分布式事务,但在网站前台,企业更多的是采用消息中间件来处理分布式事务。
ACID
问题

  • 分布式服务的事务问题
    在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是 分布式事务
    分布式问题

1.2 CAP定理和Base理论

1.2.1 CAP定理

CAP理论描述了分布式系统中的基本原则,其中C是指Consistency(一致性),A是指Availability(可用性)和P是指Partition tolerance(分区容错性)。CAP原则指CAP三者不能同时满足,要么能同时满足CP即同时满足区分容错性和一致性,要么同时满足AP即同时满足分区容错性和可用性。从中可以看出,P是分布式系统的基础,没有区分容错性
就谈不上分布式系统了。 CAP只能满足AP或CP的原因是,分布式节点之间通常存在一个数据拷贝的过程,在这一个过程中是只能满足AP或者CP的。举个例子好了,比如redis分布式集群中,当一个写请求打到一个主节点上,几乎同时另一个读请求打到redis这个主节点的对应从节点上,此时请问该从节点能返回刚才写在主节点的数据吗?若要保证CP,此时数据正在从主节点复制到从节点的路上,此时该节点的该数据是不可用的;若要保证AP,因为数据正在从主节点复制到从节点的路上,因此节点间的数据状态是不一致的。 一般在分布式设计中都在围绕CAP中做取舍、平衡,例如Zookeeper选择CP,Redis的主从架构则倾向于AP,Eureka注册中心也是AP。
CAP

  • 一致性
    一致性
  • 可用性 : 用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
    可用性
  • 分区:因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区
  • 容错:在集群出现分区时,整个系统也要持续对外提供服务
    分区容错性
    总结:分布式系统节点通过网络连接,一定会出现分区问题(P) 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足
1.2.2 Base理论

前面讲到分布式系统的CAP原则要么同时满足AP要么同时满足CP。BASE 理论是对CAP 中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。。BASE是指Basically Available(基本可用 的),Soft state(软状
态),Eventual consistency(最终一致性)。

  • Basically Available是指在分布式集群节点中,若某个节点宕机,或者在数据在节点间复制的过程中,只有部分数据不可用,但不影响整个系统的整体的可用性。
  • Soft state是指软状态即这个状态只是一个中间状态,允许数据在节点集群间操作过程中存在存在一个时延,这个中间状态最终会转化为最终状态。
  • Eventual consistency是指数据在分布式集群节点间操作过程中存在时延,与ACID相反,最终一致性不是强一致性,在经过一定时间后,分布式集群节点间的数据拷贝能达到最终一致的状态。
  1. BASE理论是对CAP的一种解决思路,包含三个思想:
  • Basically Available(基本可用)∶分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
  • Soft State(软状态)∶在一定时间内,允许出现中间状态,比如临时的不一致状态。
  • Eventually Consistent(最终一致性)∶虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致
  1. 而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
  • AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致
  • CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
  1. CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状
    态。

这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务
分布式事务

2.本地消息表

2.1本地消息表简介

本地消息表的最终一致方案采用BASE原理,保证事务最终一致。在一致性方面,允许一段时间内的不一致,但最终会一致。本地消息表实现分布式事务应该是网站前台使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。 使用本地消息表,消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。 消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。下面我们将介绍采用本地消息表机制,使用RabbitMQ实现分布式事务。

2.2 使用RabbitMQ实现分布式事务

RabbitMQ实现分布式事务主要采用了RabbitMQ消息的可靠性传递,并结合定时任务对发送失败信 息进行补偿发送。这种事务实现方式与我们之前介绍的本地事务相差较远, 之前我们讲的本地事务如果 包括A,B两个逻辑单元,当前一个逻辑单元A成功,而后一个逻辑单元B失败时,逻辑单元A回滚即可。而 分布式事务,由于不在同一个数据库中,涉及网络信息传递,因此回滚的开销会很大。因此本地消息表 分布式事务强调A逻辑成功后,B逻辑也要保证成功。如果不成功,B逻辑会重复执行,直到成功为止 (实际生产往往会设置重复执行次数,超出次数后,转人工处理)。 下面我们将延续上一章我们讲述的银行转账的案例讲解本地消息表实现分布式事务。 ABC银行某用户向ICBC银行某用户转账,ABC系统用户扣款成功,发送消息给ICBC系统给用户账号增加 余额。然而消息发送失败或者在投递过程中丢失了,导致ABC用户扣款而ICBC用户账号余额没有增加, 这就导致了系统不一致性,造成了巨大的损失,为了避免类似的情况发生,就必须保证消息可靠性投 递。 可靠性投递方案包括如下四点: (1) 保证消息成功发出 (2) 保证RabbitMQ成功接收,并确认应答 (3) 保障消费者成功消费 (4) 加入补偿机制(定时任务,人工处理等)

3.分布式事务实现银行转账

  1. 准备数据库,bank_abc和bank_icbc
  2. 构建SpringBoot项目,导入Spring-web、lombox、Spring for Rabbit、MySql Driver
  3. 创建两个Maven子模块,abc和icbc,在右侧把数据库显示出来
    创建模块
    idea连接数据库
  4. 在父项目中导入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
  1. 在abc模块中添加application.yml文件
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bank_abc
    username: root
    password: root
  rabbitmq:
    virtual-host: /
    username: elbowH
    password: 123456
    host: 192.168.232.129
    port: 5672
    publisher-confirm-type: correlated
  1. 在abc模块中创建包结构和启动类
@SpringBootApplication
@MapperScan("com.elbowH.abc.mapper")
@EnableScheduling
public class App 
{
    public static void main( String[] args )
    {
        SpringApplication.run(App.class,args);
    }
}

  1. 生成实体类,所有关于时间的属性都加上JsonFormat和序列化
	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
	@JsonSerialize(using = LocalDateTimeSerializer.class)
	@JsonDeserialize(using = LocalDateTimeDeserializer.class)
	private java.time.LocalDateTime overtime;
	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
	@JsonSerialize(using = LocalDateTimeSerializer.class)
	@JsonDeserialize(using = LocalDateTimeDeserializer.class)
	private java.time.LocalDateTime createTime;
	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
	@JsonSerialize(using = LocalDateTimeSerializer.class)
	@JsonDeserialize(using = LocalDateTimeDeserializer.class)
	private java.time.LocalDateTime updateTime;
  1. 编写三个实体类对应的mapper
    mapper
  2. 定义service,编写转账方法
/**
 * @author elbowH
 * 2023/05/04 09:51
 * abc银行转账业务
 */
public interface AbcTransferService {
    /**
     * 转账功能
     * @param from 钱来自哪个账号
     * @param to 钱去哪个账户
     * @param money 转账的金额
     */
    void transferOut(String from, String to, Integer money);
}

  1. 定义实现类serviceImpl
@Service
public class TransferServiceImpl implements TransferService{
	@Resource
	private ABCUserMapper abcUserMapper;
	@Resource
	private ABCTransferRecordMapper abcTransferRecordMapper;
	@Resource
	private ABCTaskMapper abcTaskMapper;
	@Override
	@Transactional
	public void transferOut(String from, String to, Integer money) {
		//1.用户表减钱
		//2.增加转账记录
		//3.添加本地消息表,存储消息
	}
}
  1. 在ABCUserMapper中定义一个转账的方法
     /**
      * 用户金额减少
      * @param from 哪个账户
      * @param money 减少金额
      */
     @Update("update bank_abc.abc_user set account = account - #{money} where id = #{from}")
     void minusMoney(@Param("from") String from, @Param("money") Integer money);
  1. 在ABCTaskMapper中定义获取乐观锁的方法
    /**
     * 获取乐观锁
     * @param id 消息id
     * @param version 乐观锁
     * @return 返回0 失败,1 成功
     */
    @Update("update bank_abc.abc_task set version = version + 1 where id = #{id} and version=#{version}")
    int lockRecord(@Param("id") String id, @Param("version") int version);
  1. 实现Tran’s’fe’rServiceImpl实现类
@Override
    @SneakyThrows
    @Transactional(rollbackFor = Exception.class)
    public void transferOut(String from, String to, Integer money) {
        //用户减少钱
        abcUserMapper.minusMoney(from,money);
        //添加转账记录
        AbcTransferRecord record = new AbcTransferRecord();
        record.setCreateTime(LocalDateTime.now());
        record.setMoney(money);
        record.setFromUid(from);
        record.setToUid(to);
        int insert = abcTransferRecordMapper.insert(record);
        //将转账记录转json
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(record);
        //消息存储到数据库
        AbcTask abcTask = new AbcTask();
        abcTask.setCreateTime(LocalDateTime.now());
        abcTask.setRequestBody(json);
        abcTask.setTaskType("转账");
        abcTask.setStatus(0);
        abcTask.setTryCount(0);
        abcTask.setVersion(0);
        abcTask.setMqExchange("");
        abcTask.setUpdateTime(LocalDateTime.now());
        abcTask.setOvertime(LocalDateTime.now().plusMinutes(5));
        int insert1 = abcTaskMapper.insert(abcTask);
    }
  1. 定义RabbitMQ相关配置类,创建一个config包,在创建一个类
@Configuration
public class RabbitConfig {
    @Bean
    public Queue icbcQueue(){
        return QueueBuilder.durable("bank.icbc.queue").build();
    }
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

  1. 在ABCTaskMapper中定义三个方法
    /**
     * 修改信息状态
     * @param id 信息id
     * @param status 要修改的状态 0 未发送,1 已发送,2 发送失败
     * @param now 修改时间
     * @return 返回0 失败, 1 成功
     */
    @Update("update bank_abc.abc_task set status = #{status}, update_time = #{now} where id = #{id}")
    int changeStatus(@Param("id") String id, @Param("status") int status, @Param("now") LocalDateTime now);

    /**
     * 存储错误信息
     * @param id 信息id
     * @param errorMsg 错误信息
     * @return 返回0 失败, 1成功
     */
    @Update("update bank_abc.abc_task set error_msg = #{errorMsg} where id = #{id}")
    int changFail(@Param("id") String id, @Param("errorMsg") String errorMsg);

    /**
     * 增加尝试次数
     * @param id 消息id
     * @return 返回0失败, 1成功
     */
    @Update("update bank_abc.abc_task set try_count = try_count + 1 where id = #{id}")
    int changTryCount(String id);
  1. 定义rabbit包,定义定时向rabbitmq发送任务
@Component
public class BankTransferProvider implements RabbitTemplate.ConfirmCallback{
    @Resource
    private AbcTaskMapper abcTaskMapper;

    private RabbitTemplate rabbitTemplate;

    @Resource
    public void setRabbitTemplate(RabbitTemplate rabbitTemplate) {
        rabbitTemplate.setConfirmCallback(this);
        this.rabbitTemplate = rabbitTemplate;
    }


    @Scheduled(initialDelay = 3000, fixedDelay = 1000 * 60 * 5)
    public void basicPublish() {
        System.out.println("定时任务");
        LambdaQueryWrapper<AbcTask> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(AbcTask::getStatus, 0)
                .lt(AbcTask::getTryCount, 5)
                .orderByAsc(AbcTask::getCreateTime);
        List<AbcTask> abcTasks = abcTaskMapper.selectList(wrapper);
        abcTasks.forEach(abcTask -> {
            int i = abcTaskMapper.lockRecord(abcTask.getId(), abcTask.getVersion());
            if (i > 0) {
                if(LocalDateTime.now().isBefore(abcTask.getOvertime())){
                    abcTaskMapper.changTryCount(abcTask.getId());
                    rabbitTemplate.convertAndSend("bank.icbc.queue",abcTask,new CorrelationData(abcTask.getId()));
                    System.out.println(abcTask);
                }else {
                    abcTaskMapper.changeStatus(abcTask.getId(),2,LocalDateTime.now());
                }
            }
        });
    }
    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        String id = correlationData.getId();
        if(b){
            abcTaskMapper.changeStatus(id,1,LocalDateTime.now());
        }else{
            abcTaskMapper.changFail(id,s);
        }
    }

}
  1. 定义一个控制器
@RestController
public class TransferController {
    @Resource
    private AbcTransferService transferService;

    @GetMapping("/transferOut")
    public String transferOut(String from,String to,int money){
        try {
            transferService.transferOut(from,to,money);
            return "转账成功!";
        }catch (Exception e){
            return "转账失败!";
        }
    }

}
  1. 测试
GET http://localhost:8080/transferOut?from=1001&to=1002&money=1000
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值