redis防止超售

发布 : 2020-12-25 分类 : 笔记

方式一

redis在应用中越来越广泛, 其中常用的大部分操作都是非原子性的, 例如set/get/hmset…这些会在多个操作时后边的覆写前边的, 例如

1
2
3
4
graph LR
请求A-->数据库C
更多... -->数据库C
请求B-->数据库C

请求前后先后进入, 几乎同时抵达数据库, 当A检查时没有请求, B先后到达,也检查到没有请求, 此时向redis写入当前请求信息, A写入,B也写入了, 且会被后写入的覆盖, 无提示, 此时便达不到限制的目的, 而且如果秒杀场景下请求大的情况下, 一下进入的可能销售远远超过库存的数量.

此时就需要使用redis的原子性操作, 同时只能一个读写, 写入/读取有失败提示.
setnx/getset

  • setnx Key Value 设置一个键值, 如果同时写入只有一个会成功返回 1, 其余失败返回 0, 满足了并发加锁限制,
  • getset Key NewValue 获取并设置一个值, 成功会返回当前设置的值,

如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 del Key 操作会导致把其他进程已获得 的锁释放掉

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
// 函数封装
async function lockRequest(lockKey){
// 加锁, 默认无锁
let result = 0;
do {
// 1. 进入加锁, 如果失败循环判断
result = await resdis.setnx(lockKey, `${new Date().getTime() + 100}`);
// 1. 加锁是否成功
if (!result) {
let time = await redis.get(lockKey);
// 2. 加锁失败, 检测锁值时间是否小于当前时间,
let timeout = time - new Date();
if (timeout <= 0) {
// 3. 如果小于当前时间, 证明有进程超时, 直接绕过,不等待
let time = `${new Date().getTime() + 100}}`;
// 3. 增加新值并返回旧值, 原子操作,
let t = await redis.getset(lockKey, time);
// 3. 如果有值在我们之前操作, 则返回的是前值, 不等于当前值, 继续等待
if (new Date() > t) {
result = 1;
return result;
};
} else {
// 不浪费资源,超时之后再操作, setTimeout实现的会有ms级误差
await sleep(timeout);
}
}
} while(!result);
}

方式二

使用原子增减操作 incrby/decrby , 下单时对数据进行增减, 优势无超长时间等待

如下: 简略代码

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
36
37
38
const http = require('http');
const Redis = require('ioredis');
const redis = new Redis();

http.createServer(async (req, res) => {
let key = 'test:count';
let count = 0;
switch (req.url) {
case '/count':
// 查询redis库存剩余 => 实际查询数据库库存剩余即可
count = await redis.get(key);
console.log(count)
break;
case '/set_count':
// 设置库存数
await redis.set(key, 10);
break;
default: // 抢购逻辑
let stock = await redis.get(key);
// 1. 查询当前库存, 如果有再进行
if (stock <= 0) {
res.statusCode = 500;
break;
}
// 模拟下单数量不同
let num = parseInt(Math.random() * 3 + 1)
// 2. 原子减库存, 如果原子减后小于0, 则返还减量
count = await redis.decrby(key, num);
if (count < 0) {
// 2.1 返还库存
await redis.incrby(key, num);
break;
}
// 3. 可以购买
console.log('库存: %d, 购买: %d', stock, num);
}
res.end();
}).listen(3000)

此案例使用 单机redis / pm2 start app.js -i 4 / ab -c 10 -n 100 多进程进行/多请求模拟操作.

示例如下:

  1. 多进程启动
    1
    $> pm2 start app.js -i 4
  2. ab 工具模拟请求
    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    $> ab -c 10 -n 100 http://127.0.0.1:3000/buy
    This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/

    Benchmarking 127.0.0.1 (be patient).....done

    Server Software:
    Server Hostname: 127.0.0.1
    Server Port: 3000

    Document Path: /buy/
    Document Length: 0 bytes

    Concurrency Level: 10
    Time taken for tests: 0.031 seconds
    Complete requests: 100
    Failed requests: 0
    Non-2xx responses: 94
    Total transferred: 9286 bytes
    HTML transferred: 0 bytes
    Requests per second: 3180.16 [#/sec] (mean)
    Time per request: 3.144 [ms] (mean)
    Time per request: 0.314 [ms] (mean, across all concurrent requests)
    Transfer rate: 288.39 [Kbytes/sec] received

    Connection Times (ms)
    min mean[+/-sd] median max
    Connect: 0 0 0.2 0 1
    Processing: 1 2 1.4 2 8
    Waiting: 1 2 1.2 1 6
    Total: 1 3 1.4 2 8

    Percentage of the requests served within a certain time (ms)
    50% 2
    66% 2
    75% 3
    80% 3
    90% 5
    95% 6
    98% 8
    99% 8
    100% 8 (longest request)

  3. pm2 log app 日志打印
    1
    2
    3
    4
    5
    6
    7
    $> pm2 log app
    ...
    2|app | 库存: 10, 购买: 3
    2|app | 库存: 7, 购买: 3
    0|app | 库存: 7, 购买: 1
    0|app | 库存: 7, 购买: 1
    0|app | 库存: 7, 购买: 2
    通过日志打印可以看出, 库存为7的时候, 有4个请求进入, 并且下单成功, 之后便没有了, 保证了库存安全
  4. 查看剩余库存
    1
    2
    $> curl http://localhost:3000/count
    0
本文作者 : 萧逸雨
原文链接 : http://qiubo.ink/2020/12/25/redis防止超售/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!