mysql的锁----悲观锁和乐观锁
背景
我们知道,在并发编程中,锁是一种常见用于控制共享资源访问的机制,为防止并发访问情况下代码出现的异常,需要对相关业务逻辑代码段进行加锁,让请求按照指定的逻辑进行。看过《mysql高性能》,书中提到mysql有锁机制,分悲观锁和乐观锁,而悲观锁又有读锁和写锁。mysaim引擎默认是表级锁,inodb默认是行数,当然出现范围条件锁定情况下,行锁也可以升级为表锁。
mysql悲观锁
用到悲观锁的常见业务逻辑操作为 先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。
当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生阻塞等待。因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
注意点:
select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在mysql中用悲观锁务必要确定走了索引,而不是全表扫描
实验操作:
- 建表
CREATE TABLE `goods` ( `id` int(11) NOT NULL, `num` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 插入几条数据

- 开启两个mysql窗口
窗口1执行如下代码
start transaction; //锁定第四行数据 SELECT * FROM goods WHERE goods.id=4 for update
//暂不提交
commit;
窗口2执行如下代码
//更新第四行数据,发现一直阻塞等待,直到Lock wait timeout UPDATE goods set num=num-1 WHERE goods.id=4
- 结果:发现窗口2一直处于等待状态,在还未到锁定超时的情况下,只要窗口1提交,窗口2马上就提交,说明窗口1锁定了第四行。这在某些业务场景中非常有用,比如减库存的操作,防止超卖,下面伪代码如下:
//下单操作,一锁,二查,三操作
public function index()
{
try{
Db::startTrans();
$res =Db::query('select `num` from goods where id=4 limit 1 for update');
//此处如果并发执行的话,只能有一个获取到锁,进行后面业务逻辑
if($res[0]['num']>0){//如果库存大于0
//生成订单 ...
//推送消息 ...
//更新库存 其它业务逻辑操作
Db::table('goods')->where(['id'=>4])->update(['num' => $res[0]['num']-1]);
Db::commit();
return 'ok';
}else{//库存为0就反悔失败信息
Db::commit();
return 'fail';
}
}catch(\Exception $e){
var_dump($e->getMessage());
Db::rollback();
return 'fail';
}
发现在并发情况下,库存不会出现为负数。
- 补充点:
由于mysql默认的事物级别为可重复读,在同一个事务里,SELECT的结果是事务开始时时间点的状态,因此,同样的SELECT操作读到的结果会是一致的。但是,会有幻读现象
解释
在同一个事物中,读取到的数据永远都是当事物开启那一瞬间的数据状态,在整个事物中读取不到别的客户端更新的数据和新增的数据,这会出现问题,例如在整个事物期间,其它客户端新增了一条数据或者修改了某条数据,而该事物接下来正好需要再次更新这条数据或者正好需要再次新增这条数据,坏了,发现可能mysql会告诉你,你要新增的这条数据已经有人帮你抢先一步新增了,而你要修改的那条数据在你修改之前,早就有人改动了,你再改动可能会出现问题。
情景一 、
例如在事务中,我查询了某个商品库存为1,然后我准备执行接下来的生成订单,推消息,改库存等操作,就在你进行这些业务逻辑的同时,另一个客户端已经将库存减到为0了,而你浑然不知,等你业务写完了,事务开始一提交,发现库存变为-1了,坏了,这可不是自己想要的结果。
情景二
例如在事务中,你查询到商品表最近一条商品的id为587,然后你在事务中准备在新增一条商品为588,就在你新增的过程中,另一个客户端也新增了一条同样的商品id为588记录,假如你此时再去查询,发现最大id还是587,但是当你insert的时候告诉你插入行冲突了,这大概就是幻读吧
幻读情景1
t Session A Session B | | START TRANSACTION; START TRANSACTION; | | SELECT * FROM t_bitfly; | empty set | INSERT INTO t_bitfly | VALUES (1, ‘a’); | | SELECT * FROM t_bitfly; | empty set | COMMIT; | | SELECT * FROM t_bitfly; | empty set | | INSERT INTO t_bitfly VALUES (1, ‘a’); | ERROR 1062 (23000): | Duplicate entry ‘1’ for key 1 v (shit, 刚刚明明告诉我没有这条记录的)
幻读情景2
t Session A Session B | | START TRANSACTION; START TRANSACTION; | | SELECT * FROM t_bitfly; | +——+——-+ | | id | value | | +——+——-+ | | 1 | a | | +——+——-+ | INSERT INTO t_bitfly | VALUES (2, ‘b’); | | SELECT * FROM t_bitfly; | +——+——-+ | | id | value | | +——+——-+ | | 1 | a | | +——+——-+ | COMMIT; | | SELECT * FROM t_bitfly; | +——+——-+ | | id | value | | +——+——-+ | | 1 | a | | +——+——-+ | | UPDATE t_bitfly SET value=’z’; | Rows matched: 2 Changed: 2 Warnings: 0 | (怎么多出来一行)
解决方案:
在事务中加互斥锁,锁住要修改的行或者锁定相关数据行,不允许新增。
mysql乐观锁
乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,然后按照如下方式实现:
1. SELECT data AS old_data, version AS old_version FROM …;
2. 根据获取的数据进行业务操作,得到new_data和new_version
3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
// 乐观锁获取成功,操作完成
} else {
// 乐观锁获取失败,回滚并重试
}
乐观锁是否在事务中其实都是无所谓的,其底层机制是这样:在数据库内部update同一行的时候是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时再次对比版本号确认与之前获取的相同,并更新版本号,即可确认这之间没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。
总结:
- 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能
- 乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方
来源:
https://www.cnblogs.com/phpper/p/7345332.html
https://blog.csdn.net/Marvel__Dead/article/details/70209641

浙公网安备 33010602011771号