上一章主要是初步实现ActiveMQ与Springboot的集成,本章开始将重点专注在ActiveMQ的事务管理,作为生产者很多的业务场景需要对消息发送进行事务管理(即生产不一定发送),本章主要实践的案例将结合MySQL实现应用内分布式事务管理。
本章概要
1、纯MySQL事务管理
1.1、添加依赖;
1.2、添加待测试用户使用的实体User及其repository定义;(采用Spring-data-jpa实现持久层)
1.3、配置MySQL数据源;
1.4、单元测试验证;
2、加入MQ生产者验证
2.1、设置MQ服务地址;
2.2、自定义实现JmsTemplate支持事务;
2.3、将User保存和消息发送放入同一个事务方法;
2.4、单元测试;
纯MySQL事务管理
1.1、添加依赖:
<properties>
<httpclient.version>4.5.1</httpclient.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
<dependencies>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- end -->
<!-- activeMq support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!-- activeMq end -->
<!-- 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.24</version>
</dependency>
<!-- end -->
<!-- jdbc driver 6.0.4 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
<!-- <scope>runtime</scope> -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- end -->
<!-- atomikos实现分布式事务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!-- end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<fork>true</fork><!-- 如果没有该项配置,devtools不会起作用,即应用不会restart -->
</configuration>
</plugin>
<!-- 打包时跳过test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
特别注意:如果我们的服务启动后立即就shutdown,加入下面的依赖即可亦或是启动一个定时任务均可,否则将作为一个客户端应用程序shutdown。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.2、添加待测试用户使用的实体User及其repository定义(采用Spring-data-jpa实现持久层):
1.2.1、实体定义:
package com.shf.activemq.user.entity;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
/**
* 用户Entity
* @author shf
*/
@Entity
@Table(name = "t_sys_user")
@DynamicInsert@DynamicUpdate
public class User implements Serializable{
private static final long serialVersionUID = -2485725404415853576L;
private int id;
private String loginName;// 登录名
private String password;// 密码
private String name; // 姓名
private Date birthday; //出生年月
private String email; // 邮箱
public User() {
super();
}
public User(int id) {
this();
this.id = id;
}
public User(String loginName,String password) {
this();
this.loginName = loginName;
this.password=password;
}
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
//此时culumn中name属性不宜用loginName,建议使用loginname,否则在部分连接池应用下会自动被转为login_Name
@Column(name="loginname",length=5)
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
@Temporal(TemporalType.DATE)
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User [id=" + id + ", loginName=" + loginName + ", password=" + password + ", name=" + name
+ ", birthday=" + birthday + ", email=" + email + "]";
}
}
1.2.2、集成PagingAndSortingRepository持久层实现:
package com.shf.activemq.user.repository;
import java.util.List;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import com.shf.activemq.user.entity.User;
public interface UserRepository extends PagingAndSortingRepository<User,Integer>{
@Modifying
@Query("update User u set u.loginName=:loginName where u.id=:id")
public int update(@Param("loginName")String loginName,@Param("id")int id);
@Query("select t from User t ")
public List<User> getList();
}
小结:在实体定义中约定了loginname不能大于5,故在后续的验证中即可通过其长度约束直观测试。
1.3、配置AtomikosJtaPlatform,通过Atomikos支持分布式事务:
package com.shf.activemq.config;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;
/**
* 配置AtomikosJtaPlatform,通过Atomikos支持分布式事务
* @author song
*
*/
public class AtomikosJtaPlatform extends AbstractJtaPlatform {
private static final long serialVersionUID = 1L;
static TransactionManager transactionManager;
static UserTransaction transaction;
@Override
protected TransactionManager locateTransactionManager() {
return transactionManager;
}
@Override
protected UserTransaction locateUserTransaction() {
return transaction;
}
}
1.4、配置分布式事务管理器:
package com.shf.activemq.config;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
/**
* 配置分布式事务管理器
* @author song
*/
@Configuration
@EnableTransactionManagement
public class JTATransactionConfig {
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(true);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
AtomikosJtaPlatform.transactionManager = userTransactionManager;
return userTransactionManager;
}
@Bean(name = "transactionManagerJTA")
@DependsOn({ "userTransaction", "atomikosTransactionManager"})
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
AtomikosJtaPlatform.transaction = userTransaction;
TransactionManager atomikosTransactionManager = atomikosTransactionManager();
return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
}
}
1.5、配置MySQL数据源:
package com.shf.activemq.config;
import java.util.HashMap;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
/**
* 关系型数据库配置
* @author song
*
*/
@Configuration
@DependsOn("transactionManagerJTA")//必须加
@EnableJpaRepositories(basePackages = {"com.shf.activemq.user.*"},
entityManagerFactoryRef = "entityManagerFactoryJ1",
transactionManagerRef = "transactionManagerJTA"
)
public class MySQLDataSourceConfig {
@Autowired
private JpaVendorAdapter jpaVendorAdapter;
@Bean(name = "dataSourceJ1", initMethod = "init", destroyMethod = "close")
public DataSource dataSourceJ1() {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl("jdbc:mysql://localhost:3306/springboot1");
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword("root");
mysqlXaDataSource.setUser("root");
mysqlXaDataSource.setCharacterEncoding("UTF-8");
mysqlXaDataSource.setUseFastDateParsing(true);
mysqlXaDataSource.setUseUnicode(true);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("xads1");
xaDataSource.setBorrowConnectionTimeout(10);
xaDataSource.setMaxPoolSize(20);
xaDataSource.setMinPoolSize(10);
xaDataSource.setMaintenanceInterval(60);
xaDataSource.setTestQuery("SELECT 1");
return xaDataSource;
}
//------------------方式一-----------------------
@Bean(name = "entityManagerFactoryJ1")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryJ1() throws Throwable {
System.out.println("entityManagerFactoryJ1 create");
HashMap<String, Object> properties = new HashMap<String, Object>();
properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
properties.put("javax.persistence.transactionType", "JTA");
LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
entityManager.setDataSource(dataSourceJ1());
entityManager.setJpaVendorAdapter(jpaVendorAdapter);
entityManager.setPackagesToScan("com.shf.activemq.user.*");
entityManager.setPersistenceUnitName("persistenceUnitJ1");
entityManager.setJpaPropertyMap(properties);
return entityManager;
}
@Bean(name = "entityManagerJ1")
public EntityManager entityManagerJ1() throws Throwable {
return entityManagerFactoryJ1().getObject().createEntityManager();
}
}
1.6、在未加入ActiveMQ之前,首先编写如下UserService测试当时保存用户事务是否生效:
package com.shf.activemq.user.service;
import java.util.ArrayList;
import java.util.List;
import javax.jms.Queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.shf.activemq.user.entity.User;
import com.shf.activemq.user.repository.UserRepository;
/**
* 用户Service
* @author song
*
*/
@Service("userService")
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional(value="transactionManagerJTA",propagation=Propagation.REQUIRED,rollbackFor=Exception.class)
public List<User> bathSaveJTA(List<User> userList) throws Exception {
try{
List<User> list=new ArrayList<User>(10);
for(User user:userList){
list.add(userRepository.save(user));
}
return list;
}catch(Exception e){
throw new Exception("出现异常了");
}
}
}
1.7、单元测试验证事务
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes={AppProducer.class})//加载启动类
public class AppTest
{
@Autowired
private UserService userService;
@Test
public void testUserSave(){
try{
List<User> list=new ArrayList<User>(10);
list.add(new User("111","111"));
list.add(new User("223212","222"));
list.add(new User("333","333"));
userService.bathSaveJTA(list);
}catch(Exception e){
e.printStackTrace();
}
}
}
小结:通过数据库和控制台可以看到事务生效
加入MQ生产者验证
2.1、设置MQ服务地址:
# ACTIVEMQ
spring.activemq.broker-url=tcp://localhost:61626
2.2、自定义实现JmsTemplate支持事务:
通过阅读源码中实现自动装配的ActiveMQAutoConfiguration类,可以找到JmsAutoConfiguration、ActiveMQXAConnectionFactoryConfiguration(
特别注意,在自动装配中其实并没有被执行,其需要@ConditionalOnBean({ XAConnectionFactoryWrapper.class })依赖,默认情况下并没有注册XAConnectionFactoryWrapper的bean实例)等重要配置,在JmsAutoConfiguration中默认的JmsTemplate没有支持事务,故需要实现自定义JmsTemplate的bean实例,实现如下:
package com.shf.activemq.config;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Session;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.jms.JmsProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
/**
* 自定义JmsTemplate,支持事务
* @author song
*
*/
@Configuration
public class JmsTemplateConfiguration {
private final JmsProperties properties;
private final ObjectProvider<DestinationResolver> destinationResolver;
private final ObjectProvider<MessageConverter> messageConverter;
public JmsTemplateConfiguration(JmsProperties properties,
ObjectProvider<DestinationResolver> destinationResolver,
ObjectProvider<MessageConverter> messageConverter) {
this.properties = properties;
this.destinationResolver = destinationResolver;
this.messageConverter = messageConverter;
}
/**
* 配置生产者的JmsTemplate
* @param connectionFactory
* @return
*/
@Bean
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
jmsTemplate.setPubSubDomain(this.properties.isPubSubDomain());
DestinationResolver destinationResolver = (DestinationResolver) this.destinationResolver.getIfUnique();
if (destinationResolver != null) {
jmsTemplate.setDestinationResolver(destinationResolver);
}
MessageConverter messageConverter = (MessageConverter) this.messageConverter.getIfUnique();
if (messageConverter != null) {
jmsTemplate.setMessageConverter(messageConverter);
}
//deliveryMode, priority, timeToLive 的开关,要生效,必须配置为true,默认false
jmsTemplate.setExplicitQosEnabled(true);
//DeliveryMode.NON_PERSISTENT=1:非持久 ; DeliveryMode.PERSISTENT=2:持久
jmsTemplate.setDeliveryMode(DeliveryMode.PERSISTENT);
//默认不开启事务
System.out.println("默认是否开启事务:"+jmsTemplate.isSessionTransacted());
//如果不启用事务,则会导致XA事务失效;
//作为生产者如果需要支持事务,则需要配置SessionTransacted为true
jmsTemplate.setSessionTransacted(true);
return jmsTemplate;
}
}
2.3、将User保存和消息发送放入同一个事务方法:
2.3.1、定义一个P2P队列:
import javax.jms.Queue;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
@Configuration
@EnableJms
public class JmsBaseConfiguration {
/**
* 定义点对点队列
* @return
*/
@Bean
public Queue queue() {
return new ActiveMQQueue("my.queue");
}
}
2.3.2、优先定义user用户保存和消息发送在一个事务中
@Service("userService")
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Queue queue;
/**
* 测试分布式事务
* @param userList
* @return
*/
@Transactional(value="transactionManagerJTA",propagation=Propagation.REQUIRED,rollbackFor=Exception.class)
public List<User> bathSaveJTA(List<User> userList) throws Exception {
try{
//发送队列消息
this.jmsMessagingTemplate.convertAndSend(this.queue, "生产者辛苦生产的点对点消息成果");
System.out.println("生产者:辛苦生产的点对点消息成果");
List<User> list=new ArrayList<User>(10);
for(User user:userList){
list.add(userRepository.save(user));
}
// System.out.println(1/0);
return list;
}catch(Exception e){
throw new Exception("出现异常了");
}
}
}
2.4、单元测试:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes={AppProducer.class})//加载启动类
public class AppTest
{
@Autowired
private UserService userService;
@Test
public void testUserSave(){
try{
List<User> list=new ArrayList<User>(10);
list.add(new User("111","111"));
list.add(new User("223212","222"));
list.add(new User("333","333"));
userService.bathSaveJTA(list);
}catch(Exception e){
e.printStackTrace();
}
}
}
查看数据库变化:
在springboot1数据库中原始user数据
在activeMQ数据库中原始数据
activeMQ控制台
执行完成单元测试数据库和控制台没有任何数据变化,IDE控制台能够看到提示信息以及事务回滚:
提示信息
继续往上翻看控制台可以看到默认打印出来的JmsTemplate默认是不开启事务的,并且消息已经生产但由于事务回滚并没有发出
2.5、我们不采用新增用户触发异常事务回滚,如果我们的服务层仅仅处理消息发送呢,修改UserService代码如下:
/**
* 测试分布式事务
* @param userList
* @return
*/
@Transactional(value="transactionManagerJTA",propagation=Propagation.REQUIRED,rollbackFor=Exception.class)
public List<User> bathSaveJTA(List<User> userList) throws Exception {
try{
//发送队列消息
this.jmsMessagingTemplate.convertAndSend(this.queue, "生产者辛苦生产的点对点消息成果");
System.out.println("生产者:辛苦生产的点对点消息成果");
List<User> list=new ArrayList<User>(10);
// for(User user:userList){
// list.add(userRepository.save(user));
// }
System.out.println(1/0);
return list;
}catch(Exception e){
throw new Exception("出现异常了");
}
}
执行单元测试后我们也可以发现事务生效了,并没有消息发出,数据库也没有数据新增。
2.6、调整单元测试代码,提供一个可保存的正常数据,看看是否能够新增:
@Test
public void testUserSave(){
try{
List<User> list=new ArrayList<User>(10);
list.add(new User("111","111"));
list.add(new User("22312","222"));
list.add(new User("333","333"));
userService.bathSaveJTA(list);
}catch(Exception e){
e.printStackTrace();
}
}
activeMQ数据库表新增了一行数据
springboot1数据库中user表新增了三行数据
activeMQ控制台
综上,通过JtaTransactionManager验证完成上一章中待完成的“MQ本地事务、MQ与MySQL单应用分布式事务”内容。