前言:面试/笔试常见数据库问题之一,做个笔记,供以后查阅。
1.脏读、不可重复读、幻读
在讨论事务隔离级别前,我们先想象一下多个事务并发访问数据库时,可能存在的问题:
脏读:一个事务读取到了另外一个事务没有提交的数据
事务T1更新了数据,还未提交,此时事务T2执行查询操作,读取到了T1未提交的数据,这种情况称为脏读。 T2读到的实际上是数据库内存中数据,这种数据称为脏数据,由于还未提交,因此脏数据是不可靠的,基于脏数据所做的操作也可能是不正确的(如果T1执行回滚操作)。
不可重复读:在同一事务中,两次读取同一数据,得到内容不同
事务T1查询数据库一条记录,紧接着事务T2对T1查询的记录进行更新并提交,T1再次查询该条记录时取到的数据与上次不同,这种情况称为不可重复读。如果前后的结果一样,则是可重复读。
幻读:同一事务中,用同样的操作读取两次,得到的记录数不相同
事务T1查询表中记录,返回结果集,紧接着事务T2插入一条记录并提交,T1再次查询时结果集中包含T2刚刚插入的记录,这种情况称为幻读。一般情况下,幻读正是我们所需要的。
2.事务隔离级别
为了避免上述并发问题,SQL标准定义了几种事务的隔离级别(事务隔离级别,是指一个事务对数据库的修改与并行的另一个事务的隔离程度)。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED(读取未提交) | √ | √ | √ |
READ COMMITTED(读取已提交) | × | √ | √ |
REPEATABLE READ(可重复读) | × | × | √ |
SERIALIZABLE(串行化) | × | × | × |
可见,事务隔离级别越高,数据一致性就越高,与之相对的,数据库并发性就越低,同时对性能的影响也越大。SQL标准定义默认事务隔离级别为SERIALIZABLE,但最常见的隔离级别为READ COMMITED,这也是大多数数据库的默认隔离级别。
3.Oracle事务隔离级别
Oracle数据库支持三种事务隔离级别,分别为READ COMMITED、SERIALIZABLE以及READ-ONLY MODE(只读模式)。默认隔离级别为READ COMMITED。
READ COMMITED
事务内执行的查询只能看到查询执行前(非事务开始前)就已经提交的数据,Oracle永远不会读取脏数据(未提交的数据)。
当一个事务正在访问查询的数据时,其它事务可修改这些数据,在同一个事务内的两次查询间隔期间,数据可能被其它事务修改。因此,如果一个事务内同一查询执行两次,可能会出现不可重复读和幻读。
在该隔离级别下,有时候为保证操作的排它性,可为目标数据加锁,以保证在本次事务提交前,外界无法修改,在提交时释放锁。SERIALIZABLE
事务内执行的查询只能看到事务开始前就已经提交的数据,以及事务内INSERT、DELETE、 UPDATE语句对数据的修改。不会出现不可重复读或幻读。
READ-ONLY MODE
事务内的查询只能看到事务开始前就已经提交的数据,同时事务内不能执行INSERT、DALETE、UPDATE语句。
4.Hibernate悲观锁/乐观锁
由于READ COMMITED隔离级别仅杜绝了脏读现象,仍有可能发生不可重复读及幻读,在处理一些要求数据处理唯一性的业务时,为保证数据访问的排它性,则需为目标数据加锁,使得数据无法被外界(包括本系统的其它事务及 外部系统的事务)修改。
4.1悲观锁
悲观锁通常基于数据库锁机制实现,通过对查询加锁,以防止检索的记录被其它事务修改。Hibernate可以使用LockMode、LockOptions来加锁。
// 1.开始查询时就加锁
// 1.1 load
this.getSession.load(User.class, id, LockMode.UPGRADE);
// 1.2 query
Query query = this.getSession().createQuery(hql);
query.setLockMode(LockMode.UPGRADE_NOWAIT);
query.setLockOptions(LockOptions.UPGRADE_NOWAIT);
// 2.在需要更新时加锁
this.getSession().lock(user, LockMode.UPGRADE);
通过打印SQL语句可以发现,当使用悲观锁时,Hibernate会在生成的SQL语句后加上 for update / for update nowait 子句,通过 for update 子句锁定符合条件的记录,保证本次事务提交前,外界无法修改这些记录,事务提交时再释放事务过程中的锁。
4.2乐观锁
乐观锁本质上不是锁,而是一种冲突检测机制,通常基于数据库版本记录机制实现:在数据库表中增加版本version列,用来记录每行数据的版本,同时实体类中增加该字段,再在Hibernate配置文件中id节点之后加上version节点。这样每次更新数据时,该行记录对应的版本会自动加一。应用程序每次更新记录时把数据库当前版本号与读取的版本号进行对比,若不一致,则不能更新。
// 1.实体类增加version字段
private int version;
// 2.Hibernate配置文件增加version节点
<id name="id" type="java.lang.String">
<column name="ID" length="32" />
<generator class="uuid.hex"></generator>
</id>
<version name="version" column="version" type="java.lang.Integer"/>
4.3悲观锁和乐观锁的区别
悲观锁假定当前事务操作数据库资源时,肯定会有其它事务同时访问该资源,为了避免当前事务的操作受到干扰,先锁定资源,直到自己操作完成后再释放锁。
悲观锁是数据库层面的控制,尽管能保证操作的独占性,但是由于在事务的整个过程中锁住记录,会严重影响数据库并发性能。
乐观锁假定当前事务操作数据库资源时,不会有其它事务同时访问该资源,直到当前事务提交时,再基于数据的版本标识,通过应用程序来检测是否可提交。
乐观锁是应用程序层面的控制,既保证了事务的并发访问,又能有效防止数据更新,但它也有一定局限性,由于是在应用层加锁,此时若是在数据库中直接修改数据,应用层是感知不到的,需配合其它技术一起使用。