本文来源于公众号:勾勾的Java宇宙,莫得推广,全是干货!
原文链接:mp.weixin.qq.com/s/aNaiuJNrH…
作者:李国
说说秒杀
秒杀,是对正常业务流程的考验。因为它会产生突发流量,平常一天的请求可能要集中在几秒内完成。比如,京东的某些抢购,可能库存就几百个,但是瞬时进入的流量可能是几十万上百万。
但如果参与秒杀的用户需要等待很长时间,那用户体验就非常差了。你可以想象一下拥堵的高速公路收费站,估计就能理解秒杀者的心情了。
与此同时,被秒杀的资源会成为热点,发生并发争抢。比如 12306 的抢票,如果单纯使用数据库来接受这些请求,就会产生严重的锁冲突,这也是秒杀业务难的地方。
此时,秒杀前端需求与数据库之间的速度是严重不匹配的,而且秒杀的资源是热点资源。这种场景下,采用缓存就非常合适,可以来看看 Redis 是如何助力秒杀的。
Redis 助力秒杀
处理秒杀业务有三个绝招:
-
选择速度最快的内存作为数据写入;
-
使用异步处理代替同步请求;
-
使用分布式横向扩展。
一个秒杀系统是非常复杂的,一般来说,秒杀可以分为一下三个阶段:
-
准备阶段,会提前载入一些必需的数据到缓存中,并提前预热业务数据,用户会不断刷新页面,来查看秒杀是否开始;
-
抢购阶段/秒杀阶段,会产生瞬时的高并发流量,对资源进行集中操作;
-
结束清算,主要完成数据的一致性,处理一些异常情况和回仓操作。
设计 Hash
在最重要的秒杀阶段,我们可以设计一个Hash数据结构,来支持库存的扣减。
seckill:goods:${goodsId}{
total: 100,
start: 0,
alloc:0
}
在这个 Hash 数据结构中,有三个重要部分。
-
total是一个静态值,表示要秒杀商品的数量,在秒杀开始前,会将这个数值载入到缓存中。 -
start是一个布尔值,秒杀开始前的值为0,通过后台或者定时,将这个值改为1,则表示秒杀开始。 -
此时,
alloc将会记录已经被秒杀的商品数量,直到它的值达到total的上限。
static final String goodsId = "seckill:goods:%s";
String getKey(String id) {
return String.format(goodsId, id);
}
public void prepare(String id, int total) {
String key = getKey(id);
Map<String, Integer> goods = new HashMap<>();
goods.put("total", total);
goods.put("start", 0);
goods.put("alloc", 0);
redisTemplate.opsForHash().putAll(key, goods);
}
Lua 脚本解决同步问题
秒杀的时候,首先需要判断库存,才能够对库存进行锁定。这两步动作并不是原子的,在分布式环境下,多台机器同时对 Redis 进行操作,就会发生同步问题。
为了解决同步问题——
- 一种方式就是使用 Lua 脚本,把这些操作封装起来,这样就能保证原子性;
- 另一种方式就是使用分布式锁,篇幅关系暂且不谈。
下面是一个调试好的 Lua 脚本,可以看到一些关键的比较动作和HINCRBY命令,能够成为一个原子操作。
local falseRet = "0"
local n = tonumber(ARGV[1])
local key = KEYS[1]
local goodsInfo = redis.call("HMGET",key,"total","alloc")
local total = tonumber(goodsInfo[1])
local alloc = tonumber(goodsInfo[2])
if not total then
return falseRet
end
if total >= alloc + n then
local ret = redis.call("HINCRBY",key,"alloc",n)
return tostring(ret)
end
return falseRet
对应的秒杀代码如下,由于我们使用的是 String 的序列化方式,所以会把库存的扣减数量先转化为字符串,然后再调用 Lua 脚本。
public int secKill(String id, int number) {
String key = getKey(id);
Object alloc = redisTemplate.execute(script, Arrays.asList(key), String.valueOf(number));
return Integer.valueOf(alloc.toString());
}
执行仓库里的testSeckill方法。启动 1000 个线程对 100 个资源进行模拟秒杀,可以看到生成了 100 条记录,同时其他的线程返回的是0,表示没有秒杀到。
欢迎大佬们关注公众号 勾勾的Java宇宙,拒绝水文,收获干货!