秒杀随笔
2021-03-10 本文已影响0人
_不想翻身的咸鱼
方法:
- mysql悲观锁
- mysql乐观锁
- PHP+redis分布式锁
- PHP+redis乐观锁(redis watch)
mysql悲观锁
悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其他事务。以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库提供的锁机制才能真正保持数据的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据。)
准备数据
DROP TABLE IF EXISTS goods;
CREAT TABLE IF NOT EXISTS goods(
id INTEGET NOT NULL,
money INTEGET,
version INTEGET,
primary key (id)
)ENGINE = INNODB;
insert into goods value(1,0,1)
select * from goods;
set autocommit=0;
#开两个客户端:
session1:
select * from goods where id =1 for update;
之后在session2:
select * from goods where id =1 for update;
这种情况,session2会进入等待
mysql乐观锁
乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时,才会对数据的冲突与否进行检测。如果没有冲突那就ok;如果出现冲突了,则返回错误信息并让用户决定如何去做。
乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。
-- goods_number表示库存
update items set goods_number=goods_number-1,version=version+1 where id = 100 and version=#{version};
<?php
$version = select version from goods;
#省略业务逻辑
update goods set money = 1, version=version+1 where version={$version};
#上面这个只是用来表达意思的代码,并不是实际代码
#成功的那个,mysql会返回更新成功,php就返回前端,恭喜你秒杀成功
#失败的那个,mysql会返回更新失败,php就返回前端,不好意思,秒杀失败
总结:
乐观锁不锁数据,而是通过版本号控制,会有不同结果返回给php,把决策权交给后端.
对比:乐观锁不需要锁数据,性能高于悲观锁
PHP+redis分布式锁
- 分布式锁本质是占一个坑,当别的进程也要来占坑时发现已经被占,就会放弃或者稍后重试
- 占坑一般使用 setnx(set if not exists)指令,只允许一个客户端占坑
- 先来先占,用完了在调用del指令释放坑
- 但是这样有一个问题,如果逻辑执行到中间出现异常,可能导致del指令没有被调用,这样就会陷入死锁,锁永远无法释放
- 为了解决死锁问题,我们拿到锁时可以加上一个expire过期时间,这样即使出现异常,当到达过期时间也会自动释放锁
- 这样又有一个问题,setnx和expire是两条指令而不是原子指令,如果两条指令之间进程挂掉依然会出现死锁
- 为了治理上面乱象,在redis 2.8中加入了set指令的扩展参数,使setnx和expire指令可以一起执行
相当于是php线程锁,100000个抢购请求并发过来,有100000个线程,但同一时刻只会有一个线程在执行业务代码,其他线程都在死循环当中等待。
redis 分布式锁与原理
EXISTS job #job 不存在
SETNX job "programmer" #job设置成功
SETNX job "code-farmer" #尝试覆盖job失效
get job #查出programmer,没有被覆盖
可见,SETNX和SET是有区别的,SETNX只能用1次,set是可以无数次的。redis分布式锁就是利用了这个机制。
分布式锁实例代码:
$expire = 10; //有效期10秒
$key = 'lock';
$value = time() + $expire; //锁的值 = Unix时间戳 +锁的有效期
$status = true;
while($status){
$lock = $redis->setnx($key, $value);
if(empty($lock)){
$value = $redis->get($key);
#如果当前时间大于设置的有效期,意味着过期,所以删除key
if( time() > $value){
$redis->del($key);
}
}else{
$status = false;
//下面是具体的业务流程....
}
}
也可以封装成方法:
class RedisMutexLock
{
/**
* 缓存 Redis 连接。
*
* @return void
*/
public static function getRedis()
{
// 这行代码请根据自己项目替换为自己的获取 Redis 连接。
return YCache::getRedisClient();
}
/**
* 获得锁,如果锁被占用,阻塞,直到获得锁或者超时。
* -- 1、如果 $timeout 参数为 0,则立即返回锁。
* -- 2、建议 timeout 设置为 0,避免 redis 因为阻塞导致性能下降。请根据实际需求进行设置。
*
* @param string $key 缓存KEY。
* @param int $timeout 取锁超时时间。单位(秒)。等于0,如果当前锁被占用,则立即返回失败。如果大于0,则反复尝试获取锁直到达到该超时时间。
* @param int $lockSecond 锁定时间。单位(秒)。
* @param int $sleep 取锁间隔时间。单位(微秒)。当锁为占用状态时。每隔多久尝试去取锁。默认 0.1 秒一次取锁。
* @return bool 成功:true、失败:false
*/
public static function lock($key, $timeout = 0, $lockSecond = 20, $sleep = 100000)
{
if (strlen($key) === 0) {
// 请更换为自己项目抛异常的方法。
YCore::exception(500, '缓存KEY没有设置');
}
if (!is_int($timeout) || $timeout < 0) {
YCore::exception(500, "timeout 参数设置有误");
}
$start = self::getMicroTime();
$redis = self::getRedis();
do {
// [1] 锁的 KEY 不存在时设置其值并把过期时间设置为指定的时间。锁的值并不重要。重要的是利用 Redis 的特性。
$acquired = $redis->set("Lock:{$key}", 1, ['NX', 'EX' => $lockSecond]);
if ($acquired) {
break;
}
if ($timeout === 0) {
break;
}
usleep($sleep);
} while ((self::getMicroTime()) < ($start + ($timeout * 1000000)));
return $acquired ? true : false;
}
/**
* 释放锁
*
* @param mixed $key 被加锁的KEY。
* @return void
*/
public static function release($key)
{
if (strlen($key) === 0) {
// 请更换为自己项目抛异常的方法。
YCore::exception(500, '缓存KEY没有设置');
}
$redis = self::getRedis();
$redis->del("Lock:{$key}");
}
/**
* 获取当前微秒。
*
* @return bigint
*/
protected static function getMicroTime()
{
return bcmul(microtime(true), 1000000);
}
}
PHP+redis乐观锁
原理
当用户购买时,通过 WATCH 监听用户库存,如果库存在watch监听后发生改变,就会捕获异常而放弃对库存减一操作
如果库存没有监听到变化并且数量大于1,则库存数量减一,并执行任务
**弊端 **
Redis 在尝试完成一个事务的时候,可能会因为事务的失败而重复尝试重新执行
保证商品的库存量正确是一件很重要的事情,但是单纯的使用 WATCH 这样的机制对服务器压力过大
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6397);
//sales商品的销量(也就是卖了多少件)
$redis->watch('sales')
$reids->get('sales');
//秒杀的库存
$number = 100;
if($sales >= $number){
exit('秒杀结束');
}
//开启事务
$redis->multi();
$redis->incr('sales'); //将key中存储的数字值增1,如果key不存在,那么key的值会先被初始化为0,然后再执行incr操作.
$res = $redis->exec();//成功1,失败0
if($res){
//秒杀成功
$sql = "update goods set store=store-1 where id =1";
if($sql){
echo '秒杀成功';
}
}else{
exit('抢购失败')
}