分布式锁主要是为了解决分布式集群多线程对同一资源的抢占问题,举一个常见的例子。
有一批macbook要秒杀抢购,只有100台,预计会有10W人进行抢购,如何确保我们的库存更新是正常的呢?
假设A用户购买了一台电脑,我们可以采用以下几种方式加锁:
1、采用乐观锁:
start transaction;
select num from product where product_id = 1001; //假设查询出num=99
update product set num = num - 1 where product_id = 1001 where num = 99;
commit;
注:事务可加可不加。弊端:
1、DB压力较大,并发较高的场景下,单主也就几千的连接数,根本扛不住
2、会做很多无效的sql执行,比如多个线程同时命中到了num=99,但是update的时候只会有一条更新成功,其余的影响行数均为0。
2、采用悲观锁
start transaction;
select num from product where product_id = 1001 for update; //假设查询出的num=99
update product set num = num - 1 where product_id = 1001;
commit; 弊端:
1、DB压力过大,并发问题同样无法解决,而且 for update 锁会导致其余的 select 也无法正常使用。
3、setNx实现分布式锁
主要说下基于 Redis 的 setNx 实现的锁
setNx:
当key存在时,什么也不做,并return 0;
当key不存在时,设置对应的值,并return 1;基于该特点,我们可以通过 setNx 解决抢购问题,依次为先到达的用户加锁,后续用户进来发现还未释放锁,可进行轮训检测或处理一些其他的业务逻辑。
如果 setNx 之后 redis 服务挂了,锁一直无法释放,岂不是成了死锁,那么死锁如何避免?
一般就是针对本次 set 设置一个过期时间,但是同样存在原子性问题。redis 目前的 set 命令已经解决了这个问题,可以支持nx作为参数传递:
PHP的使用示例:$redis->set('key', 'value', ['nx', 'ex'=>10]);弊端:
1、如果我们的程序处理超时了,导致 A 的请求还未处理完,锁已过期。此时 B 请求进入,加锁。然后 A 请求处理完毕释放锁,可能会把 B 的锁给释放掉。可以通过 key 附带上当前用户的信息,确保谁创建的谁来删除。
4、redlock实现分布式锁
主要还是依赖 redis 的 set,只是在 set 的时候使用 setNx、expire 的方式。
redlock 主要解决的是多台redis的场景下, 节点完全独立,且无需进行数据同步的时候。
假设有5个redis节点,相互无主从、无集群关系。客户端用相同的key和具有唯一性的value在5个节点上请求锁,请求锁的超时时间小于锁的自动释放时间。当在 (N/2)+1 个节点(超过半数)请求到了锁,即认为真正拿到了锁,如果没有拿到锁,则把部分已锁的 redis 释放。
5、List的队列实现
用户购买即往redis的队列里增加用户,比如库存只有100,List的长度达到100之后,后续请求直接返回已抢完即可,简单示例如下:
1、用户购买入队列处理
$key = "product_id_1001"; //定义key
$num = 100; //定义需要抢购的数量
if($redis->lLen(key) >= 100){
return false;
}
$redis->rPush($key, $uid); //记录下本次购买成功的uid2、异步脚本一直循环消费生成订单数据
$key = "product_id_1001"; //定义key
$num = 100; //定义需要抢购的数量
$count = 0; //定义已经生成的交易数
while($count < $num){
if($redis->lPop){
//处理购买逻辑,insert等操作
$count ++;
}
}注意:
1、需要考虑同一个用户点击了两次提交,是否可以允许重复提交?是否需要加操作锁、鉴权幂等校验。
2、加入100个商品,抢了90个之后过了很久都没有人继续抢,消费脚本一直在执行是否合理。
3、脚本在消费过程中挂掉了怎么处理。
ps:关于 redis 的使用推荐两个网站