翼度科技»论坛 编程开发 mysql 查看内容

MySQL乐观锁与悲观锁

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
说明

遇见并发情况,需要保证数据的准确性,也就是与正确的预期一致,此时就会用到锁。
锁是在并发下控制程序的执行逻辑,以此来保证数据按照预期变动。
如果不加锁,并发情况下的可能数据不一致的情况,这是个概率问题。
乐观锁CAS

简介

乐观锁很乐观,假设数据一般情况不会造成冲突,属于程序层面的逻辑锁,在数据进行更新时,才进行锁的检测。是通过添加一个版本号的方式实现的,每当数据这一行所在的数据发生变化,则对应的版本号+1,更新数据时,将版本号作为查询条件。
至于是否要加事务,看写操作单条数据还是写操作多条数据。
注意:网上很多解决方案用时间戳来做version字段,我持反对意见,并发可能是一瞬间的事,不到一秒就有好多请求,用时间戳粒度太大,用随机字符串都比用这个强。
用法
  1. #示例
  2. update test set score = score + 1 where id = 1
  3. #优化为,这种简单,但是会有ABA的问题:
  4. select score as old_score from test where id  = 1;
  5. update test set score = score + 1 where id = 1 and score = old_score;
  6. #或者添加一个version字段,这种不存在ABA的问题
  7. select version from test where id  = 1;
  8. update test set score = score + 1 where id = 1 and version = version;
复制代码
适用场景


  • 读多写少:由于并发写操作较少,乐观锁的修改数据受影响行数为0概率也较低。
  • 允许一定量的重试或不需要重试的场景:这个要根据业务,否则来回重试会降低性能。
优点

实现简单:乐观锁在代码上就可以实现,不需要额外对数据库额外操作。
无死锁风险:悲观锁有死锁风险,乐观锁没有。
无需重试情况下,性能较高:乐观锁机制在并发访问情况下,不需要像悲观锁那样阻塞其他事务,提供了更高的并发性能,前提当前业务需求能容忍写操作失败的情况。
缺点

并发冲突:多加了一个where条件,只能保证数据最终不会出错,不能保证每条写操作的SQL都执行成功(也就是受影响行数>0)。
不提供强一致性:强一致性要求数据的状态在任何时刻都保持一致,悲观锁是到写操作那一步才去验证,期间只是做了个where条件的过滤。
ABA问题:一个字段的值在请求X中查询出来是A,后续代码实现乐观锁,因为并发量大,同时过来一个Y请求,将A值改成了B,因为一些业务原因又改成了A,整个过程虽然不影响请求X的结果,且能正常执行,但是联合其它数据,这个情况是否符合业务场景,不好说,所以最好的解决方案,就是专门做一个version字段,且不会与之前的version重复,即可,把这个version字段作为where条件,而不是存A或者B字段的所在字段作为where条件。
悲观锁

简介

悲观锁比较悲观,假设数据一定会造成冲突,属于MySQL层面的锁。通过加锁阻塞其他事务,悲观锁可以保证在任何时刻,只有一个事务能够修改或访问共享资源,从而实现了强一致性。这意味着在悲观锁机制下,每个事务的读写操作都是有序、线性的。
需要事务的参与。
用法

在事务中的查询语句添加for update即可。
  1. 如果此时执行了三行内容没有commit,再次执行update test set score = score + 1 where id = 1;则处于阻塞状态,需要等commit之后,才能执行。
  2. start transaction;
  3. select * from test where id = 1 for update;
  4. update test set score = score + 1 where id = 1;
  5. commit;
复制代码
适用场景

写多写操作的前提,是保证数据不出错,悲观锁的机制很符合。
优点

强一致性:基于事务又加锁,一致性可以保证。
实现简单:在事务中for update即可,开发者不需要在这上面关注太多。
缺点

死锁风险:悲观锁在使用不当的情况下可能导致死锁。如果多个事务持有锁并相互等待对方释放锁的情况发生,就可能发生死锁。
性能较低:悲观锁通常需要在整个事务过程中锁定资源,这可能导致其他事务阻塞。
模拟实现

前置准备
  1. #创建一个非常简单的表,并插入一条数据
  2. CREATE TABLE `test` (
  3.   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  4.   `score` int(11) NOT NULL,
  5.   PRIMARY KEY (`id`)
  6. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  7. INSERT INTO `test` (`id`, `score`) VALUES (1, 0);
复制代码
需求模拟

查询test表id为1的数据,检测到score值为0,则自增,否则终止。
不加锁实现

为了提升性能,使用了原生PDO操作MySQL去实现。
  1. //连接数据库
  2. $pdo = new \PDO("mysql:host=127.0.0.1;port=3306;dbname=temp;", 'root', 'root');
  3. $pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
  4. $pdo->query('set names utf8mb4');
  5. //查询
  6. $query = $pdo->query('select score from test');
  7. $query->setFetchMode(\PDO::FETCH_ASSOC);
  8. $res = $query->fetchALL();
  9. if($res[0]['score'] == 0) {
  10.     $res = $pdo->exec('update test set score = score + 1 where id = 1');
  11.     var_dump($res);
  12. }
复制代码
并发模拟

用ab压测,发现效果不明显,可能是ab工具不够力或者电脑线程数量太少导致。
这里用的是ApiPost的压测工具。500个并发去多次压测一轮,发现score值是3,证明确实因为并发造成了与预期结果不一致的情况。
乐观锁解决方案(忽略ABA问题)
  1. #将sql改为如下所示,实测多次,score最大值是1
  2. #注意这种行为,只能保证score的值最大是1,无法保证执行这个SQL的时候,受影响行数>0
  3. update test set score = score + 1 where id = 1 and score = 0
复制代码
悲观锁解决方案
  1. $pdo = new \PDO("mysql:host=127.0.0.1;port=3306;dbname=temp;", 'root', 'root');
  2. $pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
  3. $pdo->query('set names utf8mb4');
  4. $redis = new Redis;
  5. $redis->connect('127.0.0.1', 6379);
  6. try {
  7.     $pdo->beginTransaction();
  8.     $stmt = $pdo->prepare("select * from test where id = 1 for update");
  9.     $stmt->execute();
  10.     $res = $stmt->fetch(PDO::FETCH_ASSOC);
  11.     if($res['score'] == 0) {
  12.         $stmt = $pdo->prepare("UPDATE test SET score = (score + 1) where id  = 1");
  13.         $stmt->execute();
  14.         $pdo->commit();
  15.         $redis->incr('commit');
  16.     } else {
  17.         $redis->incr('rollback');
  18.         $pdo->rollBack();
  19.     }
  20. } catch (PDOException $e) {
  21.     $pdo->rollBack();
  22. }
  23. // 关闭数据库连接
  24. $pdo = null;
复制代码
500个并发压测一轮,查看redis数据,commit数量为1,其余499全部都是rollback,这么多的回滚不代表大错特错(演示效果),而是因为第一个事务执行成功后,再执行其它事务,正因为一个一个排队,就不会出现同时读取多个score值为0的情况了。

来源:https://www.cnblogs.com/phpphp/p/17983522
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具