面筋:4——MySQL中的悲观锁和乐观锁

2018-02-05 10:33:42来源:oschina作者:杨春炼人点击

分享

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


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


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框架为例),具体代码为:


public function test() {
$id = 1;// 用户id
$score = 10; // 用户的积分

$Model = M('user1', 'test_');
$user_info = $Model->where(array('id'=>$id))->find();

$data = array('score'=>array("exp", "score+{$score}"));

$res = $Model->where(array('id'=>$id))->save($data);
echo 'result:'.$res.'--sql:'.$Model->getLastSql();
}

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


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


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


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


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=MyISAMDEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
用户表(test_user1)的初始数据为:
id username score version
1 user01 0 0

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


public function test() {
$id = 1;// 用户id
$score = 10; // 用户的积分
$Model = M('user1', 'test_');
$user_info = $Model->where(array('id'=>$id))->find();
$version = $user_info['version']?:0; // 版本号
// 版本号加1
$data = array('score'=>array("exp", "score+{$score}"), 'version'=>array("exp", "version+1"));

// 乐观锁的核心思想:更新数据时,加入版本号version条件
$res = $Model->where(array('id'=>$id, 'version'=>$version))->save($data);
echo 'result:'.$res.'--sql:'.$Model->getLastSql();
}

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

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台