Redis 计数器实现并发场景下的优惠券领取功能

计数器为 Redis 应用场景之一,通过计数器我们可以实现实际业务中的很多需求,例如:PV/UV、接口并发限制、抽奖、优惠券领取等,本篇主要介绍计数器在并发场景下的优惠券领取功能实现。

业务背景

业务需求方做优惠券发放活动,共优惠券 10 张,参与用户 100 人,先到先得,此处只是做一个例子介绍,假设每次并发 20 用户同时访问,如何保证不超领取呢?

相关命令介绍

  • exists:判断指定 key 是否存在
  • setnx:设置值,若该值存在不做任何处理
  • incr:计数

流程图

实践 | Redis 计数器实现并发场景下的优惠券领取功能 - 图1

编码工作

每发送一次领取请求,采用 incr 命令进行自增,由于 Redis 单线程的原因,可以保证原子性,不会出现超领。

luck.js

  1. // Redis链接建立
  2. const Redis = require('ioredis');
  3. const redis = new Redis(6379, '127.0.0.1');
  4. // 将日志写入指定文件
  5. const fs = require('fs');
  6. const { Console } = require('console');
  7. const output = fs.createWriteStream('./stdout.log');
  8. const errorOutput = fs.createWriteStream('./stderr.log');
  9. const logger = new Console(output, errorOutput);
  10. async function luck() {
  11. const count = 10;
  12. const key = 'counter:luck';
  13. const keyExists = await redis.exists(key);
  14. if (!keyExists) { // 如果 key 不存在初始化设置
  15. await redis.setnx(key, 0);
  16. }
  17. const result = await redis.incr(key);
  18. if (result > count) { // 优惠券领取超限
  19. logger.error('luck failure', result);
  20. return;
  21. }
  22. logger.info('luck success', result);
  23. }
  24. module.exports = luck;

起一个简单的 HTTP 服务,浏览器执行 http://127.0.0.1:3000/luck 接口,实现优惠券领取

app.js

  1. const http = require('http');
  2. const luck = require('./luck');
  3. http.createServer((req, res) => {
  4. if (req.url === '/luck') {
  5. luck();
  6. res.end('ok');
  7. }
  8. }).listen(3000);

压力测试

这里采用 ab 进行并发压测,-c 指每次并发数,-n 指总的请求数

  1. $ ab -c 20 -n 100 http://127.0.0.1:3000/luck
  2. This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
  3. Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
  4. Licensed to The Apache Software Foundation, http://www.apache.org/
  5. Benchmarking 127.0.0.1 (be patient).....done
  6. Server Software:
  7. Server Hostname: 127.0.0.1
  8. Server Port: 3000
  9. Document Path: /luck
  10. Document Length: 2 bytes
  11. Concurrency Level: 20
  12. Time taken for tests: 0.073 seconds
  13. Complete requests: 100
  14. Failed requests: 0
  15. Total transferred: 7700 bytes
  16. HTML transferred: 200 bytes
  17. Requests per second: 1361.54 [#/sec] (mean)
  18. Time per request: 14.689 [ms] (mean)
  19. Time per request: 0.734 [ms] (mean, across all concurrent requests)
  20. Transfer rate: 102.38 [Kbytes/sec] received
  21. Connection Times (ms)
  22. min mean[+/-sd] median max
  23. Connect: 0 2 2.1 2 10
  24. Processing: 1 11 5.3 10 22
  25. Waiting: 1 11 5.2 10 20
  26. Total: 4 13 5.4 14 22
  27. Percentage of the requests served within a certain time (ms)
  28. 50% 14
  29. 66% 17
  30. 75% 18
  31. 80% 18
  32. 90% 20
  33. 95% 21
  34. 98% 22
  35. 99% 22
  36. 100% 22 (longest request)

查看领取成功日志 cat stdout.log 是我们预先设置的 10 个名额,如下所示:

stdout.log

  1. luck success 1
  2. luck success 2
  3. luck success 3
  4. luck success 4
  5. luck success 5
  6. luck success 6
  7. luck success 7
  8. luck success 8
  9. luck success 9
  10. luck success 10

领取失败 cat stderr.log 日志查看

stderr.log

  1. luck failure 11
  2. luck failure 12
  3. luck failure 13
  4. luck failure 14
  5. luck failure 15
  6. ...

示例源码