03.数据库开发 - 第4章.事务

事务原理与开发

事务transaction:

           什么是事务?

           事务是并发控制的基本单位,指作为单个逻辑工作单元执行的一系列操作,且逻辑工作单元需满足ACID特性。

           i.e. 银行转账:开始交易;张三账户扣除100元;李四账户增加100元;结束交易。

事务的特性:ACID

           原子性Atomicity: 整个交易必须作为一个整体来执行。(要么全部执行,要么全部不执行)

           一致性Consistency: 整个交易总体资金不变

           隔离性Isolation:

                     case1: 若张三给李四转账过程中,赵五给张三转账了200元。两个交易并发执行。

                               

                     case2:脏读:张三给别人转账100元之后张三存钱200元,存钱后转账由于系统原因失败回滚。读取一个事务未提交的更新。

T1

T2

读取张三余额100

 

(转账)更新张三余额0

读取张三余额0

T1 Roolback()

(存钱)更新张三余额200

 

结束(余额200)

                     case3:不可重复读:同一个事务,两次读取同一数值的结果不同,成为不可重复读。

                                T1张三读取自己余额为100;T2读取张三余额100;T2存钱更新为300;T1读取余额为300。T1中两次读取张三余额即为不可重复读。

                     case4:幻读:两次读取的结果包含的行记录不一样。

                                T1读取所有用户(张三、李四);T2新增用户赵五;T1读取所有用户3个;T1/T2结束。T1中两次读取的结果中行记录数不同,成为幻读。

                     需要避免上述cases的产生

                     隔离性:交易之间相互隔离,在一个交易完成之前,不能受到其他交易的影响

           持久性 Durability: 整个交易过程一旦结束,无论出现任何情况,交易都应该是永久生效的

使用JDBC进行事务控制:

           Connection类中

           .setAutoCommit(): 开启事务(若为false,则该Connection对象后续的sql都将作为事务来处理;若为true,则该Connection对象后续的所有sql都将作为单独的语句执行(默认为ture))

           .commit(): 事务被提交,即事务生效并结束

           .rollback(): 回滚,回退到事务开始之前的状态

           i.e.

          

实现ZhangSi(1)给LiSan(2)转账的过程:

           (非事务:)

public static void TransferNonTransaction() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
        String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
        // transfer 100 from ZhangSi(1) to LiSan(2)
        ptmt = conn.prepareStatement(sql);
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSi");
        ptmt.setInt(3, 1);
        ptmt.execute();

        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSan");
        ptmt.setInt(3, 2);
        ptmt.execute();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

                     执行完第一个ptmt.execute()后,数据库中ZhangSi的Account=0,LiSan的Account=0;

                     出现了一个中间状态,对于整个业务逻辑的实现是不可接受的。如果此时程序崩溃了将不可挽回。

          

           (事务:)

public static void TransferByTransaction() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
        
        // Using Transaction mechanism
        conn.setAutoCommit(false);
        String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
        ptmt = conn.prepareStatement(sql);
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSi");
        ptmt.setInt(3, 1);
        ptmt.execute();

        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSan");
        ptmt.setInt(3, 2);
        ptmt.execute();
        
        // Commit the transaction
        conn.commit();
        
    } catch (SQLException e) {
        // if something wrong happens, rolling back
        if(conn != null) {
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

           若在第一个ptmpt.execute()时断点,并查询数据库,结果为事务执行之前的状态,并不是中间状态。

           直到conn.commit()方法执行完毕,事务中的所有操作在数据库中才有效。

Connection类中的检查点功能:

           .setSavePoint(): 在执行过程中创建保存点,以便rollback()可以回滚到该保存点

           .rollback(SavePoint savePoint): 回滚到某个检查点

           i.e.

public static void rollbackTest() {
    Connection conn = null;
    PreparedStatement ptmt = null;
    // save point
    Savepoint sp = null;
    try {
        conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
        
        conn.setAutoCommit(false);
        String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
        ptmt = conn.prepareStatement(sql);
        ptmt.setInt(1, 0);
        ptmt.setString(2, "ZhangSi");
        ptmt.setInt(3, 1);
        ptmt.execute();
        // create a save point
        sp = conn.setSavepoint();

        ptmt.setInt(1, 100);
        ptmt.setString(2, "LiSan");
        ptmt.setInt(3, 2);
        ptmt.execute();

        // throw an exception manually for the purpose of testing
        throw new SQLException();
        
    } catch (SQLException e) {
        // if something wrong happens, rolling back to the save point created before
        // and then transfer the money to Guoyi(3)
        if(conn != null) {
            try {
                conn.rollback(sp);
                System.out.println("Transfer from ZhangSi(1) to LiSan(2) failed;\n"
                        + "Transfer to GuoYi(3) instead");
                
                // other operations
                ptmt.setInt(1, 100);
                ptmt.setString(2, "GuoYi");
                ptmt.setInt(3, 3);
                ptmt.executeQuery();
                conn.commit();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }                     
        }
        
        e.printStackTrace();
    } finally {
        try {
            if (conn != null) conn.close();
            if (ptmt != null) ptmt.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

 

事务的隔离级别:4个级别

           读未提交(read uncommited):可能导致脏读

           读提交(read commited): 不可能脏读,但是会出现不可重复读

           重复读(repeatable read):不会出现不可重复读,但是会出现幻读

           串行化(serializable):最高级别隔离,不会出现幻读,但是严格的并发控制、串行执行导致数据库性能差

           N.B: 事务隔离级别越高,数据库性能越差,但是对开发者而言编程难度越低

                     MySQL默认事务隔离级别为重复读repeatable read

 

JDBC设置隔离级别

           connection对象中,

           .getTransactionIsolation();

           .setTransactionIsolation();

 

死锁分析与解决

上节讲到数据库的隔离性,开发者一般会使用加锁来保证隔离性,但会遇到死锁的问题

场景:

           数据库:

ID

UserName

Account

Corp

1

ZhangSan

100

Ali

2

Lisi

0

Ali

           事务1:张三给李四转账100元

           事务2:张三和李四的单位改为Netease

 

           事务持锁:

                     MySQL是以行加锁的方式来避免不同事务对同一行数据的修改

                     事务1对张三这行记录的修改要使用到对这一行的行锁

                     事务2同时并发执行,事务2先修改李四的行记录的Corp,使用了对李四的行锁

                     事务1想要更新李四记录,需要持有李四的行锁,但是事务2占据了李四的行锁,于是事务1等待事务2执行完成后对李四行锁的释放。

                     事务2想要更新张三记录,需要持有张三的行锁,但是事务1占据了张三的行锁,于是事务2等待事务1执行完成后对张三行锁的释放。

                     事务1和事务2相互等待,两个事务都无法继续进行。

                     -->死锁了

死锁:

           两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。

           死锁产生的必要条件:

                     互斥:并发执行的事务为了进行必要的隔离保证执行正确,在事务结束前,需要对修改的数据库记录持锁,保证多个事务对相同数据库记录串行修改。对于大型并发系统而言是无法避免的。

                     请求和保持:一个事务需要申请多个资源,并且已经持有一个资源,在等待另一个资源锁。死锁仅发生在请求两个或者两个以上的锁对象时。由于业务需要修改多行数据库记录,难以避免。

                     不剥夺:已经获取锁资源的事务,在未执行完成前,不能被强制剥夺,只能使用完时由事务自己释放。一般用于已经出现死锁时,通过破坏该条件达到解除死锁的目的--数据库系统通常通过一定的死锁检测机制发现死锁,强制回滚持有锁的代价相对较小的事务,让另外一个事务执行完毕,就能解除死锁的问题。

                     环路等待:发生死锁时,必然存在一个事务-锁的环形链,如事务1因为锁1等待事务2,事务2因为锁2等待事务1、等等。产生原因:每个事务获取锁的顺序不一致导致。解决方法:按照同一顺序获取锁,可以破坏该条件。通过分析死锁事务之间的锁竞争关系,调整SQL的顺序,达到消除死锁的目的。i.e. 若事务1和事务2刚开始都想获取锁1,就不会形成环路,就不会出现环路等待,不会出现死锁了。--按序获取锁资源:预防死锁。

 

MySQL中的锁:

           排他锁X:与其他任何锁都是冲突的

           共享锁S:多个事务可以共享一把锁。若事务1获取了共享锁,事务2还想获取共享锁,则不需等待(是兼容的)

          

欲加锁

已有锁

X

S

X

冲突

冲突

S

冲突

兼容

加锁方式:

           外部加锁:由应用程序执行特定SQL语句进行显式添加,锁依赖关系较容易分析

                     共享锁(S):select * from table lock in share mode;

                     排他锁(X):select * from table for update;

           内部加锁:

                     为了实现ACID特性,由数据库系统内部自动添加

                     加锁规则繁琐,与SQL执行计划、事务隔离级别、表索引

哪些SQL需要持有锁?

           不需要:快照读:Innodb实现了多版本控制(MVCC),支持不加锁快照读。所有selcet语句不加锁,可以保证同一个selcet的结果集是一致的。但是不能保证同一个事务内部,select语句和其他语句的数据一致性,如果业务需要,需通过外部显示加锁。

           需要:当前读:

                     加了外部锁的select语句

                     Updata from table set…

                     Insert into …

                     Delete from table …

SQL加锁分析:

           i.e.

          

ID

UserName

Account

Corp

1

ZhangSan

100

Ali

2

LiSi

0

Ali

                     Update user set account = 0 where id =1;

                                update语句直接在ID =1行数据处加排他锁,此时若为select操作(是快读照),则不会被阻塞。

                     Select UserName from user where id = 1 in share mode;

                                该语句对行记录加了共享锁,此时若其他事务也对该行记录加共享锁,是不会阻塞的

分析死锁的常用方法:

           MySQL数据库会自动分析死锁并回滚代价最小的事务处理死锁。

           但是开发人员需要在死锁处理以后避免死锁再次发生。

           show engine innodb status;

                     其中有发生死锁时相关的SQL语句,也会列出被系统强制回滚的失去

                     分析死锁产生的原因,可以通过改变SQL顺序等操作有效避免死锁再次发生。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值