GVKun编程网logo

Mysql 锁机制 -- 乐观锁 & 悲观锁(mysql乐观锁与悲观锁)

2

对于想了解Mysql锁机制--乐观锁&悲观锁的读者,本文将是一篇不可错过的文章,我们将详细介绍mysql乐观锁与悲观锁,并且为您提供关于HibernateJPA悲观锁,乐观锁、Hibernate学习_

对于想了解Mysql 锁机制 -- 乐观锁 & 悲观锁的读者,本文将是一篇不可错过的文章,我们将详细介绍mysql乐观锁与悲观锁,并且为您提供关于Hibernate JPA 悲观锁,乐观锁、Hibernate学习_021_Hibernate中的事务控制+乐观锁+悲观锁、JAVA 乐观锁、悲观锁、Java 并发 行级锁 / 字段锁 / 表级锁 乐观锁 / 悲观锁 共享锁 / 排他锁 死锁的有价值信息。

本文目录一览:

Mysql 锁机制 -- 乐观锁 & 悲观锁(mysql乐观锁与悲观锁)

Mysql 锁机制 -- 乐观锁 & 悲观锁(mysql乐观锁与悲观锁)

[TOC]

前言

mysql 的并发操作时而引起的数据的不一致性(数据冲突):

丢失更新:两个用户(或以上)对同一个数据对象操作引起的数据丢失。

    解决方案:1. 悲观锁,假设丢失更新一定存在;sql 后面加上 for update;这是数据库的一种机制。

         2. 乐观锁,假设丢失更新不一定发生。update 时候存在版本,更新时候按版本号进行更新。

第一部分 悲观锁

1 概念

悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

2 命令行演示

2.1 准备数据

复制代码

DROP DATABASE IF EXISTS cyhTest;
CREATE DATABASE cyhTest;

USE cyhTest;

DROP TABLE IF EXISTS employee;

CREATE TABLE IF NOT EXISTS employee (
  id      INTEGER NOT NULL,
  money   INTEGER,
  version INTEGER,
  PRIMARY KEY (id)
)
  ENGINE = INNODB;

INSERT INTO employee VALUE (1, 0, 1);

SELECT * FROM employee;

复制代码

img

目前数据库中只有一条记录,且初始 Money=0

2.2 测试

测试准备:

  • 还是两个会话(终端),左边会话是白色背景、右边会话是黑色背景
  • 关闭自动提交:set autocommit = 0;

现在开始测试:

第一步:两个终端均关闭自动提交

左边:

img

右边:

img

第二步:左边利用 select .... for update 的悲观锁语法锁住记录

select * from employee where id = 1 for update; 

img

第三步:右边也尝试利用 select .... for update 的悲观锁语法锁住记录

img

可以看到,Sql 语句被挂起(被阻塞)!

提示:如果被阻塞的时间太长,会提示如下:

img

第四步:左边执行更新操作并提交事务

Sql 语句:

update employee set money = 0 + 1 where id = 1;
commit; 

结果:

img

分析:

  • Money 的旧值为 0,所以更新时 Money=0+1
  • 一执行 commit 后,注意查看右边 Sql 语句的变化

第五步:查看右边 Sql 语句的变化

img

分析:

  • 被左边悲观锁阻塞了 11.33 秒
  • Money=1,这是左边更新后的结果

2.3 结论

可以看到,当左边(事务 A)使用了 select ... for update 的悲观锁后,右边(事务 B)再想使用将被阻塞,同时,阻塞被解除后事务 B 能看到事务 A 对数据的修改,所以,这就可以很好地解决并发事务的更新丢失问题啦(诚然,这也是人家悲观锁的分内事)

第二部分 乐观锁

1 概念

1.1 理解方式一(来自网上其它小伙伴的博客)

乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就 OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。

1.2 理解方式二(来自网上其它小伙伴的博客)

乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。

1.3 我的理解

理解一:就是 CAS 操作

理解二:类似于 SVN、GIt 这些版本管理系统,当修改了某个文件需要提交的时候,它会检查文件的当前版本是否与服务器上的一致,如果一致那就可以直接提交,如果不一致,那就必须先更新服务器上的最新代码然后再提交(也就是先将这个文件的版本更新成和服务器一样的版本)

乐观锁不是数据库自带的,需要我们自己去实现。

2 如何实现乐观锁呢

首先说明一点的是:乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。

常见的做法有两种:版本号控制及时间戳控制。

版本号控制的原理:

  • 为表中加一个 version 字段;
  • 当读取数据时,连同这个 version 字段一起读出;
  • 数据每更新一次就将此值加一;
  • 当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作(PS:这完完全全就是 CAS 的实现逻辑呀~)

至于时间戳控制,其原理和版本号控制差不多,也是在表中添加一个 timestamp 的时间戳字段,然后提交更新时判断数据库中对应记录的当前时间戳是否与之前取出来的时间戳一致,一致就更新,不一致就重试。

特点

乐观并发控制相信事务之间的数据竞争概率是较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁

Hibernate JPA 悲观锁,乐观锁

Hibernate JPA 悲观锁,乐观锁

1.悲观锁

它指的是对数据被外界修改持保守态度。假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,为了保持数据被操作的一致性,于是对数据采取了数据库层次的锁定状态,依靠数据库提供的锁机制来实现。 
基于jdbc实现的数据库加锁如下:

select * from account where name="Erica" for update

在更新的过程中,数据库处于加锁状态,任何其他的针对本条数据的操作都将被延迟。本次事务提交后解锁。 
而hibernate悲观锁的具体实现如下:

String sql="查询语句";
Query query=session.createQuery(sql);
query.setLockMode("对象",LockModel.UPGRADE);

说到这里,就提到了hibernate的加锁模式:

  • LockMode.NONE:无锁机制。
  • LockMode.WRITE:Hibernate在Insert和Update记录的时候会自动获取。
  • LockMode.READ:Hibernate在读取记录的时候会自动获取。

这三种加锁模式是供hibernate内部使用的,与数据库加锁无关: 
LockMode.UPGRADE:利用数据库的for update字句加锁。 
在这里我们要注意的是:只有在查询开始之前(也就是hiernate生成sql语句之前)加锁,才会真正通过数据库的锁机制加锁处理。否则,数据已经通过不包含for updata子句的sql语句加载进来,所谓的数据库加锁也就无从谈起。 
但 是,从系统的性能上来考虑,对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多联机,假设有数以百计或上千甚至更多的并发访 问出现,我们该怎么办?如果等到数据库解锁我们再进行下面的操作,我们浪费的资源是多少?–这也就导致了乐观锁的产生。

2.乐观锁

乐观锁定(optimistic locking)则乐观的认为资料的存取很少发生同时存取的问题,因而不作数据库层次上的锁定,为了维护正确的数据,乐观锁定采用应用程序上的逻辑实现版本控制的方法。 
例如若有两个客户端,A客户先读取了账户余额100元,之后B客户也读取了账户余额100元的数据,A客户提取了50元,对数据库作了变更,此时数 据库中的余额为50元,B客户也要提取30元,根据其所取得的资料,100-30将为70余额,若此时再对数据库进行变更,最后的余额就会不正确。 
在不实行悲观锁定策略的情况下,数据不一致的情况一但发生,有几个解决的方法,一种是先更新为主,一种是后更新的为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁定。 
Hibernate 中透过版本号检查来实现后更新为主,这也是Hibernate所推荐的方式,在数据库中加入一个VERSON栏记录,在读取数 据时连同版本号一同读取,并在更新数据时递增版本号,然后比对版本号与数据库中的版本号,如果大于数据库中的版本号则予以更新,否则就回报错误。 
以刚才的例子,A客户读取账户余额1000元,并连带读取版本号为5的话,B客户此时也读取账号余额1000元,版本号也为5,A客户在领款后账户 余额为500,此时将版本号加1,版本号目前为6,而数据库中版本号为5,所以予以更新,更新数据库后,数据库此时余额为500,版本号为6,B客户领款 后要变更数据库,其版本号为5,但是数据库的版本号为6,此时不予更新,B客户数据重新读取数据库中新的数据并重新进行业务流程才变更数据库。 
以Hibernate实现版本号控制锁定的话,我们的对象中增加一个version属性,例如:

public class Account {
private int version;
....
public void setVersion(int version) {
this.version = version;
}
public int getVersion() {
return version;
}
....
}

而在映像文件中,我们使用optimistic-lock属性设定version控制,属性栏之后增加一个标签,如下:

<hibernate-mapping>
<class name="onlyfun.caterpillar.Account" talble="ACCOUNT" optimistic-lock="version">
<id...../>
<version name="version" column="VERSION"/>
....
</class>
</hibernate-mapping>

设定好版本控制之后,在上例中如果B 客户试图更新数据,将会引发StableObjectStateException例外,我们可以捕捉这个例 外,在处理中重新读取数据库中的数据,同时将 B客户目前的数据与数据库中的数据秀出来,让B客户有机会比对不一致的数据,以决定要变更的部份,或者您可 以设计程式自动读取新的资料,并重复扣款业务流程,直到数据可以更新为止,这一切可以在背景执行,而不用让您的客户知道。 
但是乐观锁也有不能解决的问题存在:上面已经提到过乐观锁机制的实现往往基于系统中的数据存储逻辑,在我们的系统中实现,来自外部系统的用户余额更 新不受我们系统的控制,有可能造成非法数据被更新至数据库。因此我们在做电子商务的时候,一定要小心的注意这项存在的问题,采用比较合理的逻辑验证,避免 数据执行错误。 
也可以在使用Session的load()或是lock()时指定锁定模式以进行锁定。 
如果数据库不支持所指定的锁定模式,Hibernate会选择一个合适的锁定替换,而不是丢出一个例外。

悲观锁与乐观锁的比较:

悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受; 
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。 
Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的生产力。

Hibernate中可以通过class描述符的optimistic-lock属性结合version描述符指定。 
optimistic-lock属性有如下可选取值: 
- none 无乐观锁 
- version 通过版本机制实现乐观锁 
- dirty 通过检查发生变动过的属性实现乐观锁 
- all 通过检查所有属性实现乐观锁

其中通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,目前唯一在数据对象脱离Session发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。

JPA 2.0 锁机制

锁是处理数据库事务并发的一种技术,当两个或更多数据库事务并发地访问相同数据时,锁可以保证同一时间只有一个事务可以修改数据。 
锁的方法通常有两种:乐观锁和悲观锁。乐观锁认为多个并发事务之间很少出现冲突,也就是说不会经常出现同一时间读取或修改相同数据,在乐观锁中,其目标是让并发事务自由地同时得到处理,而不是发现或预防冲突。两个事务在同一时刻可以访问相同的数据,但为了预防冲突,需要对数据执行一次检查,检查自上次读取数据以来发生的任何变化。 
悲观锁认为事务会经常发生冲突,在悲观锁中,读取数据的事务会锁定数据,在前面的事务提交之前,其它事务都不能修改数据。 
JPA 1.0只支持乐观锁,你可以使用EntityManager类的lock()方法指定锁模式的值,可以是READ或WRITE,如:

EntityManagerem = ... ;
em.lock (p1, READ);

对于READ锁模式,JPA实体管理器在事务提交前都会锁定实体,检查实体的版本属性确定实体自上次被读取以来是否有更新,如果版本属性被更新了,实体管理器会抛出一个OptimisticLockException异常,并回滚事务。 
对于WRITE锁模式,实体管理器执行和READ锁模式相同的乐观锁操作,但它也会更新实体的版本列。 
JPA 2.0增加了6种新的锁模式,其中两个是乐观锁。JPA 2.0也允许悲观锁,并增加了3种悲观锁,第6种锁模式是无锁。 
下面是新增的两个乐观锁模式: 
1、OPTIMISTIC:它和READ锁模式相同,JPA 2.0仍然支持READ锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC。 
2、OPTIMISTIC_FORCE_INCREMENT:它和WRITE锁模式相同,JPA 2.0仍然支持WRITE锁模式,但明确指出在新应用程序中推荐使用OPTIMISTIC_FORCE_INCREMENT。 
下面是新增的三个悲观锁模式: 
1、PESSIMISTIC_READ:只要事务读实体,实体管理器就锁定实体,直到事务完成锁才会解开,当你想使用重复读语义查询数据时使用这种锁模式,换句话说就是,当你想确保数据在连续读期间不被修改,这种锁模式不会阻碍其它事务读取数据。 
2、PESSIMISTIC_WRITE:只要事务更新实体,实体管理器就会锁定实体,这种锁模式强制尝试修改实体数据的事务串行化,当多个并发更新事务出现更新失败几率较高时使用这种锁模式。 
3、PESSIMISTIC_FORCE_INCREMENT:当事务读实体时,实体管理器就锁定实体,当事务结束时会增加实体的版本属性,即使实体没有修改。 
你也可以指定新的锁模式NONE,在这种情况下表示没有锁发生。 
JPA 2.0也提供了多种方法为实体指定锁模式,你可以使用EntityManager的lock() 和 find()方法指定锁模式。此外,EntityManager.refresh()方法可以恢复实体实例的状态。 
下面的代码显示了使用PESSIMISTIC_WRITE锁模式的悲观锁:

    // read
    Part p = em.find(Part.class, pId);
    // lock and refresh before update
    em.refresh(p, PESSIMISTIC_WRITE);
    int pAmount = p.getAmount();
    p.setAmount(pAmount - uCount); 

在这个例子中,它首先读取一些数据,然后应用PESSIMISTIC_WRITE锁,在更新数据之前调用EntityManager.refresh()方法,当事务更新实体时,PESSIMISTIC_WRITE锁锁定实体,其它事务就不能更新相同的实体,直到前面的事务提交。

Hibernate学习_021_Hibernate中的事务控制+乐观锁+悲观锁

Hibernate学习_021_Hibernate中的事务控制+乐观锁+悲观锁

事务具有ACID(Atomicity+Consistency+Isolation+Durability)特性,在多线程中存在事务并发的情况,如果处理不好的话,会出现以下五中问题:

       1:丢失更新

2:脏读:程序读到了其他程序插入到数据库中但还没有正式提交的数据。

3:不可重复读

4:第二类丢失更新

5:幻像读取:牵涉到插入和删除操作,也是不可重复读取的一个特例。

对于数据库的并发事务管理,可以通过查询java.sql.Connection的API得到数据库的事务隔离机制。基本来看有如下四种机制:

1:read-uncommitted(value=1)机制:可以读到其他并发进程没有提交的数据,很不安全。

2:read-committed(value=2):只可以读到其它进程已经提交的数据。(最佳实践

3:repeatable-read(value=4):可重复读取,其实内部实现机制就是给数据库中的对应数据加了一把锁,这样,当前进程用完此数据之前,其他所有的进程都不可以更新此数据。

4:serializable(value=8):将各个进程序列化,也就是将各个进程先排队,挨个排的执行,这样可以保证事务绝对安全,但是这样效率是最低的。

那么,在Hibernate中如何设置事务隔离级别呢?一般我们可以再hibernate.cfg.xml中设置属性hibernate.connection.isolation来实现。这里一般设置此属性的值为2(read-committed)。但是,这样可能造成不可重复读取。比如一个事务A读取了事务B已经提交的更新数据,但是如果事务B回滚了,这个时候,A第二次读取这个数据,这个时候,前后两次读取结果不一致,造成不可重复读取。面对这个问题,在hibernate中是使用加锁机制来解决这个问题的。加锁又分两种:乐观锁、悲观锁。(程序中一般不考虑幻象读取,因为在一个事务中本身就很少查询两次数据库的,用的少。)

悲观锁:在程序中,悲观的认为本进程把数据读取出来后,一定会有其他进程去修改数据库总已经读取出来的数据。所以就干脆索性给数据库中对应的记录加一把锁,所以,悲观锁的实现依赖于数据库本身的加锁机制。(我们一般的sql:select * ....... for update就是强制数据库给数据记录加锁。)

那么hibernate程序如何实现悲观锁呢?在用get()或者load()方法取对象的时候,可以选择加锁模式:一般而言只可以选择LockMode.UPGRADE这个模式(因为其他加锁模式是Hibernate内部的默认实现,在Hibernate内部使用,不需要用户去理会,比如:LockMode.None、LockMode.Read、LockMode.Write三个)。代码如下:

@Test
  • public void testQuery() {
  • Session session = this.sessionFactory.openSession();
  • session.beginTransaction();
  • Category c = (Category) session.load(Category.class, 1, LockMode.UPGRADE);
  • session.getTransaction().commit();
  • session.close();
  • }
  • 看测试结果,发出的sql语句末尾加了“for update”来强制让数据库给对应的数据记录加锁,如下所示:

    1. select
    2. category0_.id as id0_0_,
    3. category0_.name as name0_0_
    4. from
    5. Category category0_
    6. where
    7. category0_.id=? for update
    乐观锁:这就是完全依靠程序来加锁, 不依赖于具体数据库,就是 乐观的认为本进程把数据读取出来后, 不一定 会有其他进程去修改数据库中已经读取出来的数据。如果就是修改了也没事,就报一个异常就可以了。乐观锁效率较高,不会对数据库中的数据记录加锁。

    乐观锁的实现方式是:对应的类中加入一个“版本号”字段,并在对应的get方法上用@Version标示,这样,数据库会在每次更新此类对应的数据时,会自动将此记录的版本号字段值自动加1,这样,每次当前进程通过对比此字段的值就能够知道是否还有其他进程在自己对数据进行操作的过程中对数据库中的此条数据记录进行了修改。

    比如对Category类加入一个乐观锁控制:代码如下:

    1. @Entity
    2. @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
    3. public class Category {
    4. private int id;
    5. private String name;
    6. private List<Topic> topics;
    7. private int version;
    8. @Version
    9. public int getVersion() {
    10. return version;
    11. }
    12. public void setVersion(int version) {
    13. this.version = version;
    14. }

    这样,数据库表中就会多一个version字段,用来实现乐观锁机制的数据版本控制。

    JAVA 乐观锁、悲观锁

    JAVA 乐观锁、悲观锁

    见名知意:

    乐观锁就是很乐观,任何一个线程去操作数据的时候,它都认为该线程不会修改数据,也就不会去锁定该线程的写入操作。

    悲观锁就是很悲观,任何一个线程去操作数据的时候,它都认为该线程会修改数据,所以它会锁定该线程的写入操作。


    网上的答案说是乐观锁一般适用于读操作,悲观锁适用于写操作。

    自己的理解就是,只要不涉及到重要的数据,就是用乐观锁;凡是涉及到重要数据就使用悲观锁。

    Java 并发 行级锁 / 字段锁 / 表级锁 乐观锁 / 悲观锁 共享锁 / 排他锁 死锁

    Java 并发 行级锁 / 字段锁 / 表级锁 乐观锁 / 悲观锁 共享锁 / 排他锁 死锁

    前言

    锁是防止在两个事务操作同一个数据源(表或行)时交互破坏数据的一种机制。

    数据库采用封锁技术保证并发操作的可串行性。

    以 Oracle 为例:

    Oracle 的锁分为两大类:数据锁(也称 DML 锁)和字典锁。

    字典锁是 Oracle DBMS 内部用于对字典表的封锁。

    字典锁包括语法分析锁和 DDL 锁,由 DBMS 在必要的时候自动加锁和释放锁,用户无机控制。

    Oracle 主要提供了 5 种数据锁:

    共享锁(Share Table Lock,简称 S 锁)、

    排它锁(Exclusive Table Lock,简称 X 锁)、

    行级锁(Row Share Table Lock,简称 RS 锁)、

    行级排它锁(Row Exclusive Table Lock,简称 RX 锁)和

    共享行级排它锁(Share Row Exclusive Table Lock,简称 SRX 锁)。

    其封锁粒度包括行级和表级。

    以 Mysql 为例:

    行级锁 / 字段锁 / 表级锁

    针对锁粒度划分:行锁、字段锁、表锁、库锁

    (1)行锁:访问数据库的时候,锁定整个行数据,防止并发错误。

    (2)字段锁:访问数据库的时候,锁定表的某几个字段数据,防止并发错误。

    (3)表锁:访问数据库的时候,锁定整个表数据,防止并发错误。

    行锁 和 表锁 的区别:

    由浅入深举例说明:

    1)   创建测试表

    SYS@ORA11GR2>create table t_lock as select rownum as id,0 as type from dual connect by rownum <=3;
    
    Table created.
    
    SYS@ORA11GR2>select * from t_lock;
    
            ID       TYPE
    ---------- ----------
             1          0
             2          0
             3          0

    2)   会话 1:查询 type 0 的最小 id

    SYS@ORA11GR2>set time on;
    
    18:58:22 SYS@ORA11GR2>
    
    18:58:23 SYS@ORA11GR2>select min(id) from t_lock where type=0;
    
       MIN(ID)
    ----------
             1

    3)   会话 2:查询 type 0 的最小 id

    SYS@ORA11GR2>set time on
    
    18:59:31 SYS@ORA11GR2>select min(id) from t_lock where type=0;
    
       MIN(ID)
    ----------
             1

    4)   会话 1:将 ID 1 的这条记录的 type 置为 1

    19:00:53 SYS@ORA11GR2>update t_lock set type=1 where id=1;
    
    1 row updated.
    
    19:01:21 SYS@ORA11GR2>commit;
    
    Commit complete.
    
    19:01:37 SYS@ORA11GR2>select * from t_lock;
    
            ID       TYPE
    ---------- ----------
             1          1
             2          0
             3          0

    5)   会话 2:将 ID 1 的这条记录的 type 置为 2

    19:02:47 SYS@ORA11GR2>update t_lock set type=2 where id=1;
    
    1 row updated.
    
    19:03:11 SYS@ORA11GR2>commit;
    
    Commit complete.
    
    19:03:17 SYS@ORA11GR2>select * from t_lock;
    
            ID       TYPE
    ---------- ----------
             1          2
             2          0
             3          0

    6)   小结:

    我们看到 id 为 1 的 type 现在的值为 2,会话 1 将 type 更新为 1 的记录已经 “丢失”

    1.2. 悲观锁

    1)   会话 1:查询 id 为 2 的记录并进行锁定

    19:05:43 SYS@ORA11GR2>select * from t_lock where id=2 and type =0for update nowait;
    
            ID       TYPE
    ---------- ----------
             2          0
    

    2)   会话 2:查询 id 为 2 的记录,此时查询报错

    19:07:43 SYS@ORA11GR2>select * from t_lock where id=2 and type=0for update nowait;
    
    select * from t_lock where id=2 and type=0 for update nowait
    
                  *
    
    ERROR at line 1:
    
    ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired

    3)   会话 1:对 id 2 的记录进行更新。

    19:19:08 SYS@ORA11GR2>update t_lock set type=1 where id=2 and type=0;
    
    1 row updated.
    
    19:19:30 SYS@ORA11GR2>commit;
    
    Commit complete.
    
    19:19:39 SYS@ORA11GR2>select * from t_lock where id=2;
    
            ID       TYPE
    ---------- ----------
             2          1

    4)   会话 2:查询 id 2 的记录,由于已经将 id 2 type 已经变为 1,所以查不到数据了。

    19:19:15 SYS@ORA11GR2>select * from t_lock where id=2 and type=0for update nowait;
    
    no rows selected

    1.3 乐观锁

    1)   会话 1:查询 id 为 3 的伪列 ora_rowscn 的值

    19:22:00 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
    
            ID       TYPE ORA_ROWSCN
    ---------- ---------- ----------
             3          0    1246809

    2)   会话 2:查询 id 为 3 的伪列 ora_rowscn 的值

    19:23:01 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
    
            ID       TYPE ORA_ROWSCN
    ---------- ---------- ----------
             3          0    1246809

    3)   会话 1:更新 id 为 3 的 type 为 1

    19:24:22 SYS@ORA11GR2>update t_lock set type=1 where ora_rowscn=1246809 and id = 3;
    
    1 row updated.
    
    19:25:29 SYS@ORA11GR2>commit;
    
    Commit complete.

    验证:

    19:28:22 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
    
            ID       TYPE ORA_ROWSCN
    ---------- ---------- ----------
             3          1    1247164

    4)   会话 2:更新 id 为 3 的 type 为 1

    19:26:05 SYS@ORA11GR2>update t_lock set type=1 whereora_rowscn=1246809 and id =3;
    
    0 rows updated.

    验证:

    19:29:37 SYS@ORA11GR2>select id,type,ora_rowscn from t_lock where id = 3;
    
            ID       TYPE ORA_ROWSCN
    ---------- ---------- ----------
             3          1    1247164

    (因为会话 1 的事务更改了 id=3 的值,而且事务已经提交,事务的 ora_rowscn 已经变为 1247164,原来的 ora_rowscn=1246809 已经不存在,所以没有可更改的行了)

    1.4 死锁

    1)   创建测试表

    19:35:46 SYS@ORA11GR2>create table t_lock_1 (id number(2),name varchar2(15));
    
    Table created.
    
    19:35:57 SYS@ORA11GR2>create table t_lock_2 as select * from t_lock_1;
    
    Table created.
    
    19:36:24 SYS@ORA11GR2>insert into t_lock_1 values(1,''liubei'');
    
    1 row created.
    
    19:37:11 SYS@ORA11GR2>insert into t_lock_2 values (1,''guanyu'');
    
    1 row created.
    
    19:37:38 SYS@ORA11GR2>commit;
    
    Commit complete.
    
    19:37:43 SYS@ORA11GR2>select * from t_lock_1;
    
            ID NAME
    ---------- ---------------
             1 liubei
    
    19:38:01 SYS@ORA11GR2>select * from t_lock_2;
    
            ID NAME
    ---------- ---------------
             1 guanyu

    2)   会话 1:更新表 t_lock_1 的 id 字段为 1 的 name 为 “liuxuande”,不提交

    19:39:55 SYS@ORA11GR2>update t_lock_1 set name=''liuxuande'' where id =1;
    
    1 row updated.

    3)   会话 2:更新表 t_lock_2 的 id 字段为 1 的 name 为 “关云长”,不提交

     

    19:39:47 SYS@ORA11GR2>update t_lock_2 set name=''guanyunchang'' where id = 1;
    
    1 row updated.

    4)   会话 1:更新表 t_lock_2 的 id 字段为 1 的 name 为 “guanyunchang”,此时挂起状态

    19:40:30 SYS@ORA11GR2>update t_lock_2 set name=''guanyunchang'' where id =1;

    5)   会话 2:更新表 t_lock_1 的 id 字段为 1 的 name 为 “liuxuande”,此时挂起状态

     

    19:44:14 SYS@ORA11GR2>update t_lock_1 set name=''liuxuande'' where id =1;

    6)   会话 1:此时回到会话 1,出现死锁错误

    19:40:30 SYS@ORA11GR2>update t_lock_2 set name=''guanyunchang'' where id =1;
    
    update t_lock_2 set name=''guanyunchang'' where id =1
    
           *
    
    ERROR at line 1:
    
    ORA-00060: deadlock detected while waiting for resource

    会话 1 处于死锁状态,而会话 2 处于挂起状态。

    乐观锁 / 悲观锁

    悲观锁:顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

    乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于 write_condition 机制的其实都是提供的乐观锁。

    悲观锁 和 乐观锁的区别:

    两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行 retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适

    并发控制: 事务和锁的存在都是为了更好的解决并发访问造成的数据不一致性的的问题。
    乐观锁和悲观锁都是为了解决并发控制问题, 乐观锁可以认为是一种在最后提交的时候检测冲突的手段,而悲观锁则是一种避免冲突的手段。 
    乐观锁: 是应用系统层面和数据的业务逻辑层次上的(实际上并没有加锁,只不过大家一直这样叫而已),利用程序处理并发, 它假定当某一个用户去读取某一个数据的时候,其他的用户不会来访问修改这个数据,但是在最后进行事务的提交的时候会进行版本的检查,以判断在该用户的操作过程中,没有其他用户修改了这个数据。开销比较小 
    乐观锁的实现大部分都是基于版本 version 控制实现的, 当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 值加 1。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。
    除此之外,还可以通过时间戳的方式,通过提前读取,事后对比的方式实现。
    写到这里我突然想起了,java 的 cuurent 并发包里的 Automic 类的实现原理 CAS 原理 (Compare and Swap), 其实也可以看做是一种乐观锁的实现,通过将字段定义为 volalate,(不允许在线程中保存副本,每一次读取或者修改都要从内存区读取,或者写入到内存中), 通过对比应该产生的结果和实际的结果来进行保证原子操作,进行并发控制(对比和交换的正确性保证 是处理器的原子操作)。

    乐观锁的优势和劣势 
    优势:如果数据库记录始终处于悲观锁加锁状态,可以想见,如果面对几百上千个并发,那么要不断的加锁减锁,而且用户等待的时间会非常的长, 乐观锁机制避免了长事务中的数据库加锁解锁开销,大大提升了大并发量下的系统整体性能表现 所以如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以建议就要选择乐观锁定的方法, 而如果并发量不大,完全可以使用悲观锁定的方法。乐观锁也适合于读比较多的场景。 
    劣势: 但是乐观锁也存在着问题,只能在提交数据时才发现业务事务将要失败,如果系统的冲突非常的多,而且一旦冲突就要因为重新计算提交而造成较大的代价的话,乐观锁也会带来很大的问题,在某些情况下,发现失败太迟的代价会非常的大。而且乐观锁也无法解决脏读的问题

    同时我在思考一个问题,乐观锁是如何保证检查版本,提交和修改版本是一个原子操作呢? 也就是如何保证在检查版本的期间,没有其他事务对其进行操作? 
    解决方案: 将比较,更新操作写入到同一条 SQL 语句中可以解决该问题,比如 update table1 set a=1, b=2, version = version +1 where version = 1; mysql 自己能够保障单条 SQL 语句的原子操作性。 
    如果是多条 SQL 语句,就需要 mySQL 的事务通过锁机制来保障了。

    悲观锁: 完全依赖于数据库锁的机制实现的,在数据库中可以使用 Repeatable Read 的隔离级别(可重复读)来实现悲观锁,它完全满足悲观锁的要求(加锁)。 
    它认为当某一用户读取某一数据的时候,其他用户也会对该数据进行访问,所以在读取的时候就对数据进行加锁, 在该用户读取数据的期间,其他任何用户都不能来修改该数据,但是其他用户是可以读取该数据的, 只有当自己读取完毕才释放锁。

    悲观锁的优势和劣势 
    劣势:开销较大,而且加锁时间较长,对于并发的访问性支持不好。 
    优势 : 能避免冲突的发生,

    我们经常会在访问数据库的时候用到锁,怎么实现乐观锁和悲观锁呢?以 hibernate 为例,可以通过为记录添加版本或时间戳字段来实现乐观锁,一旦发现出现冲突了,修改失败就要通过事务进行回滚操作。可以用 session.Lock () 锁定对象来实现悲观锁(本质上就是执行了 SELECT * FROM t FOR UPDATE 语句)

    乐观锁和悲观所各有优缺点,在乐观锁和悲观锁之间进行选择的标准是:发生冲突的频率与严重性。 
    如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突太多或者冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略,它能避免冲突的发生。 如果要求能够支持高并发,那么乐观锁。 
    其实使用乐观锁 高并发 == 高冲突, 看看你怎么衡量了。

    但是现在大多数源代码开发者更倾向于使用乐观锁策略

    共享锁和排它锁是具体的锁,是数据库机制上的锁。 
    共享锁(读锁) 在同一个时间段内,多个用户可以读取同一个资源,读取的过程中数据不会发生任何变化。读锁之间是相互不阻塞的, 多个用户可以同时读,但是不能允许有人修改, 任何事务都不允许获得数据上的排它锁,直到数据上释放掉所有的共享锁 
    排它锁(写锁) 在任何时候只能有一个用户写入资源,当进行写锁时会阻塞其他的读锁或者写锁操作,只能由这一个用户来写,其他用户既不能读也不能写

    加锁会有粒度问题,从粒度上从大到小可以划分为 
    表锁 开销较小,一旦有用户访问这个表就会加锁,其他用户就不能对这个表操作了,应用程序的访问请求遇到锁等待的可能性比较高。 
    页锁是 MySQL 中比较独特的一种锁定级别,锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 
    行锁 开销较大,能具体的锁定到表中的某一行数据,但是能更好的支持并发处理, 会发生死锁

    事物: 用于保证数据库的一致性 
    所谓数据一致性,就是当多个用户试图同时访问一个数据库,它们的事务同时使用相同的数据时,可能会发生以下四种情况:丢失更新、脏读、不可重复读 和 幻读 
    所谓数据完整性, 数据库中的数据是从外界输入的,而数据的输入由于种种原因,会发生输入无效或错误信息。保证输入的数据符合规定, 
    数据完整性分为四类:实体完整性(Entity Integrity)、域完整性(Domain Integrity)、参照完整性(Referential Integrity)、用户定义的完整性(User-definedIntegrity)。    
     数据库采用多种方法来保证数据完整性,包括外键、约束、规则和触发器。 
      
    事务的 ACID 特性 
    原子性 Automicity,一个事务内的所有操作,要么全做,要么全不做 
    一致性 Consistency,数据库从一个一致性状态转到另一个一致性状态 
    独立性(隔离性)isolation, 一个事务在执行期间,对于其他事务来说是不可见的 
    持久性(Durability): 事务一旦成功提交,则就会永久性的对数据库进行了修改

    隔离级别: mySQL 默认的隔离级别是可重复读 
    在 SQL 中定义了四种隔离级别; 
    READ UNCOMMITED (未提交度) 事务之间的数据是相互可见的 
    READ COMMITED(提交读) 大多数数据库的默认隔离级别, 保证了不可能脏读,但是不能保证可重复读, 在这个级别里,数据的加锁实现是读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。 
    REPEATABLE READ (可重复读) 解决了不可重复读的问题,保证了在同一个事务之中,多次读取相同的记录的值的结果是一致的。 但是无法解决幻读。这个阶段的事务隔离性,在 mysql 中是通过基于乐观锁原理的多版本控制实现的。

    SERIALIZABLE (可串行化读) 最高的隔离级别,解决了幻读 ,它会在读取的每一行数据上都进行加锁, 有可能导致超时和锁争用的问题。 
    它的加锁实现是读取的时候加共享锁,修改删除更新的时候加排他锁,读写互斥,但是并发能力差。

    隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
    未提交读(Read uncommitted) 可能 可能 可能
    已提交读(Read committed) 不可能 可能 可能
    可重复读(Repeatable read) 不可能 不可能 可能
    可串行化(Serializable ) 不可能 不可能 不可能

    丢失更新: 当两个或者多个事务同时对某一数据进行更新的时候,事务 B 的更新可能覆盖掉事务 A 的更新,导致更新丢失 
    解决方案: 
    悲观锁的方式: 加锁,建议最后一步更新数据的时候加上排它锁,不要在一开始就加锁 
    执行到了最后一步更新,首先做一下加锁的查询确认数据有没有没改变,如果没有被改变,则进行数据的更新,否则失败。 一定要是做加锁的查询确认,因为如果你不加锁的话,有可能你在做确认的时候数据又发生了改变。 
    乐观锁的方式:使用版本控制实现

    级别高低是:脏读 < 不可重复读 < 幻读。所以,设置了最高级别的 SERIALIZABLE_READ 就不用在设置 REPEATABLE_READ 和 READ_COMMITTED 了

    脏读: 事务可以读取未提交的数据,比如: 
    事务 A 对某一个数据 data=1000 进行了修改: data = 2000, 但是还没有提交; 
    事务 B 读取 data 得到了结果 data = 2000, 
    由于某种原因事务 A 撤销了刚才的操作,数据 data = 1000 然后提交 
    这时事务 B 读取到的 2000 就是脏数据。正确的数据应该还是 1000 
    解决方法 : 把数据库的事务隔离级别调整到 READ_COMMITTED , 但是存在事务 A 与 B 都读取了 data,A 还未完成事务,B 此时修改了数据 data,并提交, A 又读取了 data,发现 data 不一致了,出现了不可重复读。

    不可重复读 在同一个事务之中,多次读取相同的记录的值的结果是不一样的,针对的是数据的修改和删除。 
    事务 A 读取 data = 1000, 事务还未完成; 
    事务 B 修改了 data = 2000, 修改完毕事务提交; 
    事务 A 再次读取 data, 发现 data = 2000 了,与之前的读取不一致的 
    解决办法; 把数据库的事务隔离级别调整到 REPEATABLE READ , 读取时候不允许其他事务修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题

    幻读: 当某个事务在读取某个范围内的记录的时候,另外一个事务在这个范围内增加了一行,当前一个事务再次读取该范围的数据的时候就会发生幻行,. 针对的是数据的插入 insert 
    解决方案 : 采用的是范围锁 RangeS RangeS_S 模式,锁定检索范围为只读 或者 把数据库的事务隔离级别调整到 SERIALIZABLE_READ, MySQL 中 InnoDB 和 XtraDB 利用(多版本并发控制)解决了幻读问题,

    加锁协议 
    一次封锁协议:因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。

    两段锁协议 将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁) 
    1. 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得 S 锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得 X 锁(排它锁(只有当前数据无共享锁,无排它锁之后才能获得),其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。 
    2. 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。 
    事务提交时(commit) 和事务回滚时(rollback)会自动的同时释放该事务所加的 insert、update、delete 对应的锁

    这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的

    死锁 指两个事务或者多个事务在同一资源上相互占用,并请求对方所占用的资源,从而造成恶性循环的现象。 
    出现死锁的原因: 
    1. 系统资源不足 
    2. 进程运行推进的顺序不当 
    3. 资源分配不当 
    产生死锁的四个必要条件 
    1. 互斥条件: 一个资源只能被一个进程使用 
    2. 请求和保持条件:进行获得一定资源,又对其他资源发起了请求,但是其他资源被其他线程占用,请求阻塞,但是也不会释放自己占用的资源。 
    3. 不可剥夺条件: 指进程所获得的资源,不可能被其他进程剥夺,只能自己释放 
    4. 环路等待条件: 进程发生死锁,必然存在着进程 - 资源之间的环形链 
    处理死锁的方法: 预防,避免,检查,解除死锁

    数据库也会发生死锁的现象,数据库系统实现了各种死锁检测和死锁超时机制来解除死锁,锁监视器进行死锁检测, 
    MySQL 的 InnoDB 处理死锁的方式是 将持有最少行级排它锁的事务进行回滚,相对比较简单的死锁回滚办法

    如何避免死锁? 
    避免死锁的核心思想是:系统对进程发出每一个资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入不安全或者死锁状态的动态策略。 什么是不安全的状态?系统能按某种进程推进顺序 (P1, P2, …, Pn),为每个进程 Pi 分配其所需资源,直至满足每个进程对资源的最大需求,使每个进程都可顺序地完成。此时称 P1, P2, …, Pn 为安全序列。如果系统无法找到一个安全序列,则称系统处于不安全状态。 
    其实第一和第二是预防死锁的方式,分别对应着的是破坏循环等待条件,和破坏不可剥夺条件。 
    第一: 加锁顺序: 对所有的资源加上序号,确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生,比如有资源 A, B,规定所有的线程只能按照 A–B 的方式获取资源, 这样就不会发生 线程 1 持有 A,请求 B,线程 2 持有 B 请求 A 的死锁情况发生了 
    第二: 获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求,同时放弃掉自己已经成功获得的所有资源的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。 
    第三:死锁的提前检测, 很出名的就是银行家算法。 每当一个线程获得了锁,会存储在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中,当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。 
    银行家算法: 思想: 
    当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配 
    如何预防死锁? 
    主要是通过设置某些外部条件去破坏死锁产生的四个必要条件中的一个或者几个。 
    破坏互斥条件,一般不采用,因为资源的互斥性这个特性有时候是我们所需要的; 
    破坏请求和保持条件可以一次性为一个进程或者线程分配它所需要的全部资源,这样在后面就不会发起请求资源的情况,但是这样资源的效率利用率很低; 
    破坏不可剥夺条件: 当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请,但是释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量; 
    破坏循环等待条件: ,可釆用顺序资源分配法。首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完。也就是说,只要进程提出申请分配资源 Ri,则该进程在以后的资源申请中,只能申请编号大于 Ri 的资源。 
    但是这样的话,编号必须相对稳定,这就限制了新类型设备的增加;尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使甩资源的顺序与系统规定顺序不同的情况,造成资源的浪费;此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦

    InnoDB 中事务隔离性的实现: 
    READ COMMITED 和 REPEATABLE READ 的隔离性实现:MVCC

    MVCC(多版本控制系统)的实现(目的: 实现更好的并发,可以使得大部分的读操作不用加锁, 但是 insert,delete,update 是需要加锁的): 
    MVCC 只在 READ COMMITED 和 REPEATABLE READ 这两个事务隔离性级别中使用。这是因为 MVCC 和其他两个不兼容,READ UNCOMMITED 总是读取最新的行,不关事务, 而 Seriablizable 则会对每一个读都加共享锁。 
    在 InnoDB 中,会在每行数据后添加两个额外的隐藏的值来实现 MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(即何时被删除)。 在实际操作中,存储的并不是时间,而是系统的版本号每开启一个新事务,系统的版本号就会递增。 
    通过 MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。 
    select (不加锁): 满足两个条件的结果才会被返回: 
    1. 创建版本号 <= 当前事务版本号,小于意味着在该事务之前没有其他事务对其进行修改,等于意味着事务自身对其进行了修改; 
    2. 删除版本号 > 当前事务版本号 意味着删除操作是在当前事务之后进行的,或者删除版本未定义,意味着这一行只是进行了插入,还没有删除过。 
    INSERT ; 为新插入的每一行保存当前事务的版本号作为创建版本号 
    DELETE ; 为删除的行保存当前事务的版本号为删除版本号 
    UPDATE; 为修改的每一行保存当前事务的版本号作为创建版本号

    “读” 与 “读” 的区别 
    MySQL 中的读,和事务隔离级别中的读,是不一样的, 在 REPEATABLE READ 级别中,通过 MVCC 机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据存储在缓存等地方的数据),不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

    对于这种读取历史数据(缓存数据)的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在 MVCC 中: 
    快照读:就是 select ,是不加锁的, 在 REPEATABLE READ 和 READ COMMITED 级别中 select 语句不加锁。 
    select * from table ….; 
    当前读:插入 / 更新 / 删除操作,属于当前读,处理的都是当前的数据,需要加锁。 
    select * from table where ? lock in share mode; 
    select * from table where ? for update; 
    insert; 
    update ; 
    delete; 
    事务的隔离级别实际上都是定义了当前读的级别MySQL 为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得 select 不用加锁。而 update、insert 这些 “当前读”,就需要另外的模块来解决了。(这是因为 update、insert 的时候肯定要读取数据库中的值来与当前事务要写入的值进行对比,看看在该事务所处理的数据在这一段时间内有没有被其他事务所操作(就是先读取数据库中数据的版本号与当前的版本号做检查)

    为了解决当前读中的幻读问题,MySQL 事务使用了 Next-Key 锁。Next-Key 锁是行锁和 GAP(间隙锁)的合并 
    GAP(间隙锁)就是在两个数据行之间进行加锁,防止插入操作

    行锁防止别的事务修改或删除,解决了数据不可重复读的问题

    行锁防止别的事务修改或删除,GAP 锁防止别的事务新增,行锁和 GAP 锁结合形成的的 Next-Key 锁共同解决了 RR 级别在读数据时的幻读问题

    InnoDB 中 Serializable 的隔离性实现 
    Serializable 级别使用的是悲观锁的理论, 读加共享锁,写加排他锁,读写互斥, 在 Serializable 这个级别,select 语句还是会加锁的。

    应用场景

    ORM 框架中悲观锁乐观锁的应用

         一般悲观锁、乐观锁都需要都通过 sql 语句的设定、数据的设计结合代码来实现,例如乐观锁中的版本号字段,单纯面向数据库操作,是需要自己来实现乐观锁的,简言之,也就是版本号或时间戳字段的维护是程序自己维护的,自增、判断大小确定是否更新都通过代码判断实现。数据库进提供了乐观、悲观两个思路进行并发控制。

         对于常用 java 持久化框架,对于数据库的这一机制都有自己的实现,以 Hibernate 为例,总结一下 ORM 框架中悲观锁乐观锁的应用

    1、Hibernate 的悲观锁:

         基于数据库的锁机制实现。如下查询语句:

    String hqlStr ="from TUser as user where user.name=Max";  
    Query query = session.createQuery(hqlStr);  
    query.setLockMode("user",LockMode.UPGRADE); //加锁  
    List userList = query.list();//执行查询,获取数据

           观察运行期 Hibernate 生成的 SQL 语句:

    select 
    tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, 
    tuser0_.user_type as user_type, tuser0_.sex as sex 
    from t_user tuser0_ 
    where (tuser0_.name=''Erica'' ) 
    for update  

         这里 Hibernate 通过使用数据库的 for update 子句实现了悲观锁机制。对返回的所有 user 记录进行加锁。
    2、Hibernate 的加锁模式有:
         LockMode.NONE : 无锁机制。
         LockMode.WRITE :Hibernate 在写操作(Insert 和 Update)时会自动获取写锁。
         LockMode.READ : Hibernate 在读取记录的时候会自动获取。
         这三种锁机制一般由 Hibernate 内部使用,如 Hibernate 为了保证 Update 过程中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。
         LockMode.UPGRADE :利用数据库的 for update 子句加锁。
         LockMode. UPGRADE_NOWAIT :Oracle 的特定实现,利用 Oracle 的 for update nowait 子句实现加锁。
         注意,只有在查询开始之前(也就是 Hiberate 生成 SQL 之前)设定加锁,才会真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含 for update 子句的 Select SQL 加载进来,所谓数据库加锁也就无从谈起。

    3、Hibernate 的乐观锁

         Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用 Hibernate 提供的透明化乐观锁实现,将大大提升我们的生产力。Hibernate 中可以通过 class 描述符的 optimistic-lock 属性结合 version 描述符指定。具体实现方式如下:
         现在,我们为之前示例中的 TUser 加上乐观锁机制。
    实现一、 配置 optimistic-lock 属性:

    <hibernate-mapping>  
         <class name="org.hibernate.sample.TUsertable="t_user
               dynamic-update="true" dynamic-insert="true" optimistic-lock="version">  
               ……  
         </class>  
    </hibernate-mapping> 

     optimistic-lock 属性有如下可选取值:
          none:无乐观锁
          version:通过版本机制实现乐观锁
          dirty:通过检查发生变动过的属性实现乐观锁
          all:通过检查所有属性实现乐观锁

         通过 version 实现的乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也是 Hibernate 中,目前唯一在数据对象脱离 Session 发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择 version 方式作为 Hibernate 乐观锁实现机制。
    实现二、添加一个 Version 属性描述符

    <hibernate-mapping>  
        <class name="org.hibernate.sample.TUser" table="t_user"   
              dynamic-update="true" dynamic-insert="true" optimistic-lock="version">   
        <id name="id" column="id" type="java.lang.Integer">  
            <generator class="native"/>  
        </id>  
        <version column="version" name="version" type="java.lang.Integer"/>  
    ……  
         </class>  
    </hibernate-mapping>  

         注意 version 节点必须出现在 ID 节点之后。这里声明了一个 version 属性,用于存放用户的版本信息,保存在 TUser 表的 version 字段中。

    测试:

         此时如果我们尝试编写一段代码,更新 TUser 表中记录数据,如:

    Criteria criteria = session.createCriteria(TUser.class);  
    criteria.add(Expression.eq("name","Max"));  
    List userList = criteria.list();  
    TUser user =(TUser)userList.get(0);  
    Transaction tx = session.beginTransaction();  
    user.setUserType(1); //更新UserType字段  
    tx.commit();  

         每次对 TUser 进行更新的时候,我们可以发现,数据库中的 version 都在递增。而如果我们尝试在 tx.commit 之前,启动另外一个 Session,对名为 Max 的用户进行操作,下面模拟并发更新时的情况:

    Session session= getSession();  
    Criteria criteria = session.createCriteria(TUser.class);  
    criteria.add(Expression.eq("name","Max"));  
    Session session2 = getSession();  
    Criteria criteria2 = session2.createCriteria(TUser.class);  
    criteria2.add(Expression.eq("name","Max"));  
    List userList = criteria.list();  
    List userList2 = criteria2.list();TUser user =(TUser)userList.get(0);  
    TUser user2 =(TUser)userList2.get(0);  
    Transaction tx = session.beginTransaction();  
    Transaction tx2 = session2.beginTransaction();  
    user2.setUserType(99);  
    tx2.commit();  
    user.setUserType(1);  
    tx.commit();  

         执行并发更新的代码,在 tx.commit () 处抛出 StaleObjectStateException 异常,并指出版本检查失败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行相应处理。

         这就是 hibernate 实现悲观锁和乐观锁的主要方式。

    我们今天的关于Mysql 锁机制 -- 乐观锁 & 悲观锁mysql乐观锁与悲观锁的分享已经告一段落,感谢您的关注,如果您想了解更多关于Hibernate JPA 悲观锁,乐观锁、Hibernate学习_021_Hibernate中的事务控制+乐观锁+悲观锁、JAVA 乐观锁、悲观锁、Java 并发 行级锁 / 字段锁 / 表级锁 乐观锁 / 悲观锁 共享锁 / 排他锁 死锁的相关信息,请在本站查询。

    本文标签:

    上一篇DEFT Linux 6.1 发布,数据取证工具箱(linux取证分析)

    下一篇如何在 mysql 数据库生成百万条数据来测试页面加载速度(mysql生成大量测试数据)