首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >超卖和分布式锁解决方案

超卖和分布式锁解决方案

原创
作者头像
Bess Croft
修改2021-07-23 18:08:40
1.3K0
修改2021-07-23 18:08:40
举报
文章被收录于专栏:贝丝的专栏贝丝的专栏

超卖和分布式锁解决方案

背景

要说现在在高并发场景中,哪个概念最火,那当属“秒杀”了。那么秒杀也是有自己的一些特点的:

  • 大量用户同一时间访问,造成瞬时访问量激增。
  • 数据库的并发读写激增,导致负载非常高。
  • 请求数远大于库存数,只有部分人才能秒杀成功。

当然,这篇文章不具体讲秒杀系统的设计了,主要讲一讲在秒杀系统中的一环——Redis分布式锁。虽然商家都希望自己的东西卖的越多越好,但是大多数场景下,秒杀的库存并不是特别多,这时候我们就得避免“超卖”问题的发生了。

下单的流程

正常下单的流程

当用户的下单请求到达服务端时,为了防止恶意下单,首先系统中肯定要做风控的。如果说有大量的请求过来,可能需要接口限流。然后创建订单、创建成功后扣除库存。这种方案,算是最常见的解决方案了。而且也能够保证订单不会超卖,因为创建订单之后就减库存,已经封装成了一个原子操作。

但是这样也有很明显的缺点:并发高了,操作数据库的次数会增加,对数据库的压力不用想都知道很高。

预扣库存

为了避免数据库负载增加太多,我们就可以从减少操作数据库 IO 入手。比如说扣库存这一操作,我们就没必要直接去数据库了,对吗?我们可以把库存缓存到 redis 进行预扣库存。然后通过消息队列来异步生成订单,这样用户等待的速度就会快很多,同时也给数据库减压了。

订单的生成是异步的,咱们一般都会放到 MQ、kafka 这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。而且在电商平台买,订单都会有个超时时间的,时间到了未支付,会自动退单。

单机下扣库存的处理

上面我们说到了,下单的流程中,是需要保证扣库存和创建订单的原子性的,那么在单机的情况下,就需要用事务来进行处理了。

分布式锁

秒杀的场景,往往伴随着高并发,但是单机的承载能力并不算很好,而且要考虑到服务的可用性,所以我们可能要上集群。在分布式场景中,我们为了实现不同客户端的线程对代码和资源的同步访问,保证在多线程下处理共享数据的安全性,就需要用到分布式锁技术。本篇文章的主角就来了——Redis分布式锁。

记得刚学到Java的锁概念的时候,一个通俗易懂的例子就是:一个进程进入了叫redis的厕所,但是发现坑满了(上锁了),然后就只能放弃上厕所(加锁)或者等一下再看看(重试)🤣

Redis 分布式锁的三大核心要素就是:加锁、解锁、锁超时。

首先,redis 单机和多机实现的锁,是不同的。一些人将单机 Redis 排除了分布式锁的范畴,为了避免争议,这里我也不站边了。就我所了解的,有以下一些方案:

SETNX 命令

直接使用 Redis 的 SETNX 命令,该指令只在 key 不存在的情况下,将 key 的值设置为 value,若 key 已经存在,则 SETNX 命令不做任何动作。key 是锁的唯一标识,可以按照业务需要锁定的资源来命名。

这种方式比较简单,但也存在弊端,三大核心要素的锁超时给漏了。一旦业务在释放锁之前,出现了问题,就可能导致锁无法释放,从而导致死锁。你可以理解为上厕所时纸掉坑里了,,,

EXPIRE 命令

为了防止死锁,我们可以在拿到锁之后,用 EXPIRE 对 key 设置一个过期时间。这样就能保证在没有显式释放的情况下,防止长时间被独占,因为时间到了锁会自动释放。

没错,即使实现了三大核心要素,依旧存在着一些问题。很明显的,加锁命令和设置超时时间的命令,是非原子性的。也就是说,如果在执行 SETNX 和 EXPIRE 之间发生异常,仍然可能会导致锁的超时。

使用 SET 指令扩展

为了解决前面出现的原子性问题,我们可以使用 SET 指令的扩展参数来解决。但是同时引来了一个新的问题:锁可能被提前释放了。我举个例子,线程 A 加锁后,设定的超时时间是 5s ,但是处于某些意外,执行时间为 6s。但是这时候锁已经早就自动释放了,同时被线程 B 给抢占了。但是线程 A 依旧释放了锁,也就导致了错误释放了锁

但是也不是无法解决的,我们可以给每个锁设置一个唯一的标记。别忘了 redis 是 key-value 形式的。我们加锁,是加到 key 上面去了,但是我们同样的可以在 value 上面,设置唯一的标识。然后在释放锁之前验证一下,如果当前锁的 value 和我加上去的 value 一样,那我们就释放。

又双叒叕新问题了(逐渐暴躁),判断 value 和删除 key 是两个独立的操作,所以肯定无法保证原子性。

使用 lua 脚本

为了保证判断 value 和删除 key 的原子性,我们就需要用到 lua 脚本进行处理了,lua 脚本可以保证连续多个指令的原子性执行。

1 2 3 4 5

if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

lua 脚本解决了错误释放锁的问题,但是却依旧没解决提前释放锁的问题。

Redlock

Redlock 本质上是使用 Redis 实现分布式锁的规范算法,它有很多实现,我们常见的 Redisson 就是其中一个。但是 Redlock 争议也是有的,贴几个链接大家自己去看吧:

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

http://antirez.com/news/101

https://redis.io/topics/distlock

https://carlosbecker.com/posts/distributed-locks-redis/

主要是关于安全性方面的一些争议,不过对于大多数场景来说,其实是完全够用了。

基于Java 实现的 Redisson

这是一张网图,基本上可以看出工作机制。

Redisson 是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

官方文档地址:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 超卖和分布式锁解决方案
    • 背景
      • 下单的流程
        • 正常下单的流程
        • 预扣库存
        • 单机下扣库存的处理
        • 分布式锁
    相关产品与服务
    云数据库 Redis
    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档