Spring:五、编程式事务
1 前言
spring支持声明式和编程式事务,因spring事务基于AOP,使用cglib作为代理,为父子类继承的代理模式,故而声明式事务@Transactional中,常见事务失效的场景,如方法内自调用(this.xxx的this不是代理对象)、方法修饰private(代理子类无法调用父类的private方法)、方法修饰final(因final修饰的方法,子类可以继承和重载,但无法重写)、类没有被spring管理等等,避免此类易被忽略而导致事务失效的问题,更推荐使用编程式事务。
spring官方文档:
https://docs.spring.io/spring-framework/docs/5.3.25/reference/html/data-access.html#transaction-programmatic
2 使用
依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- spring连接驱动时,如com.mysql.cj.jdbc.Driver使用 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
</dependencies>
启动类:
@MapperScan(basePackages = "com.xiaoxu.boot.mapper")
@SpringBootApplication(scanBasePackages = "com.xiaoxu")
@ImportResource(locations = {"classpath*:pool/*.xml"})
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}
PeopleMapper:
package com.xiaoxu.boot.mapper;
import com.xiaoxu.boot.dto.PeopleDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
//@Mapper
public interface PeopleMapper {
List<PeopleDTO> queryPeopleByAge(int age);
@Select("select * from my_people")
List<PeopleDTO> queryAllPeople();
int updatePeopleById(@Param("id") long id, @Param("myAge") String my_age);
}
PeopleDaoMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxu.boot.mapper.PeopleMapper">
<select id="queryPeopleByAge" resultType="com.xiaoxu.boot.dto.PeopleDTO">
select * from my_people where my_age = #{age}
</select>
<update id="updatePeopleById">
update my_people
<set>
<if test="myAge != null">
my_age = #{myAge}
</if>
</set>
<where>
id = #{id}
</where>
</update>
</mapper>
PeopleService:
@Service
public class PeopleService {
@Autowired
PeopleMapper peopleMapper;
public List<PeopleDTO> getPeoples(int Age){
return peopleMapper.queryPeopleByAge(Age);
}
public List<PeopleDTO> getAllPeople(){
return peopleMapper.queryAllPeople();
}
public long updatePeopleAgeById(String age, int id){
return peopleMapper.updatePeopleById(id, age);
}
}
druid.properties:
druid.driverClassName = com.mysql.cj.jdbc.Driver
druid.url = jdbc:mysql://localhost:3306/xiaoxu?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
druid.userName = root
druid.password = ******
DataSource.xml(配置TransactionTemplate bean,用于编程式事务):
<?xml version="1.0" encoding="UTF-8"?>
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath*:druid.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${druid.driverClassName}"/>
<property name="url" value="${druid.url}"/>
<property name="username" value="${druid.userName}"/>
<property name="password" value="${druid.password}"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager">
<ref bean="transactionManager"/>
</property>
</bean>
</beans>
AbstractTest:
package mybatis;
import com.xiaoxu.boot.MainApplication;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author xiaoxu
* @date 2023-02-03
* spring_boot:mybatis.AbstractTest
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MainApplication.class)
public abstract class AbstractTest {
}
单测类:
public class TestUserQuery extends AbstractTest{
@Autowired
PeopleService peopleService;
@Autowired
TransactionTemplate template;
@Test
public void test_01(){
template.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
List<PeopleDTO> allPeople = peopleService.getAllPeople();
allPeople.forEach(System.out::println);
System.out.println("first over");
List<PeopleDTO> allPeoples = peopleService.getAllPeople();
allPeoples.forEach(System.out::println);
System.out.println("second over");
long l = peopleService.updatePeopleAgeById("16", 1);
System.out.println("更新结果:" + l);
List<PeopleDTO> allPeople1 = peopleService.getAllPeople();
allPeople1.forEach(System.out::println);
System.out.println("third over");
String a = "1";
if(a.equals("1")){
// throw new RuntimeException("1212");
transactionStatus.setRollbackOnly();
}
return 0;
}
});
}
}
执行前于application.yml增加sql日志打印:
#mybatis的相关配置
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
# #mybatis配置文件
# config-location: classpath:mybatis-config.xml
# config-location和configuration不能同时存在
#开启驼峰命名
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
执行结果如下:
同一个事务中,mybatis的sqlSession是同一个(mybatis底层使用JDK动态代理,执行比如selectList方法时,如果上下文中开启了事务,那么sqlSession是同一个对象。而mybatis的1级缓存,在BaseExecutor中的PerpetualCache localCache中,是sqlSession维度的,即同一个sqlSession同享1级缓存),故而一个事务中多次查询,因使用的是同一个sqlSession,又因为mybatis的一级缓存是同一个sqlSession共用,故而连续两次查询一定得到的是同样的结果。如果第一次查询后有更新、插入、删除操作,那么mybatis一级缓存将会刷新。
故而上述第二次查询时,没有打印sql日志,取的数据为1级缓存中的数据。而后执行更新(或插入、删除)后,再次查询,此时打印查询sql日志。
另在TransactionTemplate事务执行中,由execute方法源码可知,默认捕获RuntimeException、Error后,执行事务回滚,当然,亦可同上述操作,在事务处理的代码逻辑捕获异常后,手动执行transactionStatus.setRollbackOnly(),亦可使事务回滚。
3 事务执行拓展
事务执行中,常见问题是,一般不能在事务执行中,执行非事务型操作,如非事务型的消息、rpc调用等等。因为若数据库事务回滚,但是消息已经发送(或rpc调用已造成影响),会造成数据不一致问题。若希望事务执行完成后,再执行部分操作如消息发送等,可以尝试如下拓展。
class DoTrans implements TransactionSynchronization{
private final Runnable runnable;
public DoTrans(Runnable runnable) {
this.runnable = runnable;
}
@Override
public void afterCompletion(int status) {
if(status == STATUS_COMMITTED){
/* 0 */
System.out.println("事务已提交");
this.runnable.run();
}else if(status == STATUS_ROLLED_BACK){
/* 1 */
System.out.println("事务已回滚, 不做处理.");
}else if(status == STATUS_UNKNOWN){
/* 2 */
System.out.println("未知状态, 不做处理.");
}
}
}
单测方法:
@Test
public void test_02(){
Runnable runnable = () -> {
System.out.println("事务已执行, 发送消息.");
};
template.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
if(TransactionSynchronizationManager.isActualTransactionActive()){
System.out.println("事务已开启.");
TransactionSynchronizationManager.registerSynchronization(new DoTrans(runnable));
}
List<PeopleDTO> allPeople = peopleService.getAllPeople();
allPeople.forEach(System.out::println);
System.out.println("first over");
List<PeopleDTO> allPeoples = peopleService.getAllPeople();
allPeoples.forEach(System.out::println);
System.out.println("second over");
// String a = "1";
// if(a.equals("1")){
// transactionStatus.setRollbackOnly();
// }
return 0;
}
});
}
执行结果如下:
TransactionSynchronization 源码:
public interface TransactionSynchronization extends Ordered, Flushable {
int STATUS_COMMITTED = 0;
int STATUS_ROLLED_BACK = 1;
int STATUS_UNKNOWN = 2;
default int getOrder() {
return 2147483647;
}
default void suspend() {
}
default void resume() {
}
default void flush() {
}
default void beforeCommit(boolean readOnly) {
}
default void beforeCompletion() {
}
default void afterCommit() {
}
default void afterCompletion(int status) {
}
}
源码中可见,afterCompletion会在事务执行后,执行对应的trigger方法进行调用。另可注册多个TransactionSynchronization对象,因实现了Ordered接口,亦可指定其执行的顺序。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/192090.html