Redis分布式锁

来源:互联网 发布:sql row_number() over 编辑:程序博客网 时间:2024/06/05 15:15

分布式锁

应用场景

在电商系统中,为了推荐自己的品牌和吸引用户量,那么会推出一个产品,这个产品只能被一个用户购买,如果一个用户正在购买时,其他用户点击购买的时候,则告知该用户,商品已经售出。

场景分析

假设购买流程为:1.用户看到商品,2.用户点击购买,3.用户使用支付宝或微信支付,4.用户购买成功

目前有2个用户:A和B

场景并发现象:

用户A:1.用户看到商品,2.用户点击购买

用户B:1.用户看到商品,2.用户点击购买

此时用户A和用户B都进入到了第三步:3.用户使用支付宝或微信支付

那可能就会出现并发问题,两个用户都同时购买了1个商品。

解决方案

根据以往经验,可知,我们如上流程可以通过锁来进行控制,传统的数据库锁的方式在此不进行介绍了,来看一下redis是如何实现的。

使用redis,我们可以解决两个用户同时操作一个数据流程的时候:

1. 当用户要购买此商品的时候,我们可以创建一个key,流程完成后,并改变此商品的出售状态(此商品已卖完),删除key。

2. 其他用户在此想购买此商品的时候,去redis中读取这个key,如果key存在,那么就证明此商品正在被一个用户购买(这个用户正在购买的流程中),告知该用户,此商品已卖完

这是我提出来的一个大概流程,但仔细思考起来,里边还有很多小细节,那我们来慢慢分析。

版本1,支付未完成退出

用户A:1.用户看到商品 -> 2.用户点击购买 -> set buying A (设置一个key的名称是buying,value是A) -> 3.用户使用支付宝或微信支付 -> 4.用户购买成功 -> 更改商品出售状态 -> delete buying (删除redis的key)

用户B:1.用户看到商品 -> 2.用户点击购买 -> get buying -> echo “此商品已卖完”

上边流程如果看懂了后,我们思考一个问题,当用户A操作到3.用户使用支付宝或微信支付时,突然用户由于一些突发情况,然后页面退出了,那么由于流程没有完成,所以这个redis中的key(buying)将一直存在,也就是说商品没有卖出,但是其他人再也不能买了。

版本2,设置过期时间

由于要解决以上问题,我们需要把这样的一个购买时间设置一个有效期,比如说如果用户在30分钟内都未购买成功,那么不管是什么原因,我都应该让这个redis中的key失效,让用户此次购买的行为失效。

也就是说,我们应该把buying设置一个有效期

用户A:1.用户看到商品 -> 2.用户点击购买 -> set buying A (设置一个key的名称是buying,value是A) -> set expire 30-> 3.用户使用支付宝或微信支付 -> 4.用户购买成功 -> 更改商品出售状态 -> delete buying (删除redis的key)

用户B:1.用户看到商品 -> 2.用户点击购买 -> get buying -> echo “此商品已卖完”

这个版本2中的用户A设置了一个key后,即使中途真的发生什么意外导致退出后,依然在指定时间后,其他用户可以继续购买。

 

setnx a 1

如果不存在就set并返回1,存在就返回0

设置过期时间

redis> SET cache_page "www.google.com"

OK

redis> EXPIRE cache_page 30  # 设置过期时间为 30 秒

(integer) 1

redis> TTL cache_page    # 查看剩余生存时间

(integer) 23

redis> EXPIRE cache_page 30000   # 更新过期时间

(integer) 1

redis> TTL cache_page

(integer) 29996

 

问题?网络延迟同时获取了buying key

那是否已经觉得这个方案很完美了呢?其实不然,我们把焦点放到set buying A(在redis设置key)的这点。

用户是这样操作的:

用户A:1.用户看到商品 -> 2.用户点击购买 -> get buying

用户B:1.用户看到商品 -> 2.用户点击购买 -> get buying

此时由于用户A和用户B同时点击购买(2.用户点击购买) 然后去读取buying,可能因为网络延迟问题,导致了同时都读取了buying,但是两个人读取buying这个key是不存在的,会产生后边的流程:

用户A:1.用户看到商品 -> 2.用户点击购买 -> get buying -> set buying A

用户B:1.用户看到商品 -> 2.用户点击购买 -> get buying -> set buying B

不管用户A还是用户B最终设置了buying这个redis的key值,依然产生了并发问题,结果肯定不是我们想要的。

版本3,使用setnx解决redis并发问题

我们知道redis本身是指令级别的,不管是使用JRdis还是spring提供的redisTemplate,我们都无法改变这个行为,那如果解决版本2中出现的问题呢?

redis中有一个指令是:setnx

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".

返回值是这样规定的:

Integer reply, specifically: 1 if the key was set 0 if the key was not set

@see:http://redis.io/commands/setnx

这个命令可以当我们设置一个key的时候,判断这个key是否已经有值,如果没有值,则设置这样的key-value,并返回1,如果有值则不设置,直接返回0。

有了这个指令就很好的解决了版本2中的问题

用户A:1.用户看到商品 -> 2.用户点击购买 -> setnx buying A (设置一个key的名称是buying,value是A) -> set expire 30-> 3.用户使用支付宝或微信支付 -> 4.用户购买成功 -> 更改商品出售状态 -> delete buying (删除redis的key)

用户B:1.用户看到商品 -> 2.用户点击购买 -> get buying -> echo “此商品已卖完”

版本4,redis事务解决过期时间无法设置

以上的版本3已经可以帮我们完成的差不多了。、

但是还有一点就是如果当用户A在redis中设置值的时候,突然掉线了,设置失效指定没有发送,那么其他用户依然再也不能买这个商品了。

解决方案是使用redis事务将setnx buying A和set expire 30 放到一起

reids事务

redis的事务是一组命令的集合。事务同命令一样都是redis的最小执行单元,一个事务中的命令要么执行要么都不执行。

首先需要multi命令来开始事务,用exec命令来执行事务。

127.0.0.1:6379> multi

OK

127.0.0.1:6379> SET cache_page "www.google.com"

QUEUED

127.0.0.1:6379>

127.0.0.1:6379> EXPIRE cache_page 30

QUEUED

127.0.0.1:6379> exec

1) OK

 

秒杀防超卖

应用场景

有一个秒杀商品,秒杀库存为10,在12:00的时候有1w用户进行点击购买,如何防止超卖

场景分析

例如有一秒杀产品A,1w用户在同一秒钟进行抢购,如果每一个用户在秒杀时,都去读取秒杀库存数,判断未超买后,再去增加占用库存数,那么这样的并发问题是显而易见的。

当然数据操作还有一个就是使用表的行锁进行控制,但是笔记认为这个从用户体验来讲,对于这个场景来说,是不可取的。

由于redis的读写高性能的特点,我们将使用redis去处理这样的秒杀场景。

解决方案

假设产品A,秒杀库存10。

1. 当创建秒杀商品A的时候,创建redis对象:set pA 10

2. 1w用户进行秒杀时,调用redis: INCRBY pA -1 (每次步长都为-1,然后返回扣减之后的库存量) 和 在redis中添加购买人与购买时间: hset pA userid 1478584205(hset 用户 秒杀时间)

3. 如果get pA < 0了,那么就不让用户购买了,echo “秒杀已卖完”

4. 当用户付款后,扣减数据库中秒杀库存数,删除redis hdel pA userid

5. 每一段时间都跑一下job去查询一下对于秒杀商品pA下单还没有付款的人,超过一段时间如果不付款则认为用户不想秒杀,则归还redis库存:INCRBY pA 1

以上步骤中,第5步,可以根据自己的业务而定。

 

 

原创粉丝点击