MySQL中的悲观锁和乐观锁

左手的ㄟ右手 2022-06-01 10:52 275阅读 0赞

悲观锁(即悲观并发控制)和乐观锁(即乐观并发控制)是数据库系统中并发控制主要采用的技术手段。针对不同的业务场景,应该选用不同的并发控制方式。

注意: 不要把它们和数据库中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。

1. 悲观锁

悲观锁: 又称悲观并发控制(Pessimistic Concurrency Control),指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务)修改持保守态度(悲观),在整个事务处理过程中,将数据进行锁定,直到事务处理完毕,才释放锁。

悲观锁,悲观地认为会发生并发问题,屏蔽一切可能违反数据一致性的操作。

悲观锁的特点:

  • 需要依靠数据库中的锁机制来实现,即通过常用的select … for update操作来实现悲观锁。
  • 需要开启事务,在事务中实现锁机制。
  • 可以最大程度的保证数据操作的独占性。
  • select for update语句中所有扫描过的行都会被锁上,这一点很容易造成问题。如果用悲观锁请确保用到了索引,而不是全表扫描。
  • 长事务中的锁等待,会导致其他用户长时间无法操作。
  • 主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

2. 乐观锁

乐观锁: 又称乐观并发控制(Optimistic Concurrency Control),乐观地认为不会发生并发问题,只在提交更新操作时检查是否违反数据的一致性。

乐观锁在数据库中的实现完全是逻辑性的,不需要数据库提供特殊的支持。一般的做法是在数据表中增加一个字段(版本号或者时间戳),作为数据的版本标识。读取数据时,将版本号一同读出;之后更新数据时,加入版本号条件,更新成功就将版本号加1。

乐观锁的重点在于,更新数据时,加入版本号匹配条件,将数据的版本与数据表中对应记录的当前版本进行匹配更新,如果数据的版本号等于数据表的当前版本号,则获取锁成功,也就是更新成功;否则,更新失败,需要回滚整个业务操作。

乐观锁是否在事务中其实是无所谓的,它的机制是:

在数据库中,update同一行的情况是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时,以版本号作为条件,再次对比版本号确认与之前获取的相同,并更新版本号,即可确认没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。

乐观锁的特点:

  • 不需要依靠数据库中的锁机制来实现,但需要在表中新增一个版本号,在逻辑上实现。
  • 无论是否开启事务,都可以在逻辑上实现乐观锁。
  • 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能。

示例: 假如,现在有一个用户表(test_user1),包含三个字段:id、username、score。用户每支付成功一次,异步回调时,就将他的积分加10。

异步回调的地址(以TP框架为例),具体代码为:

  1. public function test() {
  2. $id = 1; // 用户id
  3. $score = 10; // 用户的积分
  4. $Model = M('user1', 'test_');
  5. $user_info = $Model->where(array('id'=>$id))->find();
  6. $data = array('score'=>array("exp", "score+{$score}"));
  7. $res = $Model->where(array('id'=>$id))->save($data);
  8. echo 'result:'.$res.'--sql:'.$Model->getLastSql();
  9. }

如果支付成功的异步通知不会出现并发(理想情况下),是没有问题的。

但是,实际生活中,异步通知可能产生并发(比如用户支付成功后,异步通知同时请求了2次),该并发就会产生问题(用户的积分居然加了20!!!)。

解决办法: 用乐观锁防止该并发冲突。

首先,给用户表(test_user1)新增一个字段version。修改后的表结构为:

  1. CREATE TABLE IF NOT EXISTS `test_user1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(8) NOT NULL DEFAULT '' COMMENT '用户名', `score` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '积分', `version` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '版本号', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

用户表(test_user1)的初始数据为:


















id username score version
1 user01 0 0

其次,更改异步回调地址的代码为:

  1. public function test() {
  2. $id = 1; // 用户id
  3. $score = 10; // 用户的积分
  4. $Model = M('user1', 'test_');
  5. $user_info = $Model->where(array('id'=>$id))->find();
  6. $version = $user_info['version']?:0; // 版本号
  7. // 版本号加1
  8. $data = array('score'=>array("exp", "score+{$score}"), 'version'=>array("exp", "version+1"));
  9. // 乐观锁的核心思想:更新数据时,加入版本号version条件
  10. $res = $Model->where(array('id'=>$id, 'version'=>$version))->save($data);
  11. echo 'result:'.$res.'--sql:'.$Model->getLastSql();
  12. }

最后,我们来模拟一下并发的请求。

测试环境:Windows系统服务器(64位架构、双内核)、XAMPP集成包

测试工具:apache自带的ab压力测试工具

测试方法:打开cmd命令行,切换到ab工具的可执行文件的所在目录(D:\xampp\apache\bin)后,执行如下命令:

  1. ab -n200 -c10 http://127.0.0.1/index.php/Demo/test

参数说明:-n 表示请求的总数;-c 表示每次的请求数,即并发数。

注意: 需确保ab工具和项目代码在同一个服务器上,否则,可能无法测试出并发问题。

命令执行完毕后,用户表的数据变为:


















id username score version
1 user01 1560 156

说明: 上面的ab命令,每次执行的结果并不会完全一样。如果把-n和-c的值都改为2,再来多次执行ab命令,会发现有时用户的积分加了10,有时却加了20。原因是:假设从查询版本号到更新数据所需的时间为50ms,如果两个请求是并发(严格意义上的同时请求)的,或者两个请求的间隔时间小于50ms(可以认为是非严格意义上的并发),用户的积分都只会加10;如果两个请求的间隔时间大于50ms,用户的积分就会加20,此时,可认为这两个请求是顺序执行的。对于顺序执行的两个请求,要想使他们只更新数据一次,可用幂等性的思想来解决。比如,用订单表的订单状态为依据,如果订单的状态为已处理,则直接退出程序逻辑。

也就是说,虽然ab工具的参数-c的值指定的是并发的请求数,但可能其中的某些请求并不能达到并发的要求,就会顺序执行。

由于乐观锁与事务无关,因此无论数据表的存储引擎是InnoDB还是MyISAM,都可以使用乐观锁。

发表评论

表情:
评论列表 (有 0 条评论,275人围观)

还没有评论,来说两句吧...

相关阅读

    相关 悲观乐观

    锁( locking ) 业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算 处理中,我们希望针对某个 cut-off 时间点的数据进行处理,而不希

    相关 MySQL悲观乐观

    悲观锁(即悲观并发控制)和乐观锁(即乐观并发控制)是数据库系统中并发控制主要采用的技术手段。针对不同的业务场景,应该选用不同的并发控制方式。 注意: 不要把它们和数据库中提供

    相关 mysql悲观乐观

    1.需求分析     项目上线后,在并发的情况,数据库会插入重复的记录;在这种情况,mysql的悲观锁或乐观锁可以用来实现代码逻辑的并发控制。  2.悲观锁 ①悲

    相关 MySql悲观乐观

    mysql中的锁分为两类:悲观锁和乐观锁 悲观锁:悲观锁的特点是先拿到所在进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所

    相关 悲观乐观

    1、悲观锁:    顾名思义,每次读取数据库的数据时,都假设会被它人修改,因此要加锁将数据锁住,防止被修改。   可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加

    相关 悲观乐观

    这是一篇介绍悲观锁和乐观锁的入门文章。旨在让那些不了解悲观锁和乐观锁的小白们弄清楚什么是悲观锁,什么是乐观锁。不同于其他文章,本文会配上相应的图解让大家更容易理解。通过该文,你