MySQL-事务
事务是指满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
基本概念
事务特性
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库的数据改变就是永久的。
事务隔离级别
隔离级别 | 描述 | 脏读(不同事务下,当前事务可以读取到另外事务未提交的数据) | 不可重复读(同一事务内多次读取同一数据集合,读取到的数据是不一样) | 幻读(同一事务下,连续执行两次同样的 sql 语句可能返回不同的结果,第二次的 sql 语句可能会返回之前不存在的行。幻读是一种特殊的不可重复读问题。) |
---|---|---|---|---|
未提交读 READ UNCOMMITTED | 事务中的修改,即使没有提交,对其他事务也是可见的 | √ | √ | √ |
提交读 READ COMMITTED | 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其他事务是不可见的 | × | √ | √ |
可重复读 REPEATABLE READ | 保证在同一个事务中多次读取同样数据的结果是一样的 | × | × | √ |
可串行化 SERIALIZABLE | 强制事务串行执行 | × | × | × |
事务处理日志
redo log
ACID 的持久性是通过redo log 来保证的。
重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,而后者在磁盘中当事务提交之后会把所有修改信息都存到该日志文件中,用于刷新脏页到磁盘,发生错误时,进行数据恢复使用。
undo log
回滚日志,用于记录数据被修改前的信息,作用包含2个:提供回滚和MVCC(多版本并发控制)。
undo log 和 redo log 记录物理日志不一样,它是逻辑日志。可以认为当delete 一条记录时,undo log 中会记录一条对应的insert 的记录,当update 一条记录时,它记录一条相对应相反的update 记录。当执行rollback 时,就可以从undo log 中逻辑记录读取到相应的内容并进行回滚。
undo log 销毁:undo log 在事务执行时,并不会立即删除undo log ,因为这些日志可能还用于MVCC 。
undo log 存储:undo log 采用段的方式进行管理记录。存放在rollback segment 回滚段中,内部包含了1024个undo log segment 。
undo log 保证事务的原子性,而undo log + redo log 保证事务的一致性。
MVCC多版本并发控制
当前读
读取的是当前记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如:select lock in share mode
,select for update
,update
,insert
,update
,delete
(排他锁)都是一种当前读。
快照读
简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- RC 每次select,都生成快照读(因为每次都生成快照,所以会读到其他事务提交)。
- RR 开启事务第一个select 语句才是快照读。
- Serializable 快照读会退化为当前读。
MVCC隐藏字段
Mulit-Version Concurrency Control 多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为sql 实现的MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、unlog 日志 readview。
InnoDB 在创建表的时候会多创建2个字段,分别是DB_TRX_ID
,DB_ROLL_PTR
,。DB_ROW_ID
隐式字段 | 含义 |
---|---|
DB_TRX_ID | 最近修改事务ID,记录插入这条记录或者最后修改该记录的事务ID |
DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log ,指向上一个版本 |
DB_ROW_ID | 隐藏主键,如果表结果没有指定主键,将会生成该隐藏字段。 |
undo log 版本链
回滚日志,在insert update delete 的时候产生的便于数据回滚的日志。
当insert 的时候,产生的undo log 日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete 的时候产生的undo log 日志不仅在回滚是需要,在快照读时也需要,不会被立即删除。
不同事务或者相同事务对同一条记录进行修改,会导致该记录的undo log 生成一条记录版本链表,链表的头部是最新的旧记录,尾部是最旧的记录。
读视图
读视图是快照读SQL 执行时MVCC 提取数据的一句,记录并维护系统当前活跃的事务(未提交的)ID
read view 包括了四个核心字段:
字段 | 含义 |
---|---|
m_ids | 当前活跃的事务ID集合 |
min_trx_id | 最小活跃事务ID |
max_trx_id | 预分配事务ID,当前最大事务ID+1(事务ID是自增的) |
creator_trx_id | Read View 创建者的事务ID |
$$ 版本链数据访问规则 \begin
- trx—id=creator—trx—id, &可以访问该版本 (当前事务更改的)\
- trx—id < min—trx—id, &可以访问该版本(数据已经提交)\
- trx—id > max—trx—id, &不可以访问该版本(当前事务是在ReadView 生成之后开启的)\
- min—trx—id \le trx—id \le max—trx—id, &如果trx—id不在m-ids中是可以访问该版本的(事务已经提交) \end{cases} $$
不同的事务隔离级别,生成的readview 的时机不同。
RC 每次都生成 readview ,RR 只在事务第一次执行快照读的时候生成readview,后续复用该readview。
原理分析(RC)
RC 每次都生成 readview 。
原理分析(RR)
事务在第一次执行快照读时生成ReadView,后续复用该ReadView。
MVCC 主要还是通过隐藏字段(事务id,回滚指针)、undo log 版本链,readview 实现,MVCC + 锁保证了事务的隔离性。
事务失效场景
1.访问权限问题
java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
spring要求被代理方法必须是public
的
AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则TransactionAttribute
返回null,即不支持事务。
2. 方法用final修饰
事务方法定义成final会导致事务失效
spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
3.方法内部调用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
解决方案:通过AopContent类
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
4.未被spring管理
使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
5.多线程调用
在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
6.表不支持事务
myisam不支持事务
innodb支持事务
7.错误的传播特性
使用@Transactional
注解时,是可以指定propagation
参数的。
该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:
REQUIRED
如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。SUPPORTS
如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。MANDATORY
如果当前上下文中存在事务,否则抛出异常。REQUIRES_NEW
每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。NOT_SUPPORTED
如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。NEVER
如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。NESTED
如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
注解@Transactional
中Propagation属性值设置错误
只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
8.未抛出异常
业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常
9.嵌套事务回滚超出预期
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。
因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
解决方案:将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
// 嵌套事务 try/catch
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
10.大事务问题
1.用编程式事务处理业务
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
TransactionTemplate,在它的execute方法中,就实现了事务的功能
相较于@Transactional
注解声明式事务,我更建议大家使用,基于TransactionTemplate
的编程式事务。主要原因如下:
- 避免由于spring aop问题,导致事务失效的问题。
- 能够更小粒度的控制事务的范围,更直观。
2.将查询方法放到事务外
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
@Servcie
publicclass ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
3.事务中避免远程调用
4.事务中避免一次性处理太多数据
分页处理,1000条数据,分50页,一次只处理20条数据,这样可以大大减少大事务的出现。