悲观锁(Pessimistic Lock)
悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
这里需要注意的一点是不同的数据库对select for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,mysql就没有no wait这个选项。另外mysql还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在mysql中用悲观锁务必要确定走了索引,而不是全表扫描。
乐观锁(Optimistic Lock)
乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。
乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,
Demo:
表结构:
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`name` varchar(64) DEFAULT NULL COMMENT '商品名称',
`count` int(11) DEFAULT NULL COMMENT '商品数量',
`price` float DEFAULT NULL COMMENT '商品价格',
`version` int(11) DEFAULT NULL COMMENT '数据版本号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
悲观锁(php 实现):
<?php
date_default_timezone_set('Asia/Shanghai');
$db = new mysqli('127.0.0.1','root','792a1d086a2b0a5d','test',3306);
$bugNum = 3;
$lockSql = "LOCK TABLE `goods` WRITE";
$unLockSql = "UNLOCK TABLES";
$lockRes = $db->query($lockSql);
if (!$lockRes) {
echo "商品表锁定失败\n";
} else {
echo "商品表锁定成功\n";
}
$sql = "select * from goods where id = 1";
$res = $db->query($sql);
while($value = $res->fetch_assoc()) {
$curNum = $value['count'];
if ($curNum < $bugNum) {
echo "商品库存不足\n";
} else {
$curNum = $curNum - $bugNum;
$updateSql = "update `goods` set count = " . $curNum . " where id = 1";
echo $updateSql . "\n";
$updateRes = $db->query($updateSql);
if (!$updateRes) {
echo "商品去库存失败\n";
} else {
echo "商品去库存成功\n";
}
}
}
sleep(20);
$unLockRes = $db->query($unLockSql);
if (!$unLockRes) {
echo "商品表解锁失败\n";
} else {
echo "商品表解锁成功\n";
}
$db->close();
乐观锁(php实现):
<?php
date_default_timezone_set('Asia/Shanghai');
$db = new mysqli('127.0.0.1','root','792a1d086a2b0a5d','test',3306);
$buyNum = 3;
$sql = "select * from goods where id = 1";
$res = $db->query($sql);
while($value = $res->fetch_assoc()) {
$curNum = $value['count'];
$curVersion = $value['version'];
if ($curNum < $buyNum) {
echo "商品库存不足\n";
} else {
$curNum = $curNum - $buyNum;
$nextVersion = $curVersion + 1;
$updateSql = "update `goods` set count = " . $curNum . " and version = " . $nextVersion . " where id = 1 and version = " . $curVersion;
echo $updateSql . "\n";
$updateRes = $db->query($updateSql);
if (!$updateRes) {
echo "数据过期,商品去库存失败\n";
} else {
echo "商品去库存成功\n";
}
}
}
sleep(10);
$db->close();