1 数据库事务复习

1.1 事务四大特性(简称ACID)

1、原子性:(atomicity)事务中的全部操作在数据库中是不可分割的,要么全部完成,要么均不执行。
2、一致性:(Consistency)几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果相一致。
3、隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
4、持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。

1.2 事务的并发问题

  • 事务的并发问题,主要发生在读和写两类操作上。
    1、读并发:读上有三类并发问题:脏读、不可重复读、幻读;
    2、写并发:写上有两类并发问题:第一类丢失更新(回滚丢失更新),第二类丢失更新(提交丢失更新)。

1.3 读并发问题

1.3.1 脏读

  • A事务读取了B事务未提交的数据。
  • 说明:对于事务隔离级别设置较低的数据库,是允许将未提交的数据写入到数据库的。但是,即使写入到数据库中,若事务回滚,也是可以再将数据恢复为原数据的。
  • 所以,可能发生脏读的现象:A事务修改了某数据,但是还未提交。此时,B事务读取了该数据。但是此时A事务又发生了回滚操作。那么,B事务读取到的就是个“不存在”的脏数据。

    1.3.2 不可重复读

  • 读取了已提交的事务,A事务先读取了一个数据,而后B事务修改(update或者delete)该数据并提交。此时A事务再次读取该数据时,该数据已经别修改或者不存在。即无法再读到原来相同的数据。

    1.3.3 幻读

  • 也叫虚读。读取了已提交的事务。与不可重复读不同的是,发生了插入(insert)操作。A事务先进行了某一条件的检索操作,而后B事务插入了若干数据并提交。这些数据存在符合A事务检索条件的数据。此时A事务再做相同检索,其检索结果就会与第一次的不同。在第二次取出了“幻影”。

    1.4 事务的隔离级别

  • 为了防止读并发问题的发生,标准SQL定义了四个隔离级别。级别由低到高分别为:读取未提交,读取已提交、可重复读、串行化。随着隔离级别的提高,其防止并发的效果也是逐步提交,但其系统开销也是逐步提高的,代码的执行效率是逐步降低的。
    1、读取未提交:不繁殖任何并发问题。
    2、读取已提交:防止脏读,可能出现不可重复读或者幻读。
    3、可重复读:防止脏读和不可重复读,可能会出现幻读。
    4、串行化:不存在并发问题。
  • MySql默认的事务隔离级别为可重复读,即防止了脏读和不可重复读,但是有可能存在幻读现象。通过 select @@global.tx_isolation;可以查询MySql默认的事务隔离级别。
    SSH框架之Hibernate5专题8:事务相关内容
  • 打开JDK6的帮助文档,查找到java.sql.Connection接口,其有五个常量,代表五个隔离级别。
    SSH框架之Hibernate5专题8:事务相关内容
  • 这五个隔离级别分别代表的常量数字,可以从JDK帮助文档中查看到。
    SSH框架之Hibernate5专题8:事务相关内容

    1.5 写并发问题

    1、第一类丢失更新:也称之为回滚丢失更新。A、B事务同时读取某数据,并均做修改,A事务进行了提交,而B事务又做回滚。此时,A事务提交的更新数据丢失。
    2、第二类丢失更新:也称之为提交丢失更新。A、B事务同时读取某个数据,并均做修改。A事务先做了提交,然后B事务也做了提交。此时A事务提交的更新数据会被B事务的提交给覆盖。

    1.6 加锁机制

  • 通过加锁可以解决写并发问题。锁可以分为两类:
  • 乐观锁(Optimistic lock):每次访问数据时,都会乐观的认为其他事务此时肯定不会同时修改该数据。但是在真正修改时,会在代码中先判断数据是否已经被其他事务修改过。所以锁是加在代码中的。
  • 悲观锁(Pessimistic lock):每次访问数据时,都会悲观的认为其他事务一定会同时修改该数据。所以,其在访问数据时,在数据库中就会先给数据加锁,以防止其他事务同时修改该数据。所以锁是加在数据库中的。

    1.6.1 乐观锁

  • 乐观锁是加在代码中的锁机制,一般充当乐观锁的有两类数据:版本号和时间戳。它们的工作原理是相同的。
  • A、B事务从DB中读取数据时同时会读出一个数据版本号,当A事务将修改过的数据写入到DB中时,会使得版本号增加1,当B事务发生回滚或者覆盖时,会首先对自己数据的版本号与DB中数据的版本号进行对比。若他们相等,则说明DB中数据没有发生变化,B事务可以将数据回滚到原始状态,或者将修改写入到DB中。若小于DB中的版本号,则说明其他事务已经修改过该数据,将抛出异常。

    1.6.2 悲观锁

  • 悲观锁是加在DB中的锁机制,有分为两种。
  • 写锁,又称之为排他锁,当A事务对某数据加上排他锁后,A事务将独占该数据,可对该数据进行读、写操作。但是其他事务时不能再为该数据添加任何锁的,直到A事务将排他锁解锁,将数据释放。
  • 在SQL语句中,若要为本次操作(事务)添加排他锁,则可在正常的SQL语句最后添加上for update即可。例如:select * from student where age <20 for update
  • 读锁,又称之为共享锁。当A事务对某数据加上共享锁后,只能对数据进行读操作。但是其他事务也同时可以为该数据添加共享锁,读取该数据。但是不能够添加写锁,直到所有事务将其共享锁解锁,将数据释放,才可再对数据添加排他锁。
  • 在SQL语句中,若要为本次操作(事务)添加共享锁。则可在正常的SQL语句最后添加上lock in share mode即可。例如:select * from student where age < 20 lock in share mode

    2 Hibernate并发问题解决

    2.1 设置Hibernate事务隔离级别

  • Hibernate建议设置事务隔离级别有4级,即可重复读。从Hibernate框架解压目录\project\etc\hibernate.properties 的默认设置中可以看到。
    SSH框架之Hibernate5专题8:事务相关内容
  • Hibernate配置文件中可以对隔离级别进行设置。
        <!-- 设置事务隔离级别 -->
        <property name="hibernate.connection.isolation">4</property>

    2.2 Hibernate实现乐观锁

  • 在Hibernate映射文件中的<class/>标签中,有一个子标签<version/>,其name属性用于指定作为版本的属性名称。其还有一个子标签<timestamp/>用于指定作为时间戳的属性名称。
  • 这两个子标签的用法相同,不同的是,作为版本的属性要求其类型为int,而作为时间戳的属性,要求为类型为java.sql.timestamp。
  • 举例:optimisticlock。
  • 实体Bean中要增加一个用于记录版本的属性,要求类型为int。其值由系统自动维护:初始值为0,每修改一次自动增1。
    private Integer id;
    private String name;
    private int age;
    private int studentVersion;
  • 映射文件中增加<version/>标签,要求在<id/>和<property/>之间。
    <class name="Student" table="t_student">
        <id name="id" column="sid">
            <generator class="native"></generator>
        </id>
        <version name="studentVersion"></version>
        <property name="name" column="sname"></property>
        <property name="age" column="sage"></property>
    </class>
  • 测试方法:先插入一条数据:
                Student student = new Student("张三", 24);
                session.save(student);
  • 查看数据库中的studentVersion的值,为0。
    SSH框架之Hibernate5专题8:事务相关内容
  • 再通过get()查询出一个对象,对其进行session内的修改后,再查看DB中studentVersion的值,已经变成了1。
            Student student = session.get(Student.class, 1);
            //做session中的修改操作
            student.setName("赵六");
            System.out.println(student);

    SSH框架之Hibernate5专题8:事务相关内容

    2.3 Hibernate实现悲观锁

  • Bean类的定义中不用增加任何属性。只需要在通过get()加载对象时,,为其加锁。使用的get()方法原型为:T get(Class<T> clazz, Serializable num, LockMode lockMode)
  • 排他锁为常量:LockMode.PESIMISTIC_WRITE
  • 共享锁为常量:LockMode.PESIMISTIC_READ
  • 举例:persimisticlock

    2.3.1 添加排他锁

  • 对于排他锁,其加锁的时间为通过get()方法执行select查询后,解锁时间为当前事务结束。这期间,当前事务是可以修改被其加锁的数据的,但是其他事务是无法对该数据进行修改的。

            //从这里开始将数据锁上,只可以自己修改该数据
            Student student = session.get(Student.class, 1, LockMode.PESSIMISTIC_WRITE);
    
            //在这里添加断点,在DB中修改id为1的student的属性值,DB将”无响应“
            student.setName("田七");
            System.out.println(student);
  • 在测试时注意,在student.setName()出添加断点,当运行到此断点时,到数据库中修改id为1的student的任意属性,做保存后,关闭表。此时DB中会给出”未响应“提示。说明,数据被锁定,不能够修改。
    SSH框架之Hibernate5专题8:事务相关内容
  • 查看控制台输出的SQL语句,可看到select语句后添加了for update。证明添加了写锁。
    SSH框架之Hibernate5专题8:事务相关内容

    2.3.2 添加共享锁

  • 对于共享锁,其加锁时间也为通过get()方法执行select查询后,但是其解锁时间则是在其查询的数据被检索出来后的瞬间。即从程序运行来看,get()方法开始执行则加上了读锁,get()方法运行结束,则读锁解除。所以,加了读锁的get()方法后的修改语句,与读锁是没有任何关系的。
            //在执行该get()引发的select语句时,会为id为1的student的数据加上读锁
            //select语句执行完毕,读锁将自动解除,而非在事务提交或者回滚时才解锁。
            Student student = session.get(Student.class, 1, LockMode.PESSIMISTIC_READ);
            //此修改可以更新到DB中,因为这里没有为数据加任何锁
            student.setAge(29);
            System.out.println(student);
  • 从后台运行的SQL中可以看到,多了lock in share mode,说明添加了共享锁。
    SSH框架之Hibernate5专题8:事务相关内容