javaWeb学习笔记 --- 事务

事务

事务的概念

事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。

例如:A——B转帐,对应于如下两条sql语句

update account set money=money-100 where name='a';
update account set money=money+100 where name='b';

在这个例子中,我们要保证这两条sql要么一起成功,要么一起失败,不允许一部分成功一部分失败,这就要靠数据库的事务来实现了。

管理事务

数据库默认的事务

数据库默认支持事务的,但是数据库默认的事务是一条sql语句独占一个事务,这种模式意义不大。

注意:MyISAM存储引擎是不支持事务处理的;可以通过SHOW ENGINES查看

  • 查看表使用的存储引擎

    show table status from db_name where name=‘table_name’;

  • 修改表的存储引擎

    alter table table_name engine=innodb;

手动控制事务

sql控制事务

  • start transaction:开启事务,在这条语句之后的所有的sql将处在同一事务中,要么同时完成要么同时不完成;事务中的sql在执行时,并没有真正修改数据库中的数据。

  • commit:提交事务,将整个事务对数据库的影响一起发生。

    CREATE TABLE account(id INT, name VARCHAR(20), money DOUBLE);
    INSERT INTO account(id,name,money) VALUES(0,'lili',200);
    INSERT INTO account(id,name,money) VALUES(1,'Tom',300);
    INSERT INTO account(id,name,money) VALUES(2,'Potter',400);
    SELECT * FROM account;
    START TRANSACTION;
    UPDATE account SET money=money+10 WHERE name = 'lili';
    UPDATE account SET money=money-10 WHERE name = 'Potter';
    COMMIT;
    SELECT * FROM account;
    
  • rollback:回滚事务,将这个事务对数据库的影响取消掉。

    START TRANSACTION;
    UPDATE account SET money=money+10 WHERE name = 'lili';
    UPDATE account SET money=money-10 WHERE name = 'Potter';
    ROLLBACK;
    

JDBC中控制事务

当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。

若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:

  • conn.setAutoCommit(false) : 关闭自动提交

    关闭后,conn将不会帮我们提交事务,在这个连接上执行的所有sql语句将处在同一事务中,需要我们是手动的进行提交或回滚

  • conn.commit():提交事务

  • conn.rollback():回滚事务

public class TransJDBC {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = JDBCUtils.getConn();
            // 开启事务
            conn.setAutoCommit(false);
            String sql1 = "UPDATE account SET money=money+10 WHERE name = ?;";
            String sql2 = "UPDATE account SET money=money-10 WHERE name = ?;";
            ps = conn.prepareStatement(sql1);
            ps.setString(1,"lili");
            ps.executeUpdate();
            int a =  1 / 0;
            ps = conn.prepareStatement(sql2);
            ps.setString(2,"Potter");
            ps.executeUpdate();

            // 提交事务
            conn.commit();

        } catch (Exception e){
            e.printStackTrace();
            // 回滚事务
            if (conn != null){
                try {
                    conn.rollback();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        } finally{
            JDBCUtils.close(conn,ps,rs);
        }
    }
}
  • 也可以设置回滚点回滚部分事务。

  • SavePoint sp = conn.setSavePoint();

  • conn.rollback(sp);

  • 设置回滚点后,执行的语句超过回滚点,则不会全部回滚,只会回滚到回滚点上

  • 注意:回到回滚点后,回滚点之前的代码虽然没被回滚但是也没提交呢,如果想起作用还要做commit操作

public class TransJDBC {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        Savepoint savepoint = null;
        try {
            conn = JDBCUtils.getConn();
            // 开启事务
            conn.setAutoCommit(false);
            String sql1 = "UPDATE account SET money=money+10 WHERE name = ?;";
            String sql2 = "UPDATE account SET money=money+10 WHERE name = ?;";
            ps = conn.prepareStatement(sql1);
            ps.setString(1,"lili");
            ps.executeUpdate();

            // 设置回滚点
            savepoint = conn.setSavepoint();

            int a =  1 / 0;
            ps = conn.prepareStatement(sql2);
            ps.setString(1,"lili");
            ps.executeUpdate();

            // 提交事务
            conn.commit();

        } catch (Exception e){
            e.printStackTrace();
            // 回滚事务
            if (conn != null){
                try {
                    if (savepoint != null){
                        // 走过了回滚点
                        conn.rollback(savepoint);
                        conn.commit();
                    }
                    else{  // 没有走过回滚点
                        conn.rollback();
                    }

                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        } finally{
            JDBCUtils.close(conn,ps,rs);
        }
    }
}

事务的四大特性

事务的四大特性是事务本身具有的特点。简称ACID。

  • 原子性 (Atomicity): 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  • 一致性 (Consistency): 事务前后数据的完整性必须保持一致。
  • 隔离性 (Isolation): 事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
  • 持久性 (Durability): 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

隔离性

数据库隔离性分析

  • 数据库的其他三大特性数据库可以保证,而隔离性需要再讨论
  • 数据库的隔离性问题本质上就是多线程并发安全性问题
  • 可以用锁来解决多线成并发安全问题,但是如果用了锁,必然会造成程序的性能大大的下降,对于数据库这种高并发要求的程序来说这是不可接受的

具体分析下隔离性产生的细节:

  1. 如果两个线程并发修改,必然产生多线程并发安全问题,必须隔离开
  2. 如果两个线程并发查询,必然没有问题,不需要隔离
  3. 如果一个线程修改,一个线程查询,在不同的应用场景下有可能有问题,有可能没问题。

隔离性可能造成的问题

脏读:

打开两个mysql客户端,都执行以下语句:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

一个事务读取到另一个事务未提交的数据:a给b转账

----------------------------

CREATE TABLE account(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), money INT);

INSERT INTO account(name,money) VALUES('a',1000); -- a:1000
INSERT INTO account(name,money) VALUES('b',1000); -- b:1000

----------------------------

客户端A:

START TRANSACTION;
UPDATE account SET  money=money-100 WHERE name = 'a';
UPDATE account SET  money=money+100 WHERE name = 'b';

-----------------------------

客户端B:

START TRANSACTION;
SELECT * FROM account;  -- a:900  B: 1100
COMMIT;

-----------------------------

客户端A:

ROLLBACK;

-----------------------------

客户端B:

START TRANSACTION;
SELECT * FROM account;  -- a:1000  B: 1000
COMMIT;

-----------------------------

脏数据:未提交的随后又被撤销的数据

不可重复读:

  • 一个事务多次读取数据库中的同一条记录,多次查询的结果不同
  • 一个事务读取到另一个事务已经提交的数据

打开两个mysql客户端,都执行以下语句:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

a在一次事务中查询自己的账户情况

----------------------------

-- 隔离性演示:不可重复读
-- 设置初始值 1000
UPDATE account SET  money=1000 WHERE name = 'a';
UPDATE account SET  money=1000 WHERE name = 'b';

----------------------------

客户端A:

START TRANSACTION;
SELECT * FROM account WHERE name = 'a'; -- 1000
SELECT * FROM account WHERE name = 'b'; -- 1000

-----------------------------

客户端B:

START TRANSACTION;
UPDATE account SET money=money-100 WHERE name = 'a';
UPDATE account SET money=money+100 WHERE name = 'b';
COMMIT

-----------------------------

客户端A:

SELECT * FROM account WHERE name = 'a'; -- 900
SELECT * FROM account WHERE name = 'b'; -- 1100
COMMIT;

-----------------------------

虚读 (幻读)

  • 出现的概率非常低
  • 一个事务多次查询整表数据,多次查询时,由于有其他事务增删数据, 造成的查询结果不同
  • 一个事务读取到另一个事务已经提交的数据

打开两个mysql客户端,都执行以下语句:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

客户端A在一次事务中查询账户总额:

----------------------------

客户端A:

START TRANSACTION;
SELECT SUM(money) FROM account; -- 2000

-----------------------------

客户端B:

START TRANSACTION;
INSERT INTO account(name,money) VALUES('c',5000);
COMMIT;

-----------------------------

客户端A:

SELECT SUM(money) FROM account; -- 7000
COMMIT;

-----------------------------

数据库的隔离级别

数据库设计者在设计数据库时到底该防止哪些问题呢?防止的问题越多性能越低,防止的问题越少,则安全性越差。

数据库的四大隔离级别

  • read uncommitted :不做任何隔离,可能造成脏读、不可重复读、虚读 (幻读) 问题

  • read committed: 可以防止脏读,但是不能防止不可重复读、虚读 (幻读) 问题

  • repeatable Read: 可以防止脏读、不可重复读,但是不能防止虚读 (幻读) 问题

  • serializable:可以防止所有隔离性的问题,但是数据库就被设计为串行化的数据库,性能很低

从安全性上考虑:

  • Serializable >Repeatable Read > Read Committed > Read uncommitted

从性能上考虑:

  • Read uncommitted > Read committed > Repeatable Read > Serializable

通常从Repeatable Read和Read committed中选择一个

  • 如果需要防止不可重复读选择Repeatable Read,如果不需要防止选择Read committed

  • mysql数据库默认的隔离级别就是Repeatable Read

  • Oracle数据库默认的隔离级别是Read committed

操作数据库的隔离级别

查询数据库的隔离级别:

SELECT @@TX_ISOLATION;

修改数据库的隔离级别:

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 隔离级别;
  • 不写默认就是session,修改的是当前客户端和服务器交互时是使用的隔离级别,并不会影响其他客户端的隔离级别

  • 如果写成global,修改的是数据库默认的隔离级别 (即新开客户端时,默认的隔离级别),并不会修改当前客户端和已经开启的客户端的隔离级别

    如:set global transaction isolation level serializable;

数据库中的锁

共享锁

  • 共享锁和共享锁可以共存,共享锁和排他锁不能共存

  • 在非Serializable隔离级别下做查询不加任何锁,在Serializable隔离级别下做查询加共享锁。

案例演示:

打开两个mysql客户端,将隔离级别都设置为Serializable级别

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;  -- 设置后查询加了共享锁

分别在两个客户端中查询:

START TRANSACTION;
SELECT * FROM account;  -- 都能查询出数据,说明共享锁可以共存。

排他锁

  • 排他锁和共享锁不能共存,排他锁和排他锁也不能共存
  • 在任何隔离级别下做增删改都加排他锁

案例演示:

打开两个mysql客户端,将隔离级别都设置为Serializable级别

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

两个客户端都执行:

START TRANSACTION;
SELECT * FROM account;

-----------------------------

客户端A执行:

UPDATE account SET money=money-100 WHERE name = 'a';
-- 执行在等待,当另外一个客户端提交commit或者回滚rollback之后,修改才能成功。

-----------------------------

客户端B执行:

COMMIT;  -- 客户端B提交后释放共享锁,客户端A执行修改

-----------------------------

客户端A执行:

COMMIT;  -- 客户A提交修改,释放排它锁

死锁

  • mysql可以自动检测到死锁,错误退出一方并执行另一方

案例演示:

打开两个mysql客户端,将隔离级别都设置为Serializable级别

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

两个客户端都执行:

START TRANSACTION;
SELECT * FROM account;

-----------------------------

客户端A执行:

UPDATE account SET  money = 1000 WHERE name = 'a';
-- 客户端A升级为排它锁,等待客户端B执行

-----------------------------

客户端B执行:

UPDATE account SET  money = 1000 WHERE name = 'b';
-- 客户端B升级为排它锁,等待客户端A执行

-----------------------------

发现彼此等待,直到一方报错结束,死锁才结束。


版权声明:本文为weixin_39281866原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。