涉及到与数据库交互就必须会用到事务,如果一个方法中需要用到事务的地方没有使用事务就会造成数据不一致的风险,进而导致比较严重的bug,比如扣款时,账户的余额已经进行了扣减但是相应的订单没有生成,这种涉及账目的问题如果不使用事务进行一致性控制后果会很严重。
在项目开发中,事务又可以分为单体事务和分布式事务,对于分布式系统要实现事务会比较复杂,有时候需要引入第三方系统控制一致性;而传统的单体应用就比较容易实现事务,尤其是使用到spring框架开发项目事务使用会更容易。
使用事务进行编程可以分为声明式事务和编程式事务,声明式事务只需要在需要事务控制的方法上面添加 @Transactional 注解就可以实现事务控制;编程式事务相对代码复杂一些,需要在代码中对异常进行捕获并控制事务的提交或回滚。下面就分别介绍一下如何在项目中使用这两种事务控制:
首先需要做一些准备工作:
- 引入相关依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.2</version>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version><scope>provided</scope>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.48</version>
</dependency>
- 在application.yml中添加数据库相关的配置信息:
server:port: 8080shutdown: gracefulspring:application:name: test-dbdatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8username: rootpassword: 123456%Testhikari:auto-commit: trueconnection-timeout: 30000idle-timeout: 30000minimum-idle: 1maximum-pool-size: 4max-lifetime: 1800000connection-test-query: SELECT A FROM T_POOLPING
- 在数据库中创建表用于测试事务时使用:
-- ----------------------------
-- 学生测试表
-- ----------------------------
CREATE TABLE `student` (`id` int NOT NULL AUTO_INCREMENT,`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,`score` smallint NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- 用户测试表
-- ----------------------------
CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT,`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,`age` smallint NULL DEFAULT NULL,`sex` tinyint NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- 连接测试表
-- ----------------------------
CREATE TABLE `T_POOLPING` (`A` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
- 创建两个实体类,分别是学生信息和用户信息:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @Author xingo* @Date 2024/1/30*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student {private int id;private String name;private int score;
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** @Author xingo* @Date 2024/1/30*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {private int id;private String name;private int age;private int sex;
}
- 定义两个Mapper用于插入学生信息和用户信息:
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;/*** @Author xingo* @Date 2024/1/30*/
@Mapper
public interface StudentMapper {@Insert("insert into student(name, score) values(#{name}, #{score})")boolean insertStudent(Student student);
}
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;/*** @Author xingo* @Date 2024/1/30*/
@Mapper
public interface UserMapper {@Insert("insert into user(name, age, sex) values(#{name}, #{age}, #{sex})")boolean insertUser(User user);
}
以上准备工作完成后演示几种事务的使用:
一、声明式事务
声明式事务使用比较简单,在需要事务控制的方法上面添加 @Transactional 注解就可以实现事务控制,在spring中事务控制是通过aop实现的,所以调用方法必须要通过代理请求才能实现事务控制,并且方法内的异常一定要抛出不能在方法内捕获,否则就不能实现事务控制。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** @Author xingo* @Date 2024/1/30*/
@RestController
public class DbController {@Autowiredprivate UserMapper userMapper;@Autowiredprivate StudentMapper studentMapper;@Transactional(rollbackFor = Exception.class)@GetMapping("/db/test1")public String testdb1(int num) {Student student = Student.builder().id(1).name("张三").score(100).build();User user = User.builder().id(1).name("张三").age(11).sex(1).build();userMapper.insertUser(user);System.out.println(1 / num);studentMapper.insertStudent(student);return "ok";}}
声明式事务使用非常方便,基本不需要额外的代码控制事务,但是它的控制粒度比较大,需要在方法上面进行控制,如果方法内部有比较耗时的操作将会导致事务不能提交,这会给数据库造成比较大的压力。要想让事务控制范围比较小的场景,就需要使用编程式事务实现:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.function.Consumer;/*** @Author xingo* @Date 2024/1/30*/
@RestController
public class DbController {@Autowiredprivate TransactionTemplate transactionTemplate;@Autowiredprivate UserMapper userMapper;@Autowiredprivate StudentMapper studentMapper;@GetMapping("/db/test2")public String testdb2(int num) {Student student = Student.builder().id(1).name("张三").score(100).build();User user = User.builder().id(1).name("张三").age(11).sex(1).build();// 有返回值的编程式事务transactionTemplate.execute(new TransactionCallback<Object>() {@Overridepublic Object doInTransaction(TransactionStatus status) {try {userMapper.insertUser(user);System.out.println(1 / num);studentMapper.insertStudent(student);return "ok";} catch (Exception e) {e.printStackTrace();status.setRollbackOnly();return null;}}});return "ok";}@GetMapping("/db/test3")public String testdb3(int num) {Student student = Student.builder().id(1).name("张三").score(100).build();User user = User.builder().id(1).name("张三").age(11).sex(1).build();// 没有返回值的编程式事务transactionTemplate.executeWithoutResult(new Consumer<TransactionStatus>() {@Overridepublic void accept(TransactionStatus status) {try {userMapper.insertUser(user);System.out.println(1 / num);studentMapper.insertStudent(student);} catch (Exception e) {e.printStackTrace();status.setRollbackOnly();}}});return "ok";}
}
上面使用 TransactionTemplate 是spring中建议使用的方式,也可以使用 TransactionManager 实现编程式事务,它对事务的实现更加灵活:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** @Author xingo* @Date 2024/1/30*/
@RestController
public class DbController {@Autowiredprivate PlatformTransactionManager transactionManager;@Autowiredprivate UserMapper userMapper;@Autowiredprivate StudentMapper studentMapper;@GetMapping("/db/test4")public String testdb4(int num) {Student student = Student.builder().id(1).name("张三").score(100).build();User user = User.builder().id(1).name("张三").age(11).sex(1).build();// 定义事务,并设置隔离级别DefaultTransactionDefinition definition = new DefaultTransactionDefinition();definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);// 获取事务状态信息TransactionStatus status = transactionManager.getTransaction(definition);try {userMapper.insertUser(user);System.out.println(1 / num);studentMapper.insertStudent(student);// 提交事务transactionManager.commit(status);} catch (Exception e) {e.printStackTrace();// 回滚事务transactionManager.rollback(status);}return "ok";}
}
上面这些都是spring中提供的事务处理方式,它底层都是通过aop进行处理,再结合其他orm框架让代码比较容易开发和维护,但是归根到底与数据库交互还是要使用jdbc实现事务控制,下面展示如果使用jdbc原生方式实现事务控制的代码要怎么写:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;/*** @Author xingo* @Date 2024/1/30*/
@RestController
public class DbController {@Autowiredprivate DataSource dataSource;@Autowiredprivate UserMapper userMapper;@Autowiredprivate StudentMapper studentMapper;@GetMapping("/db/test5")public String testdb5(int num) throws Exception {Student student = Student.builder().id(1).name("张三").score(100).build();User user = User.builder().id(1).name("张三").age(11).sex(1).build();Connection conn = dataSource.getConnection();try {conn.setAutoCommit(false);PreparedStatement pstmt1 = conn.prepareStatement("insert into user(name, age, sex) values(?, ?, ?)");pstmt1.setString(1, user.getName());pstmt1.setInt(2, user.getAge());pstmt1.setInt(3, user.getSex());pstmt1.executeUpdate();System.out.println(1 / num);PreparedStatement pstmt2 = conn.prepareStatement("insert into student(name, score) values(?, ?)");pstmt2.setString(1, student.getName());pstmt2.setInt(2, student.getScore());pstmt2.executeUpdate();conn.commit();} catch (Exception e) {e.printStackTrace();conn.rollback();} finally {conn.setAutoCommit(true);conn.close();}return "ok";}
}
上面示例的所有接口可以模拟正常请求和异常请求两种方式,通过模拟产生除数为0的异常来验证事务失效的场景:
# 正常请求
http://localhost:8080/db/test1?num=1# 异常请求
http://localhost:8080/db/test1?num=0