Mushroom Notes Mushroom Notes
🍄首页
  • JavaSE

    • 基础篇
    • 数据结构
    • IO流
    • Stream流
    • 函数式接口
    • JUC
    • 反射
    • 网络编程
    • 设计模式
  • JavaEE

    • Servlet
    • JDBC
    • 会话技术
    • 过滤器监听器
    • 三层架构
  • JDK

    • 总览
  • JVM

    • 总览
  • 常用mate
  • CSS
  • JavaScript
  • rds 数据库

    • MySQL
    • MySQL 进阶
    • MySQL 库表规范
  • nosql 数据库

    • Redis
    • Redis 进阶
    • Redis 底层
    • MongoDB
  • Spring生态

    • Spring
    • Spring MVC
    • Spring boot
    • Spring Validation
  • Spring Cloud生态

    • Spring Cloud
    • 服务治理
    • 远程调用
    • 网关路由
    • 服务保护
    • 分布式事务
    • 消息中间件
  • 数据库

    • Mybatis
    • Mybatis Plus
    • Elasticsearch
    • Redisson
  • 通信

    • Netty
📚技术
  • 方案专题
  • 算法专题
  • BUG专题
  • 安装专题
  • 网安专题
  • 面试专题
  • 常用网站
  • 后端常用
  • 前端常用
  • 分类
  • 标签
  • 归档

kinoko

一位兴趣使然的热心码农
🍄首页
  • JavaSE

    • 基础篇
    • 数据结构
    • IO流
    • Stream流
    • 函数式接口
    • JUC
    • 反射
    • 网络编程
    • 设计模式
  • JavaEE

    • Servlet
    • JDBC
    • 会话技术
    • 过滤器监听器
    • 三层架构
  • JDK

    • 总览
  • JVM

    • 总览
  • 常用mate
  • CSS
  • JavaScript
  • rds 数据库

    • MySQL
    • MySQL 进阶
    • MySQL 库表规范
  • nosql 数据库

    • Redis
    • Redis 进阶
    • Redis 底层
    • MongoDB
  • Spring生态

    • Spring
    • Spring MVC
    • Spring boot
    • Spring Validation
  • Spring Cloud生态

    • Spring Cloud
    • 服务治理
    • 远程调用
    • 网关路由
    • 服务保护
    • 分布式事务
    • 消息中间件
  • 数据库

    • Mybatis
    • Mybatis Plus
    • Elasticsearch
    • Redisson
  • 通信

    • Netty
📚技术
  • 方案专题
  • 算法专题
  • BUG专题
  • 安装专题
  • 网安专题
  • 面试专题
  • 常用网站
  • 后端常用
  • 前端常用
  • 分类
  • 标签
  • 归档
  • 方案专题

    • 认证鉴权
    • 接口规范
    • 自动分发架构
    • 高性能计数服务
    • 消息未读数服务
    • 缓存数据库双写一致性问题
    • 优雅的后台操作日志
    • 签到功能
    • 秒杀库存扣减
      • 前言
      • 分布式锁
        • 测试情况
      • Lua脚本
        • 测试情况
      • 关于落库
  • 算法专题

  • BUG专题

  • 安装专题

  • 网安专题

  • 面试专题

  • 专题
  • 方案专题
kinoko
2024-02-16
目录

秒杀库存扣减方案

# 前言

目前市面上秒杀课题常见的解决方案都是资格判断和订单落库解耦的结构,本文讨论主题主要聚焦在用户下单资格判断。

假定需求包含以下部分:

  1. 存库限制
  2. 一人一单

基于这俩点需求我做了一些思考和尝试。

# 分布式锁

首先是分布式锁的方案,这个也蛮常见的,就是通过加锁来进行判断资格和库存扣减,代码示例如下:

@GetMapping("/seckill/{id}")
public Object secKill(@PathVariable Long id) {
    Long userId = getUserId();
    // 加锁
    RLock lock = redisService.getLock("seckill:lock:" + id);
    if (lock.tryLock()) {
        try {
            // 查库存
            Integer stock = redisService.get("seckill:stock:" + id);
            if (stock != null && stock > 0) {
                // 查看当前用户是否下单
                boolean havOrder = redisService.hasSetElement("seckill:order:" + id, userId);
                if (havOrder) {
                    return "不能重复下单";
                }
                // 减库存
                redisService.set("seckill:stock:" + id, --stock);
                // 添加用户下单记录
                redisService.getSet("seckill:order:" + id).add(userId);
                // TODO 加入消息队列异步落库

                return "下单成功";
            } else {
                return "库存不足";
            }
        } catch (Exception e) {
            return "下单异常";
        } finally {
            lock.unlock();
        }
    }
    return "系统繁忙,请稍后重试";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

这里redisService是一个基于redisson二次开发的工具类,锁是分布式锁。

# 测试情况

配置:100库存,1000个线程,携带不同token。

断言:“系统繁忙,请稍后重试”为异常响应。

库存扣减情况
image.png

下单记录
image.png

可以看到1k人同时下单,只有29人下单成功。

jmeter报告
image.png

乍一看好像数据表现不错,吞吐量是很高,但是异常率也很高。可见这个方案存在几个问题:

  1. 接口有效响应率低:有效响应指的是“系统繁忙,请稍后重试”以外的响应,用户下单,每次都是这个提示,看的人想痛骂产品经理,用户体验不太好。
  2. 存在原子性问题:减库存和添加用户下单记录是两次redis会话,spring的事务默认是不支持redis的,所以极端情况下是存在原子性问题的。

# Lua脚本

针对上一个方案存在的问题,lua脚本可以很好的解决。

这里安利一个IDEA的Lua脚本插件叫EmmyLua

语法参考:https://www.runoob.com/lua/lua-tutorial.html (opens new window)
脚本编写

-- 判断是否有库存 get stockKey
local stock = redis.call('get', KEYS[1])
-- 如果为key空
if stock == false then
    return 1
end
-- 如果没库存
if tonumber(stock) <= 0 then
    return 1
end
-- 校验是否已经下单 SISMEMBER orderKey userId
if (redis.call('sismember', KEYS[2], ARGV[1]) == 1) then
    return 2
end
-- 扣减库存 INCRBY stockKey -1
redis.call('incrby', KEYS[1], -1)
-- 增加下单记录 SADD orderKey userId
redis.call('sadd', KEYS[2], ARGV[1])
return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

稍微解释一下:
Redis执行Lua脚本的基本语法是EVAL script numkeys [key [key ...]] [arg [arg ...]]
比如我想执行SET name 张三这条指令,通过脚本执行就是:

EVAL "redis.call('set', KEYS[1], ARGV[1])" 1 name 张三
1
  • KEYS和ARGV是Redis这边规定的特殊占位符
  • script是脚本内容
  • numkeys是KEYS的数量,上面的1就是只有一个参数是Key
  • 后面跟的就是入参了,redis会通过key的数量判断前面多少个是KEYS,后面的就是ARGV了

程序编写

private static String seckillLuaScript;

static {
    // 加载脚本文件
    try {
        InputStream inputStream = new ClassPathResource("lua/seckill.lua").getInputStream();
        seckillLuaScript = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

@GetMapping("/seckill/{id}")
public Object secKill(@PathVariable Long id) {
    Long userId = getUserId();
    // 拼接Lua脚本入参
    String stockKey = "seckill:stock:" + id;
    String orderKey = "seckill:order:" + id;
    // 执行Lua脚本
    RScript luaScript = redisService.getLuaScript();
    Long result = luaScript.eval(RScript.Mode.READ_WRITE,
                                 seckillLuaScript, RScript.ReturnType.VALUE,
                                 List.of(stockKey, orderKey), userId);
    if (eval == 1) {
        return "库存不足";
    }
    if (eval == 2) {
        return "不能重复下单";
    }
    if (eval == 0) {
        // TODO 加入消息队列异步落库
        return "下单成功";
    }
    return "系统繁忙,请稍后重试";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

这里用的是Redisson来执行Lua脚本。

# 测试情况

配置:100库存,1000个线程,携带不同token。

断言:“系统繁忙,请稍后重试”为异常响应。

库存扣减情况
image.png

下单记录
image.png

可以看到1k人同时下单,库存完美清空。

jmeter报告
image.png

在保证吞吐量的情况下还做到了0异常,完美。

# 关于落库

落库的方案也有很多,其实就是MQ的一个选型,然后如何保证消息幂等性和可靠性,可能会存在一些极端情况导致落库失败,或者说是一个实时性的问题。这个结果无非就是库存扣减,用户下单成功但没产生订单。要么是后续与下单情况做一个比对,然后检测自动补偿,要么是等用户打客服反馈然后手动干预补偿,后者可能就有点内啥了哈哈哈,总之详细的一个实现或者思路等我哪天有想法了再补上吧。

#lua#redis#redisson#分布式锁#秒杀
上次更新: 2024/02/16 22:45:44
签到功能
二分查找

← 签到功能 二分查找→

最近更新
01
JVM 底层
09-13
02
JVM 理论
09-13
03
JVM 应用
09-13
更多文章>
Theme by Vdoing | Copyright © 2022-2024 kinoko | MIT License | 粤ICP备2024165634号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式