mysql悲观锁以及乐观锁总结和实践

注:本文乃转载,原文作者@青葱岁月

悲观锁介绍(百科):

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

 

使用场景举例:以MySQL InnoDB为例

商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

 

1如果不采用锁,那么操作方法如下:

//1.查询出商品信息

select status from t_goods where id=1;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

 

上面这种场景在高并发访问的情况下很可能会出现问题。

前面已经提到,只有当goods status为1时才能对该商品下单,上面第一步操作中,查询出来的商品status为1。但是当我们执行第三步Update操作的时候,有可能出现其他人先一步对商品下单把goods status修改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据不一致。所以说这种方式是不安全的。

 

2使用悲观锁来实现:

在上面的场景中,商品信息从查询出来到修改,中间有一个处理订单的过程,使用悲观锁的原理就是,当我们在查询出goods信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为goods被锁定了,就不会出现有第三者来对其进行修改了。

 

注:要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

 

我们可以使用命令设置MySQL为非autocommit模式:

set autocommit=0;

 

设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:

//0.开始事务

begin;/begin work;/start transaction; (三者选一就可以)

//1.查询出商品信息

select status from t_goods where id=1 for update;

//2.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2

update t_goods set status=2;

//4.提交事务

commit;/commit work;

 

注:上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交,在这里就不细表了。

 

上面的第一步我们执行了一次查询操作:select status from t_goods where id=1 for update;

与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

 

注:需要注意的是,在事务中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。拿上面的实例来说,当我执行select status from t_goods where id=1 for update;后。我在另外的事务中如果再次执行select status from t_goods where id=1
for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_goods where id=1;则能正常查询出数据,不会受第一个事务的影响。

 

补充:MySQL select…for update的Row Lock与Table Lock

上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

 

举例说明:

数据库表t_goods,包括id,status,name三个字段,id为主键,数据库中记录如下;

mysql> select * from t_goods;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
|  2 |      1 | 装备 |
+----+--------+------+
2 rows in set

mysql>

注:为了测试数据库锁,我使用两个console来模拟不同的事务操作,分别用console1、console2来表示。 

 

例1: (明确指定主键,并且有此数据,row lock)

console1:查询出结果,但是把该条数据锁定了

mysql> select * from t_goods where id=1 for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
+----+--------+------+
1 row in set

mysql>

console2:查询被阻塞

mysql> select * from t_goods where id=1 for update;  

console2:如果console1长时间未提交,则会报错

mysql> select * from t_goods where id=1 for update;
ERROR 1205 : Lock wait timeout exceeded; try restarting transaction

例2: (明确指定主键,若查无此数据,无lock)

console1:查询结果为空

mysql> select * from t_goods where id=3 for update;
Empty set  

console2:查询结果为空,查询无阻塞,说明console1没有对数据执行锁定

mysql> select * from t_goods where id=3 for update;
Empty set  

例3: (无主键,table lock)

console1:查询name=道具 的数据,查询正常

mysql> select * from t_goods where name='道具' for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
+----+--------+------+
1 row in set  

mysql>  

console2:查询name=装备 的数据,查询阻塞,说明console1把表给锁住了

mysql> select * from t_goods where name='装备' for update;  

console2:若console1长时间未提交,则查询返回为空

mysql> select * from t_goods where name='装备' for update;
Query OK, -1 rows affected  

例4: (主键不明确,table lock)

console1:查询正常

mysql> begin;
Query OK, 0 rows affected  

mysql> select * from t_goods where id>0 for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
|  2 |      1 | 装备 |
+----+--------+------+
2 rows in set  

mysql>  

console2:查询被阻塞,说明console1把表给锁住了

mysql> select * from t_goods where id>1 for update;  

例5: (主键不明确,table lock)

console1:

mysql> begin;
Query OK, 0 rows affected  

mysql> select * from t_goods where id<>1 for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  2 |      1 | 装备 |
+----+--------+------+
1 row in set  

mysql>  

console2:查询被阻塞,说明console1把表给锁住了

mysql> select * from t_goods where id<>2 for update;  

console1:提交事务

mysql> commit;
Query OK, 0 rows affected  

console2:console1事务提交后,console2查询结果正常

 

mysql> select * from t_goods where id<>2 for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
+----+--------+------+
1 row in set  

mysql>  

以上就是关于数据库主键对MySQL锁级别的影响实例,需要注意的是,除了主键外,使用索引也会影响数据库的锁定级别

 

举例:

我们修改t_goods表,给status字段创建一个索引

修改id为2的数据的status为2,此时表中数据为:  

 

mysql> select * from t_goods;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
|  2 |      2 | 装备 |
+----+--------+------+
2 rows in set  

mysql>

例6: (明确指定索引,并且有此数据,row lock)

console1:

mysql> select * from t_goods where status=1 for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  1 |      1 | 道具 |
+----+--------+------+
1 row in set  

mysql>  

console2:查询status=1的数据时阻塞,超时后返回为空,说明数据被console1锁定了

mysql> select * from t_goods where status=1 for update;
Query OK, -1 rows affected  

console2:查询status=2的数据,能正常查询,说明console1只锁住了行,未锁表

 

mysql> select * from t_goods where status=2 for update;
+----+--------+------+
| id | status | name |
+----+--------+------+
|  2 |      2 | 装备 |
+----+--------+------+
1 row in set  

mysql>  

例7: (明确指定索引,若查无此数据,无lock)

console1:查询status=3的数据,返回空数据

mysql> select * from t_goods where status=3 for update;
Empty set  

console2:查询status=3的数据,返回空数据

mysql> select * from t_goods where status=3 for update;
Empty set  

但是悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁,具体参见下面介绍:

 

乐观锁介绍:

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。那么我们如何实现乐观锁呢,一般来说有以下2种方式:

1.使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:

如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。

 

2.乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

 

使用举例:以MySQL InnoDB为例

还是拿之前的实例来举:商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

 

下单操作包括3步骤:

1.查询出商品信息

select (status,status,version) from t_goods where id=#{id}

2.根据商品信息生成订单

3.修改商品status为2

update t_goods 

set status=2,version=version+1

where id=#{id} and version=#{version};

 

那么为了使用乐观锁,我们首先修改t_goods表,增加一个version字段,数据默认version值为1。

t_goods表初始数据如下:

Sql代码  

  1. mysql> select * from t_goods;  
  2. +----+--------+------+---------+  
  3. | id | status | name | version |  
  4. +----+--------+------+---------+  
  5. |  1 |      1 | 道具 |       1 |  
  6. |  2 |      2 | 装备 |       2 |  
  7. +----+--------+------+---------+  
  8. 2 rows in set  
  9.   
  10. mysql>  

对于乐观锁的实现,我使用MyBatis来进行实践,具体如下:

Goods实体类:

Java代码  

  1. /** 
  2.  * ClassName: Goods <br/> 
  3.  * Function: 商品实体. <br/> 
  4.  * date: 2013-5-8 上午09:16:19 <br/> 
  5.  * @author chenzhou1025@126.com 
  6.  */  
  7. public class Goods implements Serializable {  
  8.   
  9.     /** 
  10.      * serialVersionUID:序列化ID. 
  11.      */  
  12.     private static final long serialVersionUID = 6803791908148880587L;  
  13.       
  14.     /** 
  15.      * id:主键id. 
  16.      */  
  17.     private int id;  
  18.       
  19.     /** 
  20.      * status:商品状态:1未下单、2已下单. 
  21.      */  
  22.     private int status;  
  23.       
  24.     /** 
  25.      * name:商品名称. 
  26.      */  
  27.     private String name;  
  28.       
  29.     /** 
  30.      * version:商品数据版本号. 
  31.      */  
  32.     private int version;  
  33.       
  34.     @Override  
  35.     public String toString(){  
  36.         return "good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;  
  37.     }  
  38.   
  39.     //setter and getter  
  40.   
  41. }  

GoodsDao

Java代码  

  1. /** 
  2.  * updateGoodsUseCAS:使用CAS(Compare and set)更新商品信息. <br/> 
  3.  * 
  4.  * @author chenzhou1025@126.com 
  5.  * @param goods 商品对象 
  6.  * @return 影响的行数 
  7.  */  
  8. int updateGoodsUseCAS(Goods goods);  

mapper.xml

Xml代码  

  1. <update id="updateGoodsUseCAS" parameterType="Goods">  
  2.     <![CDATA[ 
  3.         update t_goods 
  4.         set status=#{status},name=#{name},version=version+1 
  5.         where id=#{id} and version=#{version} 
  6.     ]]>  
  7. </update>  

GoodsDaoTest测试类

Java代码  

  1. @Test  
  2. public void goodsDaoTest(){  
  3.     int goodsId = 1;  
  4.     //根据相同的id查询出商品信息,赋给2个对象  
  5.     Goods goods1 = this.goodsDao.getGoodsById(goodsId);  
  6.     Goods goods2 = this.goodsDao.getGoodsById(goodsId);  
  7.       
  8.     //打印当前商品信息  
  9.     System.out.println(goods1);  
  10.     System.out.println(goods2);  
  11.       
  12.     //更新商品信息1  
  13.     goods1.setStatus(2);//修改status为2  
  14.     int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);  
  15.     System.out.println("修改商品信息1"+(updateResult1==1?"成功":"失败"));  
  16.       
  17.     //更新商品信息2  
  18.     goods1.setStatus(2);//修改status为2  
  19.     int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods1);  
  20.     System.out.println("修改商品信息2"+(updateResult2==1?"成功":"失败"));  
  21. }  

输出结果:

Shell代码  

  1. good id:1,goods status:1,goods name:道具,goods version:1  
  2. good id:1,goods status:1,goods name:道具,goods version:1  
  3. 修改商品信息1成功  
  4. 修改商品信息2失败  

说明:

在GoodsDaoTest测试方法中,我们同时查出同一个版本的数据,赋给不同的goods对象,然后先修改good1对象然后执行更新操作,执行成功。然后我们修改goods2,执行更新操作时提示操作失败。此时t_goods表中数据如下:

Sql代码  

  1. mysql> select * from t_goods;  
  2. +----+--------+------+---------+  
  3. | id | status | name | version |  
  4. +----+--------+------+---------+  
  5. |  1 |      2 | 道具 |       2 |  
  6. |  2 |      2 | 装备 |       2 |  
  7. +----+--------+------+---------+  
  8. 2 rows in set  
  9.   
  10. mysql>   

我们可以看到 id为1的数据version已经在第一次更新时修改为2了。所以我们更新good2时update where条件已经不匹配了,所以更新不会成功,具体sql如下:

Sql代码  

  1. update t_goods   
  2. set status=2,version=version+1  
  3. where id=#{id} and version=#{version};  

这样我们就实现了乐观锁

时间: 2024-11-01 22:28:16

mysql悲观锁以及乐观锁总结和实践的相关文章

数据库悲观锁和乐观锁

一.Oracle Oracle数据库悲观锁与乐观锁是本文我们主要要介绍的内容.有时候为了得到最大的性能,一般数据库都有并发机制,不过带来的问题就是数据访问的冲突.为了解决这个问题,大多数数据库用的方法就是数据的锁定. 数据的锁定分为两种方法,第一种叫做悲观锁,第二种叫做乐观锁.什么叫悲观锁呢,悲观锁顾名思义,就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住.而乐观锁就是认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数

【hibernate框架】使用hibernate实现悲观锁和乐观锁

四种隔离机制不要忘记:(1,2,4,8) 1.read-uncommitted:能够去读那些没有提交的数据(允许脏读的存在) 2.read-committed:不会出现脏读,因为只有另一个事务提交才会读取来 结果,但仍然会出现不可重复读和幻读现象. 4.repeatable read:MySQL默认.可重复读,读数据读出来之后给它加把锁, 其他人先别更新,等我用完了你再更新.你的事务没完,其他事务就不可能改这条记录. 8.serializable:序列化,最高级别.一个一个来,不去并发.效率最低

关于悲观锁和乐观锁的区别(最直观理解)

悲观锁(Pessimistic Lock): 每次拿数据的时候都会担心会被别人修改(疑心重很悲观),所以每次在拿数据的时候都会上锁.确保自己使用的过程中不会被别人访问,自己使用完后再解锁. 期间需要访问该数据的都会等待. 乐观锁(Optimistic Lock): 每次拿数据的时候都完全不担心会被别人修改(心态好很乐观),所以每次在拿数据的时候都不会上锁.但是在更新数据的时候去判断该期间是否被别人修改过(使用版本号等机制),期间该数据可以随便被其他人读取. 两种锁各有优缺点,不能单纯的定义哪个好

悲观锁与乐观锁

1.场景 并发更新数据,若不加锁可能导致新数据被老数据覆盖. 2.思想 乐观锁,其实就是不加锁.它有乐观的态度,认为"我操作的数据没有被别人更新".提交时万一发现已被修改就回滚. 悲观锁,悲观的态度,数据时时刻刻都会被更新,我就先将其先锁住,让别人用不了,我操作完成后再释放掉. 3.实现 乐观锁.为数据维护一个版本号(如在表中多加一列),当被更新时版本号自增.获取数据时同时拿到数据本身及当前版本号,若提交前数据被别人更新,那么在提交时服务端发现版本号过低就不提交. 悲观锁.需要数据库级

乐观锁与悲观锁及应用举例

  最近因为在工作中需要,学习了乐观锁与悲观锁的相关知识,这里我通过这篇文章,把我自己对这两个"锁家"兄弟理解记录下来;       - 悲观锁:正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)的修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态.悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据).       以常用的m

程序中的乐观锁与悲观锁,以及动手实现乐观锁 (转)

概念: 这里抛开数据库来谈乐观锁和悲观锁,扯上数据库总会觉得和Java离得很远. 悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放. 乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作. 从解释上可以看出,悲观锁具有很强的独占性,也是最安全的.而乐观锁很开放,效率高,安全性比悲观锁低,因为在乐观锁检查数

深入理解Yii2.0乐观锁与悲观锁的原理与使用

本文介绍了深入理解Yii2.0乐观锁与悲观锁的原理与使用,分享给大家,具体如下: Web应用往往面临多用户环境,这种情况下的并发写入控制, 几乎成为每个开发人员都必须掌握的一项技能. 在并发环境下,有可能会出现脏读(Dirty Read).不可重复读(Unrepeatable Read). 幻读(Phantom Read).更新丢失(Lost update)等情况.具体的表现可以自行搜索. 为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念. 这里我们都不作解释了,拿这些关键

【锁】Oracle锁系列

[锁]Oracle锁系列 1  BLOG文档结构图           2  前言部分 2.1  导读和注意事项 各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不知道的知识,~O(∩_∩)O~: ① 锁的概念.分类.及其模拟 ② 查询锁的视图及视图之间的关联 ③ 锁的参数(DML_LOCKS.DDL_LOCK_TIMEOUT) ④ FOR UPDATE及FOR UPDATE OF系列 ⑤ 带ONLINE和不带ONLINE创建索引的锁情况(是否阻塞DML操作) ⑥ 包或存

详解 hibernate 悲观锁 乐观锁 深入分析 代码实例

首先,为什么要有锁这种概念和技术呢? 什么是锁( locking ) 业务逻辑的实现过程中,往往需要保证数据访问的排他性.如在金融系统的日终结算处理中,我们希望针对某个 cut-off 时间点的数据进行处理,而不希望在结算进行过程中(可能是几秒种,也可能是几个小时),数据再发生变化.此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓的 "锁" ,即给我们选定的目标数据上锁,使其无法被其他程序修改.Hibernate 支持两种锁机制: