基于ActiveMQ的分布式事务

基于ActiveMQ的分布式事务

  • 1.简介
  • 2.背景
  • 3.实例
    • 3.1数据库(oracle)准备
    • 3.2数据库表准备
    • 3.3创建项目
    • 3.4创建包结构
    • 3.5执行流程
  • 4.代码详解
    • 4.1 dao
    • 4.2 mapper
    • 4.3 mybatis的配置
    • 4.4 spring配置
    • 4.5 多数据源的动态切换
    • 4.6 MQ
    • 4.7 定时任务
  • 5.项目总结

项目git地址:
https://github.com/a18792721831/MQ.git
分支gradle_init
建议先下载代码,然后对比代码查看。
本项目没有完成,只是实现了add,其余的query、del、modify都未实现,但原理相同。
ActiveMQ:
https://pan.baidu.com/s/1PfQVwSDr877qRK5izdIWqw

补充一张设计草图
基于ActiveMQ的分布式事务_第1张图片

1.简介

在微服务已大行其道的当下,分布式事务也是微服务理念在落地过程中最具挑战性又不得不面对的技术难题之一。目前常见的解决分布式事务问题的方案有:两阶段提交、补偿事务、本地事件表加消息队列、MQ事务消息等。

2.背景

用户注册场景:新用户注册之后给该用户新增一条积分记录。但是用户表与积分表并不在同一个数据库中。问题的核心是数据库1的事务完成后需要协调通知数据库2执行事务。
如何保证操作的原子性?

  • 用户表在创建用户后还没有来得及通知积分服务就发生宕机
  • 积分表在收到通知未来得及创建记录就发生宕机
    上述问题的本质是:如何让数据库和消息队列的操作是一个原子操作?
    事件表。
  1. 用户注册的服务收到请求后在用户表中创建一条用户记录,并且在事件表中新建一个事件,事件的状态为新建
  2. 在用户系统中开启一个定时任务定时查询事件表中为新建的记录,如果查询到数据,那么就给MQ发布这个事件,同时修改这个事件的状态为已发布
  3. 积分系统接收到MQ的消息后,在积分数据库中创建一个事件,状态为新建,当积分提交事务时,将事件状态修改为已提交。
  4. 积分系统中的定时任务查询事件表,查询到结果后增加积分记录,同时修改积分事件表。

3.实例

3.1数据库(oracle)准备

create user db1 identified by pswd;--创建db1
grant dba,create session to db1;--赋予权限
create user db2 identified by pswd;--创建db2
grant dba,create session to db2;--赋予权限

基于ActiveMQ的分布式事务_第2张图片

3.2数据库表准备

db1

CREATE TABLE subscriber(
ID NUMBER(8),
CODE VARCHAR2(50),
NAME VARCHAR2(50),
PASSWORD VARCHAR2(50),
state NUMBER(1)
);
ALTER TABLE subscriber 
ADD constraint subscriber_pk
PRIMARY KEY(ID);
ALTER TABLE subscriber
MODIFY state
NOT NULL;
ALTER TABLE subscriber
ADD CONSTRAINT subscriber_state_ck
CHECK(state IN(0,1));
CREATE SEQUENCE seq_subscriber
MINVALUE 1
MAXVALUE 99999999
START WITH 1
INCREMENT BY 1
CACHE 10;
COMMENT ON TABLE subscriber IS '用户表';
COMMENT ON COLUMN subscriber.ID IS '用户id';
COMMENT ON COLUMN subscriber.CODE IS '用户编码';
COMMENT ON COLUMN subscriber.NAME IS '用户名字';
COMMENT ON COLUMN subscriber.PASSWORD IS '用户密码';
COMMENT ON COLUMN subscriber.state IS '用户状态(0:有效,1:无效)';
CREATE TABLE event(
ID NUMBER(8),
TYPE NUMBER(1),
process NUMBER(1),
SUBSCRIBERID NUMBER(8),
createdt DATE DEFAULT SYSDATE,
UPDATEdt DATE DEFAULT SYSDATE,
CONTENT VARCHAR2(100)
);
ALTER TABLE event
ADD CONSTRAINT event_pk
PRIMARY KEY(ID);
ALTER TABLE event
ADD CONSTRAINT event_type_ck
CHECK(TYPE IN (0,1,2,3));
ALTER TABLE event
ADD CONSTRAINT event_process_ck
CHECK(process IN (0,1,2,3));
ALTER TABLE event
MODIFY CREATEdt
NOT NULL;
ALTER TABLE event 
MODIFY UPDATEdt
NOT NULL;
CREATE SEQUENCE seq_event
MINVALUE 1
MAXVALUE 99999999
START WITH 1
INCREMENT BY 1
CACHE 10;
COMMENT ON TABLE event IS '事件表';
COMMENT ON COLUMN event.id IS '事件id';
COMMENT ON COLUMN event.type IS '事件类型';
COMMENT ON COLUMN event.process IS '事件处理环节';
COMMENT ON COLUMN event.SUBSCRIBERID IS '事件操作的用户';
COMMENT ON COLUMN event.createdt IS '事件创建时间';
COMMENT ON COLUMN event.updatedt IS '事件更新时间';
COMMENT ON COLUMN event.content IS '事件传输对象';

db2

CREATE TABLE integral(
ID NUMBER(8),
subscriberid NUMBER(8),
amount NUMBER(11,2),
state NUMBER(1)
);
ALTER TABLE integral
ADD CONSTRAINT integral_pk
PRIMARY KEY(ID);
ALTER TABLE integral
ADD CONSTRAINT integral_subid_uq
UNIQUE(subscriberid);
ALTER TABLE integral
MODIFY amount
NOT NULL;
ALTER TABLE integral
MODIFY state
NOT NULL;
CREATE SEQUENCE seq_integral
MINVALUE 1
MAXVALUE 99999999
START WITH 1
INCREMENT BY 1
CACHE 10;
COMMENT ON TABLE integral IS '积分表';
COMMENT ON COLUMN integral.id IS '积分id';
COMMENT ON COLUMN integral.subscriberid IS '积分对应的用户';
COMMENT ON COLUMN integral.amount IS '积分金额';
CREATE TABLE event(
ID NUMBER(8),
TYPE NUMBER(1),
process NUMBER(1),
SUBSCRIBERID NUMBER(8),
createdt DATE DEFAULT SYSDATE,
UPDATEdt DATE,
CONTENT VARCHAR2(100)
);
ALTER TABLE event
ADD CONSTRAINT event_pk
PRIMARY KEY(ID);
ALTER TABLE event
ADD CONSTRAINT event_type_ck
CHECK(TYPE IN (0,1,2,3));
ALTER TABLE event
ADD CONSTRAINT event_process_ck
CHECK(process IN (0,1,2,3));
ALTER TABLE event
ADD CONSTRAINT event_subscriber_uq
UNIQUE(subscriberid);
CREATE SEQUENCE seq_event
MINVALUE 1
MAXVALUE 99999999
START WITH 1
INCREMENT BY 1
CACHE 10;
COMMENT ON TABLE event IS '事件表';
COMMENT ON COLUMN event.id IS '事件id';
COMMENT ON COLUMN event.type IS '事件类型';
COMMENT ON COLUMN event.process IS '事件处理环节';
COMMENT ON COLUMN event.SUBSCRIBERID IS '事件操作的用户';
COMMENT ON COLUMN event.createdt IS '事件创建时间';
COMMENT ON COLUMN event.updatedt IS '事件更新时间';
COMMENT ON COLUMN event.content IS '事件传输对象';

主键与唯一性约束会自动创建索引。

3.3创建项目

基于ActiveMQ的分布式事务_第3张图片
增加依赖

plugins {
    id 'java'
}

group 'ActiveMqDist'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    maven {
        url 'http://maven.aliyun.com/nexus/content/groups/public/'
    }
    maven {
        url 'https://repo1.maven.org/maven2/'
    }
}

dependencies {
    implementation 'org.apache.activemq:activemq-client:5.15.9'
    implementation group: 'org.apache.activemq', name: 'activemq-pool', version: '5.15.9'
    implementation 'org.apache.activemq:activemq-spring:5.15.9'
    implementation group: 'org.springframework', name: 'spring-jms', version: '5.1.9.RELEASE'
    implementation group: 'org.springframework', name: 'spring-jdbc', version: '5.1.9.RELEASE'
    implementation group: 'org.springframework', name: 'spring-orm', version: '5.1.9.RELEASE'
    implementation group: 'org.springframework', name: 'spring-tx', version: '5.1.9.RELEASE'
    implementation group: 'org.springframework', name: 'spring-aop', version: '5.1.9.RELEASE'
    implementation 'org.springframework:spring-beans:5.1.9.RELEASE'
    implementation 'org.springframework:spring-core:5.1.9.RELEASE'
    implementation 'org.springframework:spring-expression:5.1.9.RELEASE'
    implementation 'org.springframework:spring-context:5.1.9.RELEASE'
    implementation 'org.springframework:spring-aspects:5.1.9.RELEASE'
    implementation 'commons-logging:commons-logging:1.2'
    implementation group: 'com.github.noraui', name: 'ojdbc8', version: '12.2.0.1'
    implementation group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.6.0'
    implementation group: 'org.mybatis', name: 'mybatis', version: '3.5.2'
    implementation group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.2'
    implementation group: 'org.aspectj', name: 'aspectjrt', version: '1.9.4'
    implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.4'
    implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.58'
    implementation 'org.slf4j:slf4j-nop:2.0.0-alpha0'
    implementation 'javax.jms:javax.jms-api:2.0.1'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

注意,依赖不能使用activemq-all这个包,因为activemq-all包依赖了log4j相关的包,而mybatis也依赖了log4j的包,且版本不一致,所以需要依赖activemq-all的子包。

3.4创建包结构

基于ActiveMQ的分布式事务_第4张图片
包解析—技术栈解析:

aspect包是spring aop的增强类
business是封装的对外接口,与数据库交互的最顶层的接口,内部封装了与mq的交互、事件表记录的处理
businessimpl是business接口的实现
client是自测的类,只是一个简单的人机交互与business接口调用的类,同时也是spring的启动类
condition是用户、积分、事件表查询条件的java bean
dao是数据持久的接口,是最简单的增删改查接口
因为集成了mybatis,所以dao的实现是xml文件
data是数据库路由工具,数据库的动态切换的工具
domain是数据库表映射实体,也是普通的java bean
exception是自定义的异常类
messagecomsume是mq的消费者类
mqimpl是mqservice的实现类
mqservice定义了发布mq的接口,通过mq的接口即可发布消息
neums是domain中定义的一些枚举属性
routing是数据库路由动态切换的切点
routingimpl是service的上层代理,换句话说,我想调用service的方法,就必须通过routing来调用,否则数据库不会动态进行切换
service是对数据持久的服务封装,也是业务核心
serviceimpl是service的实现
timingtask是基于spring的定时任务
util是封装的一些工具

3.5执行流程

client中调用business的接口中的方法,business中的方法是集成mqservice和routing的方法。mqservice会向mq服务器发布mq,routing则进行动态的数据库切换。然后routing调用service,这个时候,数据库已经切换完毕,service有事务的封装服务。service调用dao实现数据库持久。
同时定时任务也在根据时间表达式进行执行。

为什么这样设计?

  • dao使用的是mybatis,mybatis是使用代理的方式实现的,即mybatis会根据xml和接口生成代理,实现SQL的执行。
  • service拥有事务,保证业务的完整性
  • routing是因为service已经有了事务,有了事务就会复用session连接。因为我们是在aspect中的切面中实现数据库的动态切换的,所以要保证aspect在获取session之前执行,但是有了事务,就会首先获取session,所以不得不多封装一层。

4.代码详解

4.1 dao

dao使用的是mybatis
即java代码只定义接口:
基于ActiveMQ的分布式事务_第5张图片
其中dao内部抽取的包没有特殊的含义,只是个人风格。
因为inte是数据库db2的独有的数据库表,同样的sub也是db1独有的表;而event是db1和db2都有的表,且数据结构相同。

注意点:
1.xml文件不能放在接口的同文件目录下,在eclipse中一般是接口与xml在一起,但是因为使用的是gradle项目,在gradle项目中,java目录下只会加载代码,不会加载xml文件,所以,xml文件不能放在接口的同文件目录下。
2.在gradle项目中,xml文件只能放在resources目录下,这样项目才会加载xml文件,而且需要在build.gradle文件中指定加载的具体操作,网上很多,需要配置一下,比如打包的时候将xml的mapper文件拷贝到java的对应目录下,手动实现接口与mapper在一起。不过我们用的是另一种方法。
配置方法参见这篇文章:
gradle项目中资源文件的相对路径打包处理技巧
3.实现mybatis在gradle中加载mapper的xml文件,还有一种方式就是在resources目录下,创建与接口相同的目录结构。比如我的接口的全类名是基于ActiveMQ的分布式事务_第6张图片
com.study.dao.EventDao.java
那么mapper文件的相对目录也是
com.study.dao.EventDao.xml
是不是和在eclipse中写mybatis达到了相同的目的。

ok 接口的方法很简单:

void addEvent(Event event);
void modifyEvent(Event event);
void delteEvent(Event event);
Event queryEventById(Long id);
List<Event> queryEventByCondition(EventCondition condition);

基于ActiveMQ的分布式事务_第7张图片
使用了阿里的规范检测,所以最简单的接口也需要有注释。良好的编码习惯要有,记忆会遗忘,但是注释却不会自动删除。

4.2 mapper

mapper的xml文件使用的是mybatis的动态SQL实现的。
有这样一个理念(忘记名词是啥了):本层的问题最好本层解决,尽可能少的将问题泄露出去。比如我们新增用户的时候,用户的状态默认就是新建,那么对于新建时,如果传输的用户实体中如果状态属性为空,那么避免空指针异常,初始化为新建。可以将这个异常在出现异常的dao层解决,避免泄露到外层。

这是一个反例,不赞成在dao有很多的业务逻辑,我是为了复习下mybatis的动态SQL的使用,所以在dao中实现了大量业务逻辑。

但是这是一个不好的点,dao层应该保持尽可能的纯粹,不要掺杂业务逻辑。

这是一个反例。(重要的事情说三遍,不要被我误导)

基于ActiveMQ的分布式事务_第8张图片
要点:

  • mapper的命名空间要对,以接口的结构为准。
    基于ActiveMQ的分布式事务_第9张图片
    • 尽可能的抽取对于表的表名,全字段名,最好定义为可复用的SQL
      这样做的好处就比较多,表结构修改时,只需要修改一处即可,还有就是全字段可以保证查询的SQL查询的属性是全的,不存在属性遗漏的情况。
      基于ActiveMQ的分布式事务_第10张图片
      因为使用的是oracle数据库,oracle没有自增主键这个特性,但是oracle使用序列实现主键自增。
      需要注意,db1和db2的序列完全独立,没有关联。

其实将事件表的主键在db1和db2中关联起来,这样会将业务处理的难度下降一些,但是实现就会复杂一些。

oracle中的insert使用序列的mapper如下:

<insert id="addEvent" useGeneratedKeys="true" keyProperty="id" keyColumn="ID" parameterType="com.study.domain.Event">
        <selectKey resultType="java.lang.Long" keyProperty="id" order="BEFORE">
            SELECT SEQ_EVENT.NEXTVAL AS ID FROM DUAL
        selectKey>
        INSERT INTO
        <include refid="str_table_even"/>
        (<include refid="str_select_event"/>)
        VALUES(#{id}
        <choose>
            <when test="eventType != null">
                ,#{eventType.value}
            when>
            <otherwise>
                ,0
            otherwise>
        choose>
        <choose>
            <when test="processType != null">
                ,#{processType.value}
            when>
            <otherwise>
                ,0
            otherwise>
        choose>
        <choose>
            <when test="subscriberId != null">
                ,#{subscriberId}
            when>
            <otherwise>
                ,NULL
            otherwise>
        choose>
        <choose>
            <when test="createdt != null">
                ,#{createdt}
            when>
            <otherwise>
                ,SYSDATE
            otherwise>
        choose>
        ,NULL
        )
    insert>

selectKey就是mybatis对于oracle数据库的支持,order="BEFORE"表示在外层SQL执行前执行。

这里有一个知识点,使用了useGeneratedKeys="true"属性属性设置,就表示mybatis会将insert语句中的使用的id返回,但是接口定义为void,这是因为使用了这个属性,mybatis在执行完SQL,完成插入语句后,会将主键即keyProperty=“id” keyColumn="ID"属性将java实体的属性与数据库主键字段映射在一起。完成插入操作,会将数据库中ID字段的值设置到java实体的id属性中,实现主键的返回。

前面已经封装了数据库表的表名和全字段名,后面的SQL中使用即可使用。
基于ActiveMQ的分布式事务_第11张图片
这个是实现空指针的避免,类似if语句(反例)

4.3 mybatis的配置

mybatis的配置文件在resources的mybatis目录中,也是只有一个文件,这个是个人编码风格。
基于ActiveMQ的分布式事务_第12张图片



<configuration>
    <settings>
        
        <setting name="lazyLoadingEnabled" value="true"/>
        
        <setting name="aggressiveLazyLoading" value="false"/>
        

    settings>
    
    <typeAliases>
        
        <package name="com.study"/>
    typeAliases>
configuration>

配置中有详细的注释。
sql调试在开发中可以打开,但是打开后会有大量的控制台输出。。

4.4 spring配置

spring的配置在resources的spring中
基于ActiveMQ的分布式事务_第13张图片
其中:
mq-jms.xml是定义消费者和mq的主题的关系。
spring-aop.xml是打开注解扫描,启动注解的sapectJ支持以及指定定时任务的解析驱动
spring-beans.xml是开启基于注解的依赖注入
spring-dataBase.xml是配置数据源、加载配置文件、配置mybatis的mapper扫描器、mybatis的工厂、以及多数据源的配置
spring-transaction.xml是开启事务基于注解的扫描、注册事务管理器、定义事务等级
详解:
基于ActiveMQ的分布式事务_第14张图片
在spring-dataBase.xml中配置了两个数据源,db1和db2,然后定义一个多数据源的路由表的bean
在spring-transaction,xml中对路由表进行事务管理。
换句话说,就是路由表中的所有的数据源都将受到这一个事务管理器的管理。

4.5 多数据源的动态切换

核心思想是在项目中埋一个切点,这个切点就是routing包,然后在aspect中编写切面,routing中根据数据库分为多个包,一个数据库一个包,这个包下的方法被调用就表示使用这个数据源。
这点使用aop进行增强。
因为在spring中已经定义了数据库路由的路由表,接下来就是如何进行路由。
这里推荐这篇文章
Spring + Mybatis 多数据源配置与使用总结
基于ActiveMQ的分布式事务_第15张图片
data包中有两个类:
MyDataSource类继承AbstractRoutingDataSource实现determineCurrentLookupKey方法
determineCurrentLookupKey方法返回的是DataSourceManage中的属性,即一个枚举在这里插入图片描述
这个枚举就是所有的数据源,枚举含有一个name的属性基于ActiveMQ的分布式事务_第16张图片
这个name就是在spring中配置的数据源。
而DataSourceManage有一个本地线程属性,这个线程属性是枚举,放的值就是枚举。
这样,只要这个DataSourceManage的ThreadLocal变量的值,就是当前连接的数据源。
接下来只要在aspect中配置切点,根据包名切换不同的数据源。
为了保证切换的数据库正确,虽然在spring配置中指定了默认的数据源是用户数据库即db1但是在aspect中每次用完都会将这个属性置空,用来保证下次使用之前一定会先设置数据源,然后使用数据库。避免将db2的数据库存入了db1.
基于ActiveMQ的分布式事务_第17张图片
基于ActiveMQ的分布式事务_第18张图片
在before方法中指定数据源,在after方法中清空数据源。。
但是这里可能会有性能问题,可以不清空,延迟设置数据源。如果两次调用都用的是db1的数据源,那么就不需要多次设置了。

4.6 MQ

MQ的配置其实没有多少区别,这个项目主要是学习使用MQ,所以都是MQ最基础的东西,并没有新的东西。。
mq-jms.xml


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	   http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="activeMQConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="tcp://localhost:61616"/>
    bean>
    <bean id="cachingConnectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory">
        <property name="targetConnectionFactory" ref="activeMQConnectionFactory"/>
        
        <property name="sessionCacheSize" value="5"/>
    bean>
     
    <bean id="subscriberTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="active-sub"/>
    bean>
    
    <bean id="integralTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="active-integral"/>
    bean>
    
    <bean id="jmsSubTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="cachingConnectionFactory"/>
        <property name="defaultDestination" ref="subscriberTopic"/>
    bean>
    
    <bean id="jmsIntegralTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="cachingConnectionFactory"/>
        <property name="defaultDestination" ref="subscriberTopic"/>
    bean>
    
    <bean id="subListener" class="com.study.messagecomsume.SubsciberMessageListener"/>
    
    <bean id="integralListener" class="com.study.messagecomsume.IntegralMessageListener"/>
    
    <bean id="subContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="cachingConnectionFactory"/>
        <property name="destination" ref="integralTopic"/>
        <property name="messageListener" ref="subListener"/>
    bean>
    
    <bean id="topic1Container" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <property name="connectionFactory" ref="cachingConnectionFactory"/>
        <property name="destination" ref="subscriberTopic"/>
        <property name="messageListener" ref="integralListener"/>
    bean>
beans>

4.7 定时任务

定时任务可以使用xml配置,或者开启基于注解的驱动扫描。
使用的方式也很简单,只需要在需要定时执行的方法上写@Scheduled(cron = "*/5 * * * * *")注解即可.

5.项目总结

项目本身没有什么难度,只是将之前学习的知识串起来,然后在集成的过程中有一些小小的细节需要注意。胆大心细即可。。

你可能感兴趣的:(java,MyBatis,spring,oracle,ActiveMQ,MQ)