文章目录
- 前言
- 一、事务是什么?
- 二、事务的特性
- 2.1隔离性
- 2.2事务的隔离级别
- 三、@Transactional注解
- @Transactional注解简介
- 基本用法
- 常用属性配置
- 事务传播行为
- 事务隔离级别
- 异常处理与回滚
- 性能优化建议
- 四、 事务不生效的可能原因
- 方法访问权限非public
- 自调用问题
- 异常被捕获未抛出
- 数据库引擎不支持事务
- 未启用事务管理
- 特殊场景:final/static方法
- 五、分布式事务考虑
- 总结
前言
在开发过程中,遇到多个数据库操作的时候往往只知道加上@transactional注解(spring框架)但是没有系统学习数据库的事务知识以及各种用法,本文会举例介绍数据库中事务的概念以及用法
一、事务是什么?
在实际的项目开发过程中会涉及很多不可分割的数据库操作,如转账业务的进账和出帐,要么全部执行成功,要么全部失败回滚。这样一些的对数据库操作的集合就叫事务。现在假设数据库中有一个表accounts
balance | user_id |
---|---|
1000 | 1 |
100 | 2 |
在数据库中执行一段事务的sql如下:
-- 显式事务示例
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT; -- 或 ROLLBACK 回滚
很多人会觉得这里的COMMIT或者ROLLBACK之前,数据库操作只是在内存中,并没有更改表中的数据,需要注意的是,这里的COMMIT或者ROLLBACK 之前,数据已经是持久化了,也就是写入了磁盘(数据库表中)。
二、事务的特性
根据对事务的理解,可以归纳出事务应该具有的特性
原子性(Atomicity):事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚。
一致性(Consistency):事务执行前后,数据库从一个一致状态转变为另一个一致状态。
隔离性(Isolation):多个事务并发执行时,一个事务的操作不应影响其他事务。
持久性(Durability):事务提交后,其对数据的修改是永久性的。
2.1隔离性
在上述特性中比较难理解的应该是隔离性,可以想象这样一个场景:
因为网络延迟或者条件异常,事务A未提交还没来得及回滚
-- 事务A
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
事务B开始查询事务,查询的结果就是balance = balance - 100
-- 事务B
BEGIN TRANSACTION;
select balance WHERE user_id = 1 from accounts;
commit;
事务A回滚,此刻a,b两个事务间就发生了干扰,a的事务的提交影响了b事务对数据库的正常读取。
-- 事务A
ROLLBACK ;--回滚
所以隔离性就是指的的是a事务对与b事务的互不影响的程度。
2.2事务的隔离级别
事务的隔离级别也可以叫做事务的互不影响程度的级别。
读未提交(Read Uncommitted):影响程度最高,即使事务没有commit,另外的事务也可以读到,可能导致脏读。
-- ----------------------
-- 窗口1(事务A)
-- ----------------------
-- 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;-- 修改A账户余额(未提交)
UPDATE account SET balance = 800 WHERE name = 'A账户';
SELECT * FROM account; -- 查看修改后结果:A账户800-- ----------------------
-- 窗口2(事务B)
-- ----------------------
-- 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;-- 查询A账户余额(读到未提交的数据)
SELECT * FROM account; -- 结果:A账户800(脏读)-- 窗口1(事务A)回滚
ROLLBACK;-- 窗口2再次查询
SELECT * FROM account; -- 结果:A账户恢复1000(脏读数据消失)
读已提交(Read Committed):只能读取已提交的数据,避免脏读但可能出现不可重复读(一个事务内的多次查询结果不一致)。即b开启事务
进行第一次查询,此时a事务开启事务,a更改完数据后,事务提交,b开启第二次查询,查询结果不一致。
-- ----------------------
-- 窗口1(事务A)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION; --设置读已提交的隔离级别-- 修改A账户余额并提交
UPDATE account SET balance = 800 WHERE name = 'A账户';
COMMIT; -- 提交事务-- ----------------------
-- 窗口2(事务B)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;-- 第一次查询(事务A未提交时)
SELECT * FROM account; -- 结果:A账户1000-- 此时事务A已提交...-- 第二次查询(事务A已提交)
SELECT * FROM account; -- 结果:A账户800(不可重复读)ROLLBACK;
可重复读(Repeatable Read):确保同一事务多次读取结果一致(相当于事务开始时候进行了一次数据库快照,事务内的查询的是查询开启事务时刻的数据库快照的数据),避免不可重复读但可能出现幻读(同一事务内多次查询返回不同行数)。
-- ----------------------
-- 窗口1(事务A)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;-- 修改A账户余额并提交
UPDATE account SET balance = 800 WHERE name = 'A账户';
COMMIT;-- ----------------------
-- 窗口2(事务B)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;-- 第一次查询
SELECT * FROM account; -- 结果:A账户1000-- 此时事务A已提交...-- 第二次查询(结果与第一次一致)
SELECT * FROM account; -- 结果:A账户1000(可重复读)-- 提交事务B后,才能看到A的修改
COMMIT;
SELECT * FROM account; -- 结果:A账户800
这里需要强调下为什么会出现幻读同一事务内多次查询返回不同行数)。
虽然不可重复读读的是快照读,但是使用update/insert/delete等时,是会先当前读,也就是说的是已提交的数据,此刻的快照读会更新为当前使用update/insert/delete等时数据库的快照,所以再之后的普通读select中的快照和最开始的快照的版本是不一样的了,读的数据也就不一样了。幻读例子如下:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;-- 插入新记录
INSERT INTO account(name, balance) VALUES ('C账户', 500);
COMMIT;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;-- 第一次查询(返回2条记录)
SELECT COUNT(*) FROM account; -- 此时事务A插入新记录并提交...-- 第二次查询(仍返回2条记录)
SELECT COUNT(*) FROM account; -- 执行更新操作后再次查询(出现幻读)
UPDATE account SET balance = balance+100 WHERE name = 'A账户';
SELECT COUNT(*) FROM account; -- 返回3条记录
COMMIT;
串行化(Serializable):最高隔离级别,完全禁止并发问题,所有事务完全同步性能最低(按照一定的顺序执行)。事务A插入数据,事务B在查询时被阻塞,避免幻读。
-- 事务A
-- ----------------------
-- 窗口1(事务A)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;-- 查询当前数据
SELECT * FROM account; -- 结果:A账户1000,B账户1000-- ----------------------
-- 窗口2(事务B)
-- ----------------------
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;-- 插入新数据(会被事务A阻塞,直到A提交或回滚)
INSERT INTO account (name, balance) VALUES ('C账户', 1000);-- 窗口1提交事务
COMMIT;-- 窗口2的插入操作继续执行(此时事务A已提交)
-- 提交后,窗口1再次查询会看到新数据(幻读解决)
三、@Transactional注解
@Transactional注解简介
@Transactional
是Spring框架提供的声明式事务管理注解,用于简化数据库事务操作。通过在方法或类上添加该注解,可以自动管理事务的开启、提交、回滚等操作,无需手动编写事务代码。
基本用法
在Spring Boot项目中,需要在启动类或配置类上添加@EnableTransactionManagement
以启用事务管理功能。随后可以在服务层方法上使用@Transactional
注解:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Transactionalpublic void createUser(User user) {userRepository.save(user);}
}
常用属性配置
@Transactional
提供多个属性用于定制事务行为:
#使用注解时候默认配置如下
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout = -1,readOnly = false,rollbackFor = {RuntimeException.class, Error.class}
)
自定义注解配置
@Transactional(propagation = Propagation.REQUIRED, --事务传播行为isolation = Isolation.DEFAULT, --隔离级别,数据库默认不可重读timeout = 30, --定义超时时间,超过自动给回滚readOnly = false, --不只读,可修改rollbackFor = {SQLException.class}, --遇到异常捕获后必须回滚noRollbackFor = {NullPointerException.class} --遇到NullPointerException异常时不回滚事务
)
public void updateUser(User user) {// 业务逻辑
}
事务传播行为
Spring定义了7种事务传播行为,常用选项包括:
REQUIRED
:默认值,当前有事务则加入,没有则新建REQUIRES_NEW
:总是新建事务,暂停当前事务NESTED
:在当前事务中嵌套子事务SUPPORTS
:有事务则加入,没有则以非事务方式执行NOT_SUPPORTED
:以非事务方式执行,暂停当前事务MANDATORY
:必须在事务中调用,否则抛出异常NEVER
:不能在事务中调用,否则抛出异常
事务隔离级别
隔离级别控制事务间的可见性:
DEFAULT
:使用数据库默认级别READ_UNCOMMITTED
:读未提交READ_COMMITTED
:读已提交REPEATABLE_READ
:可重复读SERIALIZABLE
:串行化
异常处理与回滚
默认只在运行时异常和Error时回滚,可通过以下属性调整:
rollbackFor
:指定触发回滚的异常类型noRollbackFor
:指定不触发回滚的异常类型rollbackForClassName
:通过类名指定回滚异常noRollbackForClassName
:通过类名指定不回滚异常
性能优化建议
对于只读操作,建议明确设置readOnly=true
:
@Transactional(readOnly = true)
public User getUser(Long id) {return userRepository.findById(id);
}
避免在事务方法中进行远程调用或耗时操作,防止事务长时间占用连接。
四、 事务不生效的可能原因
在很多面试题会问事务不生效的原因, @Transactional 是基于Spring AOP实现的事务切面,所以失效本质上可以归因于AOP代理未生效或切面逻辑被阻断。Spring通过 @EnableTransactionManagement 开启事务支持,底层使用 TransactionInterceptor 拦截目标方法,生成代理对象(JDK动态代理或CGLIB代理)。只有通过代理对象调用方法时,事务切面才会生效;若直接调用目标对象(如 this.方法() ),会绕过代理,导致事务失效。
方法访问权限非public
Spring事务代理要求目标方法必须是public,若定义为private/protected/default,代理无法拦截方法调用。
@Transactional // 失效
private void saveData() {// 操作数据库
}
自调用问题
同类中非事务方法调用事务方法,因直接调用this而非代理对象,导致拦截失效。
public class OrderService {public void createOrder() {this.saveOrder(); // 自调用,事务失效}@Transactionalpublic void saveOrder() {// 保存订单}
}
异常被捕获未抛出
spring默认仅对RuntimeException和Error回滚,若捕获异常且未重新抛出,事务管理器无法感知异常。示例:
@Transactional
public void update() {try {jdbcTemplate.update("...");} catch (DataAccessException e) {// 捕获后未抛出,事务不会回滚}
}
通过@Transactional注解指定需回滚的异常:
@Transactional(rollbackFor = IOException.class)
将受检异常转换为事务管理器可识别的类型:
catch (IOException e) {throw new RuntimeException("Wrapped exception", e);
}
数据库引擎不支持事务
如MySQL的MyISAM引擎不支持事务,需改用InnoDB。配置示例:
CREATE TABLE test (id INT PRIMARY KEY
) ENGINE=InnoDB; // 必须为InnoDB
未启用事务管理
Spring Boot需配置@EnableTransactionManagement(默认自动开启),传统项目遗漏此注解会导致事务失效。示例缺失:
@SpringBootApplication
// 若手动配置需添加@EnableTransactionManagement
public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);}
}
特殊场景:final/static方法
代理无法增强final/static方法,导致事务失效。示例:
@Transactional
public final void process() { // final方法失效// 业务逻辑
}
五、分布式事务考虑
对于跨服务的事务操作,@Transactional
只能管理本地事务。分布式场景可考虑:
- Seata框架
- 消息队列最终一致性
- TCC模式
- SAGA模式
总结
在开发过程中遇到过在同一条件下查询出不同的数据结果,最后发现是对事务的理解使用欠缺导致。这个问题在开发过程中通过还挺常见的,不仅仅是在面对事务的时候加个@Transactional注解就完事了。
本文梳理了数据库事务的基本概念和隔离级别,同时对spring中的事务框架做了简单的介绍,觉得有用请点下赞吧。