社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

MySQL乐观锁与悲观锁

马哥Linux运维 • 3 月前 • 66 次点击  

说明

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

乐观锁CAS

简介

乐观锁很乐观,假设数据一般情况不会造成冲突,属于程序层面的逻辑锁,在数据进行更新时,才进行锁的检测。是通过添加一个版本号的方式实现的,每当数据这一行所在的数据发生变化,则对应的版本号+1,更新数据时,将版本号作为查询条件。
至于是否要加事务,看写操作单条数据还是写操作多条数据。

注意:网上很多解决方案用时间戳来做version字段,我持反对意见,并发可能是一瞬间的事,不到一秒就有好多请求,用时间戳粒度太大,用随机字符串都比用这个强。

用法

#示例
update test set score = score + 1 where id = 1
#优化为,这种简单,但是会有ABA的问题:
select score as old_score from test where id = 1;
update test set score = score + 1 where id = 1 and score = old_score;
#或者添加一个version字段,这种不存在ABA的问题
select version from test where id = 1;
update test set score = score + 1 where id = 1 and version = version;

适用场景

  1. 读多写少:由于并发写操作较少,乐观锁的修改数据受影响行数为0概率也较低。

  2. 允许一定量的重试或不需要重试的场景:这个要根据业务,否则来回重试会降低性能。

优点

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

缺点

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

悲观锁

简介

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

用法

在事务中的查询语句添加for update即可。

如果此时执行了三行内容没有commit,再次执行update test set score = score + 1 where id = 1;则处于阻塞状态,需要等commit之后,才能执行。
start transaction;
select * from test where id = 1 for update;
update test set score = score + 1 where id = 1;
commit;

适用场景

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

优点

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

缺点

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

模拟实现

前置准备

#创建一个非常简单的表,并插入一条数据
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`score` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `test` (`id`, `score`) VALUES (1, 0);

需求模拟

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

不加锁实现

为了提升性能,使用了原生PDO操作MySQL去实现。

//连接数据库
$pdo = new \PDO("mysql:host=127.0.0.1;port=3306;dbname=temp;", 'root', 'root');
$pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
$pdo->query('set names utf8mb4');

//查询
$query = $pdo->query('select score from test');
$query->setFetchMode(\PDO::FETCH_ASSOC);
$res = $query->fetchALL();


if($res[0]['score'] == 0) {
$res = $pdo->exec('update test set score = score + 1 where id = 1');
var_dump($res);
}

并发模拟

用ab压测,发现效果不明显,可能是ab工具不够力或者电脑线程数量太少导致。
这里用的是ApiPost的压测工具。500个并发去多次压测一轮,发现score值是3,证明确实因为并发造成了与预期结果不一致的情况。

乐观锁解决方案(忽略ABA问题)

#将sql改为如下所示,实测多次,score最大值是1
#注意这种行为,只能保证score的值最大是1,无法保证执行这个SQL的时候,受影响行数>0
update test set score = score + 1 where id = 1 and score = 0

悲观锁解决方案




    
$pdo = new \PDO("mysql:host=127.0.0.1;port=3306;dbname=temp;", 'root', 'root');
$pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION);
$pdo->query('set names utf8mb4');

$redis = new Redis;
$redis->connect('127.0.0.1', 6379);

try {
$pdo->beginTransaction();

$stmt = $pdo->prepare("select * from test where id = 1 for update");
$stmt->execute();
$res = $stmt->fetch(PDO::FETCH_ASSOC);

if($res['score'] == 0) {
$stmt = $pdo->prepare("UPDATE test SET score = (score + 1) where id = 1");
$stmt->execute();
$pdo->commit();
$redis->incr('commit');
} else {
$redis->incr('rollback');
$pdo->rollBack();
}
} catch (PDOException $e) {
$pdo->rollBack();
}

// 关闭数据库连接
$pdo = null;

500个并发压测一轮,查看redis数据,commit数量为1,其余499全部都是rollback,这么多的回滚不代表大错特错(演示效果),而是因为第一个事务执行成功后,再执行其它事务,正因为一个一个排队,就不会出现同时读取多个score值为0的情况了。

链接:https://www.cnblogs.com/phpphp/p/17983522

(版权归原作者所有,侵删)


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/166862
 
66 次点击