使用spring解决分布式事物

概述:提及分布式事务,各位可能都不陌生,在互联网流量如此大的今天,可以说网站的搭建再也不是一台服务器就能搞定的,大量的服务器集群和数据库集群为网站的高压力提供了支持,但是同时系统的复杂性,编码中的需要考虑的问题也越来越多,单点故障怎么办,网络通信延迟造成数据混乱怎么解决,这些都让当今的架构和编码难度成倍的增加,今天就和大家聊一聊分布式架构中常见的分布式事务问题—多源数据库事务的管理
我们借助一个场景来说:
这里写图片描述
处理程序里面有这一个这样的步骤,它需要把消息处理之后发送的MQ,同时往日志里面插一条数据,这个过程很简单,但是如果处理不得当会出现数据错乱问题,在与DATA和MQ通信的时候如果任何一方发生故障则会导致数据的不一致,这两者显然是具有事务性的。
理论:从理论上说数据的一致性分为一下几种:
1.强一致性
2.弱一致性
3.最终一致性
强一致性是最严格的标准,它要求数据的实时一致,拿上面的情况来说,如果我们往MQ中推送消息成功那么要求在DATA中也必须同时更新数据成功,如果第二个操作失败则必须撤回第一个操作,数据要求实时一致
而弱一致性就要求没那么严格了,美团网的订客房系统不知道大家有没有体验过,全国的客房信息很多,美团显然不可能实时拉取所有的客房信息,他是每隔一个时间段来拉去一次,这个时候显示的数据就可能和最新的数据不一样,但是当你下单的时候会有一个二次验证,对当前下单的客房进行一次拉取,这样就避免了延迟数据的下单,这种一致性的解决就是弱一致性。它可能并不是实时的一致,会有延迟。
而最总一致性则是三种之中要求最弱的一种,它更像是一种不作为的处理方法,在系统出错率低的情况下保证效率优先,通过后期的补救手段来将数据完成一致。
解决思路:
理论说了这么多,现在说下实际处理的方法,我们先拿第一种方式来说:
思路:回想一下单数据库的解决思路
这里写图片描述
上图就是经典的数据库提交过程,先是一次prepare,准备阶段就像是一次试提交,试提交没问题了然后才真正提交。如果试提交有问题就会重复的试提交直到连接超时。如果是多个数据源是不是也能有个试提交的过程呢,答案是显然的
这里写图片描述
道理是一样的,向两个数据源进行准备阶段的试提交,如果这里都OK了,然后开始第二阶段的提交,试提交依然充当了一个排除员的角色,我们所担心的一个成功一个失败的情况显然是能被排查掉的。
代码:
java提供了事务的接口规范JPA来规范各种需要事物的操作,spring使用template的模式提供了各种支持,我们这里不讨论底层的实现,以rabbitmq和mysql为例,来看看spring怎么结合两者实现同步事物的。
spring-mysql的配置

<!-- 配置连接池 -->
<bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
    <!-- 数据库驱动 -->
    <property name="driverClass" value="${jdbc.driver}" />
    <!-- 相应驱动的jdbcUrl -->
    <property name="jdbcUrl" value="${jdbc.url}" />
    <!-- 数据库的用户名 -->
    <property name="username" value="${jdbc.username}" />
    <!-- 数据库的密码 -->
    <property name="password" value="${jdbc.password}" />
    <!-- 检查数据库连接池中空闲连接的间隔时间,单位是分,默认值:240,如果要取消则设置为0 -->
    <property name="idleConnectionTestPeriod" value="60" />
    <!-- 连接池中未使用的链接最大存活时间,单位是分,默认值:60,如果要永远存活设置为0 -->
    <property name="idleMaxAge" value="30" />
    <!-- 每个分区最大的连接数 -->
    <property name="maxConnectionsPerPartition" value="150" />
    <!-- 每个分区最小的连接数 -->
    <property name="minConnectionsPerPartition" value="5" />
</bean>

<!-- 定义事务管理器 -->
<bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>  

<!-- 定义事务策略 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--定义查询方法都是只读的 -->
        <tx:method name="query*" read-only="true" />
        <tx:method name="find*" read-only="true" />
        <tx:method name="get*" read-only="true" />
        <tx:method name="select*" read-only="true" />

        <!-- 主库执行操作,事务传播行为定义为默认行为 -->
        <tx:method name="save*" propagation="REQUIRED" />
        <tx:method name="update*" propagation="REQUIRED" />
        <tx:method name="delete*" propagation="REQUIRED" />

        <!--其他方法使用默认事务策略 -->
        <tx:method name="*" />
    </tx:attributes>
</tx:advice>

<aop:config>
    <!-- 定义切面,所有的service的所有方法 -->
    <aop:pointcut id="txPointcut" expression="execution(* com.DF.service.*.*(..))" />
    <!-- 应用事务策略到Service切面 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>

rabbitmq配置

<!-- 连接配置 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbit.ip}" username="${rabbit.username}" 
    password="${rabbit.password}" port="${rabbit.port}"  virtual-host="${rabbit.vhost}"/>
<rabbit:admin connection-factory="connectionFactory"/>

<!-- spring template声明-->
<rabbit:template id="amqpTemplate" connection-factory="connectionFactory" />

**代码部分:**

public class FenbushiServiceImpl implements FenbushiService {
@Autowired
private MessageLogService messageLogService;
@Override
public Long saveLogAndSendMq(){
    /*
     * service逻辑:
     * 1.查询当天最大的messageid+1 作为当前的messageid 
     * 2.发送到队列
     * 3.更新messageid
     * 步骤二和三是可以调换的
     * 这个过程是明显具有事物性的  如果说步骤2成功 3失败  数据将错乱 
     * 这里尝试几种解决的思路和办法
     */
    Long messageid;
    try {
        messageid = schemeOne();

    } catch (Exception e) {
        return null;
    }
    return messageid;
}

/*
 *1.方案一  在程序中控制  采用失败回滚的方式 
 *这种方案就是先进行步骤1和3  然后执行二  如果2失败处理异常的时候进行3的回滚
 *这种主要是利用了spring事物的管理  把数据库的事物依然交给spring管理  而异构
 *数据源的事物交给代码管理
 */
Long schemeOne(){

    //步骤1
    long messageid = messageLogService.selectMaxIdToday();

    //步骤2
    messageLogService.savelog(new Message_Log(messageid, 1, 1, "在程序中尝试解决事物", 
            null, null, null, null, null, null, null, null, null, null, null, null, null));


    try {
        //步骤3
        EventMessage message = new EventMessage(null,ParsePropUtil.getProp(ConfigurtionManager.RABBITMQ_ENTERE_ROATINGKEY)
                , ParsePropUtil.getProp(ConfigurtionManager.RABBIT_ENTER_EXCHANGE)
                , "在程序中尝试解决事物".getBytes());
        int i = 1/0; // 模拟步骤3出现问题
        MessageSender.sendMessage2Exchange(message);

    } catch (Exception e) {
        //spring中声明式事物的管理  所以这里抛出运行时异常 spring会捕捉 并回滚service
        throw new RuntimeException();//rollback!
    }
    return messageid;
}

看下封装的MessageSender类:

@Service
public class MessageSender {
// 发送模板  静态变量需要set方法注入
private static RabbitTemplate amqpTemplate ;    
@Autowired
public void setAmqpTemplate(RabbitTemplate amqpTemplate) {
    MessageSender.amqpTemplate = amqpTemplate;
}

// 将消息发送到交换机
public  static void sendMessage2Exchange(final EventMessage eventMessage){
    amqpTemplate.convertAndSend(eventMessage.getExchangeName(), eventMessage.getRoatingKey(),
            eventMessage.getEventData(),new MessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    message.getMessageProperties().setPriority(eventMessage.getProrityLevel());
                    return message;
                }
            });
}

在同一个service中,spring通过template的方式使两者具有事物,数据具有实时一致性,但是很明显由于service中的方法没有运行完成时,事物是始终挂起的,这将导致程序的效率大大降低,所以这种方法虽然让数据实时一致,但在当今的IT领域采用率并不高。
实际上,在绝大多数情况下,操作一和操作二是不容易出现异常数据的,在很低错误率的情况下更多的企业希望采用方式三来解决问题。在最终一致的前提下,我们不需要考虑对操作一和操作二的成功与否,这样效率大大提高,我们只需要在定期的排查异常,发现异常的时候后期处理异常的数据,保证数据最终的一致。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值