悲观锁与乐观锁

锁及事务隔离水平的关系

  1. 事务的隔离水平:指同时运行多个事务时,事务与事务之间的影响程度。其实是利用了各种类型的锁来解决并发问题,数据库内部基础机制,执行事务其实也在执行锁。(其他章有讲)

  2. 锁: 主要指悲观锁乐观锁

    只有当事务的隔离水平无法解决并发问题时,才需要手动设置锁。

                 |--共享所
         |--行锁-|
|--悲观锁-|       |--排它锁
|        |--表锁(不使用索引时)
|       
|--乐观锁
--排他锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;

--共享锁
SELECT * FROM table_name WHERE ... FOR UPDATE;

在事务中,关于锁的锁定,对于update、delete、insert,Innodb会自动加排它锁
普通的select,不会加任何锁。我们可以显示的加共享锁或排它锁。

综合:隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

悲观锁

悲观锁示例1:

//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 where …;

//4.提交事务
commit;/commit work;

上面的第一步我们执行了一次查询操作: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

MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定索引(如主键),MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

1. 明确指出索引(如主键),查有数据,row lock

事务1:(console1)

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

mysql>  

事务2:

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

说明:事务1未提交,事务2将阻塞。长时间会报错。

2. 明确指出索引(如主键),查无数据,无lock

事务1:

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

事务2:

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

说明:查询结果为空,查询无阻塞,事务1无锁定

3. 没有用到索引、索引不明确(范围查),table lock

事务1:无索引时

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

mysql>  

事务2:

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

说明:无索引,事务1把整个表锁定了。查询这个表的其他字段也会阻塞。

事务1:(范围查时)

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>  

事务2:

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

说明:事务2会阻塞。当事务1提交后,事务2会查询出来。

注:①没有用到索引
      ②索引不明确,如 where id>1
将会表锁(table lock)。

乐观锁

乐观锁的实现方式

1. 使用数据版本(Version)

为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。

当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
以确保期间没有更新过。

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

2. 增加时间戳

在记录上增加修改的时间戳(也可利用ora_rowscn伪列)。即在事务开始时,获取该记录的时间戳,修改时,校验该时间戳,若一致则修改。(跟version差不多)

3. 更新时带入原始数据(见总结)。

悲观锁与乐观锁的对比

  1. 这两种策略的核心其实就是持有锁的时间的起止点不同悲观锁是从读取记录的那一刻就开始了,而乐观锁只从UPDATE那一刻开始;结束的点两者是一样的,都是发出commit或rollback命令。所以,悲观锁策略会使锁的持续时间更长,而乐观锁的持续时间则较短。其影响就是并发。悲观锁的并发性低于乐观锁

  2. 无论是采用哪种策略,都要保证数据的完整性。所以,在采用乐观锁策略时,是有可能出现数据的不完整。举例来说:储户甲的存款余额100元,假设在几乎相同的时刻,发生了两笔业务,业务1为其存入了50元,另一个业务是其网上购物消费了30元。显然,这两个操作结束后,甲的存款余额应为120元(100+50-30)。但我们设想一下在数据库层面,可能出现这种情况,当其在银行柜台存入50元时,银行操作员收到了甲存入的50元现金,并通过 select 语句看到甲的当前余额为100元(其发出的指令是下面的语句:

select 余额
   from 存款余额表
where 储户帐号=储户甲的银行帐号;)
--接着:
update 存款余额表
      set 余额=150
    where 储户帐号=储户甲的银行帐号;

但就在其看到甲的余额为100元,到其修改甲的余额为150这期间,甲在网上的消费行为导致交易系统已经将甲的余额变成了70元(100-30)并提交了。当银行员工发出的指令也被提交后,甲的余额变成了150元,相当于甲网上消费的行为没有产生任何扣款。这显然是不正确的,更是要避免出现的。

策略1:悲观锁

select 余额
   from 存款余额表
where 储户帐号=储户甲的银行帐号 for update;)

不足:当上述占时过长,会影响其他(如存钱、消费等)操作阻塞。

因为考虑到悲观锁策略可以产生的这种问题,所以,我们在设计应用时,可以采取一些其它方法来避免上述情况的发生。其思想就是在真正提交时,确认要修改的数据没有变化过。主要的方法如下:

策略2:乐观锁

(1)更新时带入原始的数据。

update 存款余额表
      set 余额=150
    where 储户帐号=储户甲的银行帐号 and 余额=100;

(2)在记录上增加修改的时间戳(也可利用ora_rowscn伪列)。即在事务开始时,获取该记录的时间戳,修改时,校验该时间戳,若一致则修改。
(3)其实,我上面举的这个例子,如果在业务设计时,选择更新指令为

update 存款余额表
      set 余额=余额+50
    where 储户帐号=储户甲的银行帐号;

那么,即使是在乐观锁策略的情况下,依然可以保证数据的正确性和完整性。

总结

在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.

对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值