Redis Lua脚本解决秒杀下库存校验问题
场景
基本需求
秒杀活动,到时间点后,用户会对商品进行购买。
涉及问题
- 秒杀场景下,瞬时的并发会比较高
- 商品的数量是有限的,不能超买超卖
- 每个用户最多只能抢购一件商品
综合上面问题,传统关系型数据库不能良好的支持。
解决思路
主要流程
Created with Raphaël 2.2.0 开始 提交秒杀请求 扣减库存 跳转下单 订单成功 结束 增加库存 yes no yes no
这里是解决提交秒杀请求后,如何设计扣减库存的实现。
使用redis lua脚本
为什么使用Redis Lua
- Redis为单线程模型,Lua脚本执行具有原子性
- Redis可以支持高并发访问
Lua脚本设计
-- KEYS [good]
-- ARGV [uid]
-- return -1-库存不足 0-重复购买 1-成功
local good = KEYS[1]
local activity = ARGV[1]
local uid = ARGV[2]
local gooduids = good .. ':' .. activity .. ':uids'
local isin = redis.call('SISMEMBER', gooduids, uid)
if isin > 0 then
return 0
end
local goodstock = good .. ':' .. activity .. ':stock'
local stock = redis.call('GET', goodstock)
if not stock or tonumber(stock) <= 0 then
return -1
end
redis.call('DECR', goodstock)
redis.call('SADD', gooduids, uid)
return 1
说明
使用Redis EVAL命令执行
EVAL script numkeys key [key …] arg [arg …]
如 eval “…” 2 goodid activityid uid
Created with Raphaël 2.2.0 开始 用户是否参加过 返回0 结束 是否还有库存 扣减库存 添加用户信息 返回1 结束 返回-1 结束 yes no yes no
- 从参数中获取good编号,activity编号,uid。
- 使用SISMEMBER命令,判断“good:activity:uids”set中,是否有uid,有表示参加过秒杀,返回0
- 使用GET命令,查询“good:activity:stock”获取库存,判断是否还有库存,库存小于等于0,返回-1
- 使用DECR命令,对“good:activity:stock”减一,扣减库存,使用SADD命令添加“good:activity:uids”用户uid,返回1
java测试代码
使用Jedis作为驱动
mport redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class JedisLua {
static final JedisPool pool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
pool = new JedisPool(config, "127.0.0.1");
Runtime.getRuntime().addShutdownHook(new Thread(()->pool.close()));
}
/** * 重置商品的库存 * @param good * @param activity * @param stock */
public static final void reset(String good, String activity, int stock) {
Jedis jedis = pool.getResource();
try {
jedis.set(good + ":" + activity + ":stock", stock + "");
jedis.del(good + ":" + activity + ":uids");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public static final String RUSH_TO_BUY_LUA =
"local good = KEYS[1]\n" +
"local uid = ARGV[1]\n" +
"local activity = KEYS[2]\n" +
"local gooduids = good .. ':' .. activity .. ':uids'\n" +
"\n" +
"local isin = redis.call('SISMEMBER', gooduids, uid)\n" +
"\n" +
"if isin > 0 then\n" +
" return 0\n" +
"end\n" +
"\n" +
"local goodstock = good .. ':' .. activity .. ':stock'\n" +
"local stock = redis.call('GET', goodstock)\n" +
"\n" +
"if not stock or tonumber(stock) <= 0 then\n" +
" return -1\n" +
"end\n" +
"\n" +
"redis.call('DECR', goodstock)\n" +
"redis.call('SADD', gooduids, uid)\n" +
"return 1";
/** * 加载lua脚本到redis中 * @return */
public static String rushToBuySHA1() {
Jedis jedis = pool.getResource();
try {
return jedis.scriptLoad(RUSH_TO_BUY_LUA);
} finally {
if (jedis != null) {
jedis.close();
}
}
}
/** * * @param good 商品编号 * @param activity 活动编号 * @param uid 用户id * @param scriptsha1 redis lua 脚本sha1值 * @return */
public static int rushToBuy(String good, String activity, String uid, String scriptsha1) {
Jedis jedis = pool.getResource();
try {
if (scriptsha1 != null) {
return ((Long) jedis.evalsha(scriptsha1, Arrays.asList(good, activity), Arrays.asList(uid))).intValue();
} else {
return ((Long) jedis.eval(RUSH_TO_BUY_LUA, Arrays.asList(good, activity), Arrays.asList(uid))).intValue();
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public static void main(String[] args) throws InterruptedException {
final String scriptsha1 = rushToBuySHA1();
final String goodid = "good0";
final String activityid = "active1";
reset(goodid, activityid, 10);
Map<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService service = Executors.newCachedThreadPool();
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 0; i < 50000; i++) {
final String uid = "uid" + i;
tasks.add(() -> {
int r = rushToBuy(goodid, activityid, "" + uid, scriptsha1);
if (r == 1) {
map.put(uid, r);
}
return r;
});
}
service.invokeAll(tasks);
System.out.println(map.size());
System.out.println(map);
service.shutdownNow();
}
}
运行main函数,控制台打印
10
{uid6=1, uid7=1, uid4=1, uid5=1, uid2=1, uid3=1, uid0=1, uid1=1, uid8=1, uid10=1}
商品10的库存,50000个请求并发,最后10个用户获取了库存。