首页 > 数据库 >MySQL乐观锁与悲观锁

MySQL乐观锁与悲观锁

时间:2024-01-23 22:00:53浏览次数:43  
标签:pdo update 乐观 score 悲观 MySQL test where id

说明

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

乐观锁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的情况了。

标签:pdo,update,乐观,score,悲观,MySQL,test,where,id
From: https://www.cnblogs.com/phpphp/p/17983522

相关文章

  • MySQL Update语句一个非常经典的“坑”
    起因最近好几次有开发同学在钉钉上问我,比如下图: 问题归纳起来就是:在MySQL里面update一条记录,语法都正确的,但记录并没有被更新…结论小结:在一条UPDATE语句中,如果要更新多个字段,字段间不能使用“AND”,而应该用逗号分隔。现象刚遇到这个问题的时候,我拿到这条语句直接在测试......
  • MySQL Cluster与Replication
    1、综述  MySql架构可以分为两种形式,一个是MySQLCluster,一个是MySQLReplication,Cluster即集群模式,Replication即主从复制(读写分离)模式。  MySQLCluster基于NDB存储引擎,其优点是高可用(即节点出现故障时自动切换到备用节点)和高可伸缩性(可以添加或删除节点以提高系统扩展性......
  • mysqlbinlog~导出sql总结
    mysqlbinlog是MySQL数据库中的一个实用程序,它用于处理二进制日志文件(也称为“binlogs”)。这些文件包含了在MySQL服务器上发生的所有更改和操作的信息。mysqlbinlog工具可以帮助你查看、分析或者应用这些日志。基础介绍下面是一些关于mysqlbinlog的基本介绍:功能查看二进制日志......
  • Java resultset判断mysql表是否存在
    importjava.sql.*;publicclassCheckTableExistence{publicstaticvoidmain(String[]args)throwsSQLException{Stringurl="jdbc:mysql://localhost:3306/mydatabase";//MySQL服务器地址及数据库名称Stringusername="root"......
  • mysql patition by--分区函数
    分区函数patitionbygroupby是分组函数,partitionby是分区函数partitionby并没有groupby的汇总功能。partitionby统计的每一条记录都存在,而groupby将所有的记录汇总成一条记录(类似于distinctEmpDepartment去重)相同点:groupby后的聚合函数,partionby后的orderby......
  • Ubuntu 安装 MySQL
    安装aptinstallmysql-server卸载aptremovemysql-server1、sudoaptpurgemysql-*2、sudorm-rf/etc/mysql//var/lib/mysql3、sudoaptautoremove4、sudoaptautoreclean重要:Mysql5.7.x及以上的版本可能没有初始密码,直接输入:mysql-uroot就直接进入mys......
  • 各版本 MySQL 并行复制的实现及优缺点
    MySQL并行复制已经是老生常谈,笔者从2010年开始就着手处理线上这个问题,刚开始两三年也乐此不疲分享,现在再提这个话题本来是难免“炒冷饭”嫌疑。最近触发再谈这个话题,是因为有些同学觉得“5.7的并行复制终于彻底解决了复制并发性问题”,感觉还是有必要分析一下。大家都说没有银弹,但......
  • MySQL密码过期策略
    如果要设置密码永不过期的全局策略,可以这样:(注意这是默认值,配置文件中可以不声明)[mysqld]default_password_lifetime=0禁用密码过期:ALTERUSER'testuser'@'localhost'PASSWORDEXPIRENEVER;......
  • 一台MySQL服务器启动多个端口
    一台MySQL服务器启动多个端口在测试Mysql多主一从服务器,即一个从服务器多端口同步不同主库。本文记录了开启不同端口的操作。详细步骤:1、首先要先把my.cnf配置文件复制一份,开几个端口要复制几份当然要重新命名.如:cp/etc/my.cnf/etc/my3306.cnfcp/etc/my.cnf/etc/my3307.cn......
  • 使用KeepAlived搭建MySQL高可用环境
     使用KeepAlived搭建MySQL的高可用环境。首先搭建MySQL的主从复制在Master开启binlog,创建复制帐号,然后在Slave输入命令 2016年7月25日 配置安装技巧: 1,使用yuminstall安装keepAlived  2,安装完后,配置/etc/keepalived/keepalived.conf配置文件,一开始配置名字取错了,老有......