Mysql事务隔离与Spring
Posted 不会写代码的丝丽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mysql事务隔离与Spring相关的知识,希望对你有一定的参考价值。
Spring学习事务的时候看到很多只读事务 和事务隔离和事务传播和事务挂起,为了更好的理解学习了mysql这块的知识。
本文用的SQL命令
#------ 设置事务隔离登记-----
#读已提交
set session transaction isolation level read committed ;
#读未提交
set session transaction isolation level read uncommitted ;
#可重复度 mysql默认级别
set session transaction isolation level repeatable read ;
#串行化
set session transaction isolation level serializable ;
#------ 开启事务-----
#开启可读写事务
start transaction;
#开启只读事务
start transaction read only;
#开启读写事务 与start transaction;等价
start transaction read write;
# 提交事务
commit;
# 事务回滚
rollback;
#------事务挂起-----
#记录事务保存点 mm为保存点自定义的id
savepoint mm;
#回滚事务到mm 保存点.直接运行rollback全部回滚
ROLLBACK TO mm;
#删除保存点
RELEASE SAVEPOINT mm;
# 获取事务隔离类 如 read committed等
SHOW VARIABLES LIKE 'transaction_isolation';
事务
事务个人定义:某一任务序列的集合。
举例:
小明在银行取钱的一个序列
上面5个步骤合在一起我们就称为一个事务。
在Mysql我们可以理解多个Sql语句的合集。
如下Sql:
# 小明给小张转账
update User set money=money-23 where username='小明';
update User set money=money+23 where username='小张';
上述两个sql语句根据固定顺序构成了一事务。
我们之所以把一定任务序列构成事务,是因为我们希望任务序列要么全部执行成功,要么不执行。
举个例子:
# 小明给小张转账
update User set money=money-23 where username='小明';
#假设只执行上面的小明的 money=money-23 但是还没执行
#小张的money=money+23语句,此时出现了异常不能执行(比如断电) money=money+23
#那么我们希望 money=money-23 语句就行回滚,就像没发生过一样,小明
#和小张金额一起没有任何变化
update User set money=money+23 where username='小张';
未解决上面的问题可以采用mysql 为我们提供事务语句解决
# 开启事务
start transaction ;
# start transaction read only; 只读事务 后续讲解
#事务开启后如果没有执行 commit 那么数据并不会真正写入数据库中,
# 但sql语句会生产临时数据(临时数据后面在引用问题,当然不会扩展到mvvc内容,主要我太菜)。
# 小明给小张转账
update User set money=money-23 where username='小明';
update User set money=money+23 where username='小张';
#提交事务
commit ;
#根据需要回滚
# rollback;
数据库只有一个,但是数据库操却并发着。比如12306有无数个请求(无数个事务)在修改数据库,那么这么多事务并发操作必然存在问题。
我们知道在事务中SQL语句在没有提交事务的时候,虽然不会最终写入数据库,但会产生临时数据,这些临时数据会被其他事务读取。
问题1: 脏读
事务读取到其他事务未提交的临时数据。
问题2: 不可重复读
事务两次读取同一记录的数据不一致。
问题3:幻读
事务A 修改/删除/更新 了一些在其他事务已经删除或者插入的数据
上图中某个事务没有id=2的数据(此时其他事务插入的),但是更新id=2为的数据居然成功。
参考:
维基百科 Isolation https://en.wikipedia.org/wiki/Isolation_(database_systems)
针对事务并发导致的几个问题,数据库innodb提供几个事务隔离策略让我们针对上诉的问题针对性解决。read committed、read uncommitted、 repeatable read、serializable。
read uncommitted 读未提交
此隔离模式会导致事务可以读取其他未提交的事务的临时数据。
假设B事务修改了User表的小李的Age=20字段 但未提交事务,此时A事务将读取到小李的Age字段等于20。
模拟环境如下:
A事务、 B事务。
表结构:
-- auto-generated definition
create table User
(
id int auto_increment
primary key,
username varchar(255) null,
birthday date null,
sex varchar(255) null,
address varchar(255) null,
money int null
);
A事务执行任务如下:
可见此隔离无法阻止脏读,同理可证明无法阻止幻读和不可重复度问题。
这在注意一个问题,事务B
此时对User表id为1的数据进行了行锁。其他事物如果需要修改id为1的数据需要事务B
提交事务才可。
read committed 读已提交
可以读取其他事务已提交的数据,对于其他未提交的事务的临时数据是无法查询。
此处可证明read committed
能解决数据脏读问题。但对于幻读
和可重复读
无法解决。
repeatable read 可重复读
多次重复读取可保持一致性
在事务A
修改id=1
的数据时会自动同步B事务提交id=1
新数据(假设事务B
同时修改id=2
和id=1
的数据,此时也不会刷新id=2
的新数据,只是刷新id=1
)
再次提一个小心点可重复读,修改一个数据时,此数据行若被其他事务修改,那么以其他事物已提交的数据为数据源做修改。
同理事务B
插入新的数据行,事务A
同样无法查询到。除非事务A
使用update
更新整个表。
但可重复读无法避免幻读现象
事务A中查询不到id=2
的数据,然后执行更新id=2
操作居然成功了
serializable 串行执行
一个事务只能等候另一个事务执行完成了才可以使用。
此隔离等级可解决幻读 重复读 脏读问题,但效率过差
只读事务
只读事务是指建立在四种隔离机制之上的。表示当前事务只能对表进行读取操作,其他删除/更新/插入会导致锁异常。
如执行以下下SQL语句
set session transaction isolation level read committed ;
start transaction read only ;
#不能执行
#insert User(id,username) values (2,'小帅');
#不能执行
#update User set username=concat(username,' 放学') ;
#不能执行
#delete from User;
#因为设置了 read committed ; 查询其他事务已经提交的数据,和原来无差别
select *
from User;
commit ;
start transaction read only ;
开启可读事务,mysql会对其进行优化处理 。但事务内部查询可见性依然由事务隔离级别决定。
事务的保存点
有时候我们希望在事务执行时可以控制部分区域回滚,可以让我们更细腻化的控制的。
其他命令这里直接给出结论读者可以自己进行验证。存在多个存储点时,执行rollback
将回滚全部,而rollback to XX
会回滚到指定点。RELEASE SAVEPOINT xx;
删除指定存储点位置 后面无法执行此id
的rollback to XX
。
Tip:Spring事务传播NESTED类型就是使用Savepoint
事务嵌套问题
事务开启后无法变更隔离级别,包含事务保存点savepoint
之后调用set session transaction isolation level xxxx ;
也无法更改
执行以下sql文件
set session transaction isolation level read uncommitted;
start transaction;
SHOW VARIABLES LIKE 'transaction_isolation';#返回read uncommitted
select *
from User;
#想修改为 读已提交
set session transaction isolation level read committed ;
SHOW VARIABLES LIKE 'transaction_isolation'; #虽然返回read committed 但是依然是read uncommitted;
#结果还是read uncommitted;
select *
from User;
#想修改为 读已提交
#并且在一次开始 依然没用
set session transaction isolation level read committed ;
start transaction;
#结果还是read uncommitted;
select *
from User;
commit;
效果图
Spring事务传播
事务类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前存在事务,那么使用当前事务,自身事务配置将 不生效,如果不存在使用自身配置信息创新事务 |
PROPAGATION_SUPPORTS | 如果当前存在事务,那么使用当前事务,如果没有那么以非事务运行 |
PROPAGATION_MANDATORY | 支持(使用)当前事务,如果不存在事务抛出异常 |
PROPAGATION_REQUIRES_NEW | 如果当前存在事务,则挂起当前事务,然后开启自身配置事务 |
PROPAGATION_NOT_SUPPORTED | 以非事务的方式运行,如果有事务存在,则挂起当前事务 |
PROPAGATION_NEVER | 已非事务的方式运行,如果有事务存在,则抛出异常 |
PROPAGATION_NESTED | 如果当前事务存在,则嵌套事务执行 使用savePoint(保存点)技术 |
介绍几个关键API:
DataSourceUtils.getConnection(dataSources)
获取当前线程的数据源的Connection对象
PROPAGATION_REQUIRED和PROPAGATION_REQUIRES_NEW
//TestTx.java
@Component
public class TestTx
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
DataSource dataSource;
/**
* 事务隔离级别为 读取已提交事务的数据
* 传播级别:PROPAGATION_REQUIRED 如果当前事务存在事务那么自身事务配置不生效,直接加入到现有事务中
* 如果没有事务那么使用自身的事务隔离级别创建事务
*/
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public void query()
List<User> query = jdbcTemplate.query("select * from User", new BeanPropertyRowMapper(User.class));
System.out.println("TestTx ::" + query);
print("TestTx.query", dataSource);
public void print(String prefix, DataSource dataSources)
try
System.out.println("\\n\\n" + prefix + " : \\n" +
"getTransactionIsolation : " + DataSourceUtils.getConnection(dataSources).getTransactionIsolation() +
"\\nconn : " + DataSourceUtils.getConnection(dataSources) + "\\n\\n");
catch (SQLException e)
e.printStackTrace();
//TestTx2.java
@Component
public class TestTx2
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
DataSource dataSource;
/**
* 事务隔离级别:读取已经提交事务的数据
* 传播:如果存在事务那么挂起,开启自己的新事务
*/
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
public void query()
List<User> query = jdbcTemplate.query("select * from User", new BeanPropertyRowMapper(User.class));
System.out.println("TestTx2::" + query);
print("TestTx2.query", dataSource);
public void print(String prefix, DataSource dataSources)
try
System.out.println("\\n\\n" + prefix + " : \\n" +
"getTransactionIsolation : " + DataSourceUtils.getConnection(dataSources).getTransactionIsolation() +
"\\nconn : " + DataSourceUtils.getConnection(dataSources) + "\\n\\n");
catch (SQLException e)
e.printStackTrace();
测试一
//TestBean.java
@Component
public class TestBean
@Autowired
TestTx testTx;
@Autowired
TestTx2 testTx2;
/**
* 开启事务 可以读取一些未提交的事务数据
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void testOne()
try
//#22行
testTx.query();
//#24行
testTx.query();
//#26行
testTx2.query();
catch (Exception e)
e.printStackTrace();
testOne方法输出:
TestTx ::[Userid=688022, userName='新插入未提交', birthday=null, sex='null', address='null', Userid=688024, userName='张三', birthday=null, sex='null', address='null', Userid=688025, userName='张三', birthday=null, sex='null', address='null']
TestTx.query :
getTransactionIsolation : 1
conn : com.mchange.v2.c3p0.impl.NewProxyConnection@7d32c62e [wrapping: com.mysql.cj.jdbc.ConnectionImpl@59ffa66d]
TestTx ::[Userid=688022, userName='新插入未提交', birthday=null, sex='null', address='null', Userid=688024, userName='张三', birthday=null, sex='null', address='null', Userid=688025, userName='张三', birthday=null, sex='null', address='null']
TestTx.query :
getTransactionIsolation : 1
conn : com.mchange.v2.c3p0.impl.NewProxyConnection@7d32c62e [wrapping: com.mysql.cj.jdbc.ConnectionImpl@59ffa66d]
TestTx2::[Userid=688024, userName='张三', birthday=null, sex='null', address='null', Userid=688025, userName='张三', birthday=null, sex='null', address='null']
TestTx2.query :
getTransactionIsolation : 2
conn : com.mchange.v2.c3p0.impl.NewProxyConnection@73f75fec [wrapping: com.mysql.cj.jdbc.ConnectionImpl@4d021f9e]
输出分析:
id=688022, userName='新插入未提交', birthday=null, sex='null', address='null'
为其他事务未提交的脏数据
结论一:
TestBean.java
的 22行
和24行
的testTx.query();
打印出相同的查询结果,且能读取到脏数据。证明自己的事务TestTx
的query
方法配置Isolation.READ_COMMITTED
没有生效,而是采用TestBean
类testOne
方法的READ_UNCOMMITTED
。
结论二:
TestBean.java
的 22行
和24行
的testTx.query();
打印出相同Connection
对象。加入其他事务的时候会使用同一Connection
对象。
结论三:
TestBean.java
的26行testTx2.query();
没有打印出id=688022, userName='新插入未提交', birthday=null, sex='null', address='null'
证明自身的Isolation.READ_COMMITTED
生效。
结论四:
TestBean.java
的26行testTx2.query();
打印的Connection
对象与 22行
和24行
的testTx.query();
不相同。证明不同一个事务Connection
对象不同.
测试二
@Component
public class TestBean
@Autowired
TestTx testTx;
@Autowired
TestTx2 testTx2;
/**
* 开启事务 可以读取一些未提交的事务数据
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void testOne()
try
testTx.query();
testTx.query();
testTx2.query();
catch (Exception e)
e.printStackTrace();
/**
* 与测试一不同的是 自身没有开启事务
*/
public void testTwo()
try
testTx.query();
testTx.query();
testTx2.query();
catch (Exception e)
e.printStackTrace();
testTwo输出:
TestTx ::[Userid=688024, userName='张三', birthday=null, sex='null', address='null', Userid=688025, userName='张三', birthday=null, sex='null', address='null']
TestTx.query :
getTransactionIsolation : 2
conn : com.mchange.v2.c3p0.impl.NewProxyConnection@7d32c62e [wrapping: com.mysql.cj.jdbc.ConnectionImpl@59ffa66d]
TestTx ::[Userid=688024, userName='张三', birthday=null, sex='null', address='null', Userid=688025, userName='张三', birthday=null, sex='null', address='null']
TestTx.query :
getTransactionIsolation : 2
conn : com.mchange.v2.c3p0.impl.NewProxyConnection@7577863f [wrapping: com.mysql.cj.jdbc.ConnectionImpl@73f75fec]
TestTx2::[Userid=688024, userName='张三', birthday=null, sex='null', address='null', Userid=688025, userName='张三', birthday=null, sex='null', address='null']
TestTx2.query :
getTransactionIsolation : 2
conn : com.mchange.v2.c3p0.impl.NewProxyConnection@2a4e842f [wrapping: com.mysql.cj.jdbc.ConnectionImpl@73f75fec]
结论一:
三者使用不同Connection
对象,且使用自己的隔离级别创建事务。
测试三
关于PROPAGATION_REQUIRES_NEW
会把原事务挂起,PROPAGATION_REQUIRED
会加入现有事务的测试说明。
@Component
public class TestTx
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
DataSource dataSource;
/**
* 添加用户
* 传播级别:PROPAGATION_REQUIRED 如果当前事务存在事务那么自身事务配置不生效(readonly=true也会失效 一样可以插入,
* isolation跟随当前事务 自身也失效,此处可以参考 之前查询测试),直接加入到现有事务中.
* 如果没有事务那么使用自身的事务隔离级别创建事务
*
* @param user
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRED, readOnly = true)
public void addUser(User user)
jdbcTemplate.update("insert into User( username) values (?) ", user.getUserName());
print("TestTx.addUser", dataSource);
@Transactional(isolation = Isolation.READ_UNCOMMITTED, propagation = Propagation.REQUIRED)
public void addUserWithExeption(User user)
jdbcTemplate.update("insert into User(username) values (?) ", user.getUserName());
print("TestTx.addUserWithExeption", dataSource);
int i = 1 / 0;
/**
* 事务隔离级别为 读取已提交事务的数据
* 传播级别:PROPAGATION_REQUIRED 如果当前事务存在事务那么自身事务配置不生效,直接加入到现有事务中
* 如果没有事务那么使用自身的事务隔离级别创建事务
*/
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public void query()
List<User> query = jdbcTemplate.query("select * from User", new BeanPropertyRowMapper(User.class));
System.out.println("TestTx ::" + query);
print("TestTx.query", dataSource);
public void print(String prefix, DataSource dataSources)
try
System.out.println("\\n\\n" + prefix + " : \\n" +
"getTransactionIsolation : " + DataSourceUtils.getConnection(dataSources).getTransactionIsolation() +
"\\nconn : " + DataSourceUtils.getConnection(dataSources) + "\\n\\n");
catch (SQLException e)
e.printStackTrace();
测试类
//TestBean.java
@Component
public class TestBean
@Autowired
TestTx testTx;
/**
* 与测试一不同的是 自身没有开启事务
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void testThree() throws Exception
testTx.addUser(new User("李四"));
testTx.addUserWithExeption(new User("王五"));
这里直接说结论 李四 王五都没有插入。两种使用同一个Connection
捕获回滚异常问题,请看下图
//TestB以上是关于Mysql事务隔离与Spring的主要内容,如果未能解决你的问题,请参考以下文章