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';

事务

事务个人定义:某一任务序列的集合。

举例:
小明在银行取钱的一个序列

小明 插卡 输入密码 取款金额 ATM吐纸钞退卡

上面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=2id=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; 删除指定存储点位置 后面无法执行此idrollback 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.java22行24行testTx.query();打印出相同的查询结果,且能读取到脏数据。证明自己的事务TestTxquery方法配置Isolation.READ_COMMITTED没有生效,而是采用TestBeantestOne方法的READ_UNCOMMITTED

结论二:
TestBean.java22行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的主要内容,如果未能解决你的问题,请参考以下文章

初识事务,事务隔离级别,事务传播行为

Spring 事务类型与隔离级别

事务与Mysql隔离级别

MySQL事务

深入剖析Mysql事务和Spring事务

MySQL事务以及隔离级别