DevilKing's blog

冷灯看剑,剑上几分功名?炉香无需计苍生,纵一穿烟逝,万丈云埋,孤阳还照古陵

0%

秒杀架构浅析

原文链接

其实抛开秒杀这个场景来说正常的一个下单流程可以简单分为以下几步:

  • 校验库存
  • 扣库存
  • 创建订单
  • 支付

超卖现象

采用乐观锁的实现方式

1
2
3
4
5
6
7
8
9
<update id="updateByOptimistic" parameterType="com.crossoverJie.seconds.kill.pojo.Stock">
update stock
<set>
sale = sale + 1,
version = version + 1,
</set>
WHERE id = #{id,jdbcType=INTEGER}
AND version = #{version,jdbcType=INTEGER}
</update>
1
2
3
4
5
6
7
8
CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

version作为关键字来使用?

分布式限流的方式

1
2
3
4
5
6
7
//限流
boolean limit = redisLimit.limit();
if (!limit){
res.setCode(StatusEnum.REQUEST_LIMIT.getCode());
res.setMessage(StatusEnum.REQUEST_LIMIT.getMessage());
return res ;
}
  • 每次请求时将当前时间(精确到秒)作为 Key 写入到 Redis 中,超时时间设置为 2 秒,Redis 将该 Key 的值进行自增。
  • 当达到阈值时返回错误。
  • 写入 Redis 的操作用 Lua 脚本来完成,利用 Redis 的单线程机制可以保证每个 Redis 请求的原子性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--lua 下标从 1 开始
-- 限流 key
local key = KEYS[1]
-- 限流大小
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
local curentLimit = tonumber(redis.call('get', key) or "0")
if curentLimit + 1 > limit then
-- 达到限流大小 返回
return 0;
else
-- 没有达到阈值 value + 1
redis.call("INCRBY", key, 1)
redis.call("EXPIRE", key, 2)
return curentLimit + 1
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean limit() {
String key = String.valueOf(System.currentTimeMillis() / 1000);
Object result = null;
if (jedis instanceof Jedis) {
result = ((Jedis) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));
} else if (jedis instanceof JedisCluster) {
result = ((JedisCluster) this.jedis).eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(limit)));
} else {
//throw new RuntimeException("instance is error") ;
return false;
}
if (FAIL_CODE != (Long) result) {
return true;
} else {
return false;
}
}

乐观锁更新+分布式限流+redis缓存

  • 每次查询库存时走 Redis。
  • 扣库存时更新 Redis。
  • 需要提前将库存信息写入 Redis(手动或者程序自动都可以)

更进一步,加上kafka异步队列

因为异步了,所以最终需要采取回调或者是其他提醒的方式提醒用户购买完成。

但好像代码里面没有采用回调的方式?

其实经过上面的一顿优化总结起来无非就是以下几点:

  • 尽量将请求拦截在上游。
  • 还可以根据 UID 进行限流。
  • 最大程度的减少请求落到 DB。
  • 多利用缓存。
  • 同步操作异步化。
  • fail fast,尽早失败,保护应用