欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

事务

程序员文章站 2022-05-09 15:58:21
...

事务

事务就是一个事情。组成这个事情可能有多个单元,要求这些单元,要么全部成功,要么全都不成功。

  • 汇款包括两个单元组成:从一个账户中取出来,向另外一个账户存进去。
  • 在开发中有事物的存在,可以保证数据的完整性。

事务的操作

MySQL下事务的操作

方式1适合一个事务中有多条SQL语句

  • start transaction 开启事务
  • rollback 事务回滚
  • commit 事务提交

开启事务

create database remit;
use remit;
create table account(
    id int primary key auto_increment,
    name varchar(20),
    money double
);
insert into account values(null,'aaa',1000);
insert into account values(null,'bbb',1000);
insert into account values(null,'ccc',1000);
-- 开启事务,开启事务后可以在该过程中写任意SQL语句
start transaction;
--select语句并不能对表中的数据进行改变,此时开启事务没有意义
-- 
update account set money = money -500 where id = 1;
-- 此时重启数据库,会发现,表中的数据并没有改变

事务回滚

start transaction;
update account set monet =1;
--表中的money为1
select * from account;
rollback;
--表中的money又恢复
select * from account;

事务提交

start transaction;
update account set money = money -500 where id = 1;
commit;
--此时重启数据库,id=1的money已经变成500

方式2适合一个事务中有单条SQL语句

  • show variables like ‘%commit%’;可以查看当前autocommit 值
    • 在MySQL数据库中它的默认值是“on”代表自动事务
    • 自动事务的意义就是:执行任意一条SQL语句都会自动提交事务
  • 关闭自动提交 set autocommit = off 或者 set autocommit=0
    • 如果设置autocommit为off,意味着以后每条SQL语句都会处于一个事务中,相当于每条SQL执行前都执行start transaction
  • 测试:将autocommit的值设置成off
    • set autocommit = off 关闭自动事务
    • 必须手动commit才可以将事务提交
  • 注意:
    • MySQL默认autocommit=on
    • Oracle默认的autocommit=off

jdbc下事务的操作

java.sql.connection中有以下方法

  • setAutocommit(boolean flag);
    • 如果flag=false,它就相当于 start transaction;
  • rollBack()
    • 事务回滚
  • commit()
    • 事务提交

效果测试

public class TransactionTest1{
    public static void main(String args[]){
    //修改id=2的人的money=500
        String sql = "update account set money = 500 where id = 2";

        Connection con = JdbcUtils.getConnection();
        //开启事务,相当于 start transaction
        con.setAutocommit(false);

         Statement st= con.cresteStatement();
         st.executeUpdate(sql); 

         //事务回滚
        con.rollback();
        //事务提交
        con.commit();
        st.close();
        con.close();
    }
}
--查询数据库中表的数据
select * from account ;
-- 表的数据没有变化

开发实例事务操作,异常不能抛
在实际的开发中,出现异常后,在catch中进行回滚,在finally中进行事务的提交,以及释放资源操作

public class TransactionTest2{
    public static void main(String args[]){
        //修改id=2的人的money=500
        String sql = "update account set money = 500 where id = 2";
        Connection con = null;
        Statement st = null;
        try{
            Connection con = JdbcUtils.getConnection();
        //开启事务,相当于 start transaction
            con.setAutocommit(false);

             Statement st= con.cresteStatement();
             st.executeUpdate(sql); 
        }catch(SQLException e){
             e.printStackTrace();
              //事务回滚
             con.rollback();
        }finally{
            try{
                //事务提交
                con.commit();
                st.close();
                con.close();

            }catch(SQLException e){
                 e.printStackTrace();
            }
        }
    }
}

事务特性ACID(重点)

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

如果不考虑事务的隔离性,会出现的问题

  • 脏读:一个事务读取到另一个事务的未提交数据
  • 不可重复读:在一个事务里,两次读取的数据不一致(读提交数据)(update)
    • 例如:第一次读 账户金额是200,第二次读账户金额成100了
  • 虚读或者幻读:两次读取的数据不一致(读提交数据)(insert)
    • 例如:第一次读有两条记录,第二次读成三条或四条记录
  • 丢失更新:两个事务对同一条记录操作,后提交的事务将先提交的刘改记录覆盖了
  • 对于以上问题,可以通过设置隔离级别来解决

事务的隔离级别
分类

  • Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)(会锁表
  • Repeatable read :可避免脏读和不可重复读情况的发生。(可重复读)不可以避免虚读
  • Read committed :可避免脏读情况发生。(读已提交)
  • Read uncommitted:最低级别,以上情况均无法保证。(读未提交)
  • 开发中用第二个和第三个比较多

设置隔离级别

  • MySQL中设置
    • 查看当前事务的隔离级别
      • select @@tx_isolation
      • MySQL中默认的事务隔离级别是 :Repeatable read
      • Oracle中默认的事务隔离级别是:Read committed
    • 设置事务隔离级别
      • set session transaction isolation level
      • 例如:set session transaction isolation level Read uncommitted
  • jdbc中设置
    • 使用java.sql.connection接口中提供的方法
    • setTransactionIsolation(int level) throw SQLException
    • 参数level可以取下面值
      • Connection.TRANSACTION_READ_UNCOMMITTED
      • Connection.TRANSACTION_READ_COMMITTED
      • Connection.TRANSACTION_REPEATABLE_READ
      • Connection.TRANSACTION_SERIALIZABLE
      • (注意:不能使用Connection.TRANSACTION_NONE,因为它指定了不受支持的事务)

脏读演示
一个事务读取另一个事务的未提交数据

  • 设置A,B事务(开启两个MySQL窗口)隔离级别分别为 Read uncommitted
    • set session transaction isolation level read uncommitted;
  • 在A事务中
    • start transaction ;
    • update account set money = money -500 where name = ‘aaa’;
    • update account set money = money + 500 where name = ‘bbb’;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 500 |
      | 2 | bbb | 1500 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • 在B事务中
    • start transaction;
    • select * from account;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 500 |
      | 2 | bbb | 1500 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • 这时,B事务读取到了A事务未提交的数据,发现表中的数据已经修改,就出现的脏读
  • 此时,在A事务中
    • rollback;
    • commit;
  • 在B事务中
    • select * from account;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 1000 |
      | 2 | bbb | 1000 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • 此时,在B事务中查询表的数据,发现数据已经恢复原样。也出现了两次查询结果不一致的问题,即不可重复读。

解决脏读问题
将事务的隔离级别设置为 read committed 来解决脏读

  • 设置A,B事务隔离级别为 Read committed
    • set session transaction isolation level read committed;
  • 在A事务中
    • start transaction ;
    • update account set money = money -500 where name = ‘aaa’;
    • update account set money = money + 500 where name = ‘bbb’;
  • 在B事务中
    • start transaction;
    • select * from account;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 1000 |
      | 2 | bbb | 1000 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • 此时在B事务中,读取信息时,是不能读取到A 事务未提交的数据的,这也就解决了脏读。
  • 在A事务中,提交数据
    • commit;
    • 在B事务中,读取数据
    • select * from account;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 500 |
      | 2 | bbb | 1500 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • 此时,在B事务中,两次读取信息的结果不同,表明还存在不可重复读的问题。

解决不可重复读
将事务的隔离级别设置成Repeatable read 来解决不可重复读

  • 设置A、B事务隔离级别为 Repeatable read ;
    • set session transaction isolation level Repeatable read;
  • 在A事务中
    • start transaction ;
    • update account set money = money -500 where name = ‘aaa’;
    • update account set money = money + 500 where name = ‘bbb’;
  • 在B事务中
    • start transaction;
    • select * from account;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 1000 |
      | 2 | bbb | 1000 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • 在A事务中,提交数据
    • commit;
  • 在B事务中,读取数据
    • select * from account;
    • mysql> select * from account;
      +----+------+-------+
      | id | name | money |
      +----+------+-------+
      | 1 | aaa | 1000 |
      | 2 | bbb | 1000 |
      | 3 | ccc | 1000 |
      +----+------+-------+
      3 rows in set (0.00 sec)
  • A 事务提交后,B事务载查询,两次的查询结果一致,解决了重复读的问题
  • 在B事务中
  • commit;
  • select * from account;
  • mysql> select * from account;
    +----+------+-------+
    | id | name | money |
    +----+------+-------+
    | 1 | aaa | 500 |
    | 2 | bbb | 1500 |
    | 3 | ccc | 1000 |
    +----+------+-------+
    3 rows in set (0.00 sec)

设置事务隔离级别 Serializable,可以解决所有的问题

  • set session transactio isolation level Serializable;
  • 如果设置成这种隔离级别,那么会出现锁表,也就是说,一个事务载对表进行操作时,其他的事务操作不了。

案例:转账汇款

事务

  • 问题1:service调用了dao中两个方法完成了一个业务操作,如果其中一个方法执行失败怎么办?
    • 需要业务控制
  • 问题2:怎样进行事务控制
    • 我们在service层进行事务的开启,回滚以及提交操作
  • 问题3:进行事务操作需要使用Connection对象,那么,怎样保证,在service中的与dao中所使用的是同一个Connection
    • 在service层创建出Connection对象,将这个对象传递给dao层
  • 注意:Connection 对象使用完成后,在service层的finally中关闭
  • 而每一个PreparedStatement他们在dao层的方法中用完就关闭

在设置dao 层时

public interface AccountDao{
    public void accountOut(String accountOut,double money)throws Exception;
    public void accountIn(String accountIn,double money)throws Exception;

}

那么,我们自己去实现这个接口时,怎样去吃力,同一个Connection对象问题?
使用ThreadLocal

  • ThreadLocal 可以理解为一个Map集合
  • Map<Thread,Object>
  • set 方法是向ThreadLocal中存储数据,那么当前的key值就是当前线程对象
  • get方法是从ThreadLocal中获取数据,它是根据当前线程对象来获取值。
  • 如果我们在同一个线程中,只要在任意的一个位置存储了数据,在其他的位置,就可以获取到这个数据。

丢失更新

多个事务对同一条记录进行操作,但这些事务彼此之间不知道其他的事务进行的修改,后提交的事务将先提交的事务操作覆盖了

事务

丢失更新问题的解决

  • 悲观锁(Pessimistic Locking)
    • (假设丢失更新一定会发生的)–利用数据库内部锁机制,管理事务
      • 提供的锁机制有两种:
        • 共享锁
          • select * from table lock in share mode (读锁、共享锁)
        • 排它锁
          • select * from table for update(写锁、排他锁)
        • update语句默认添加排它锁
      • 允许一张数据表中数据记录,添加多个共享锁,添加共享锁记录,对于其他事务可读不可写的
      • 一张数据表中数据记录,只能添加一个排他锁,在添加排它锁的数据,不能再添加其他共享锁和排它锁。对于其他事务是可读不可写的
  • 乐观锁(Optimistic Locking)
    • (假设丢失更新不会发生)—采用程序中添加版本字段解决丢失更新问题

乐观锁演示
采用记录的版本字段,来判断记录是否修改过—timestamp(timestamp可以自动更新)

create table product(
    id int,
    name varchar(20),
    updatetime timestamp
);
insert into product values(1,'冰箱'null);
update product set name ='洗衣机' where id = 1;

事务

连接池

相关标签: 事务