Redis 秒杀案例
实现ab工具模拟并发超卖和超时问题解决
配置JedisPool连接池来解决超时问题利用乐观锁淘汰用户,解决超卖问题 库存遗留问题解决
什么是Lua脚本Lua脚本在redis中的优势编写Lua脚本
实现写一个简单的springboot + thymeleaf页面示例
Title
iPhone 13 Pro !!! 1元秒杀
controller
@PostMapping("/doseckill")
@ResponseBody
public String doseckill(String prodid) throws IOException {
String userid = new Random().nextInt(50000) + "";
boolean isSuccess = SecKill_redis.doSecKill(userid, prodid);
return JSON.toJSONString(isSuccess);
}
秒杀过程
// 秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException{
//1.uid和prodid非空判断
if (uid == null || prodid == null){
return false;
}
//2.连接redis
Jedis jedis = new Jedis("192.168.0.2",6379);
jedis.auth("password");
//3.拼接Key
//3.1 库存key
String kcKey = "sk:"+prodid+":qt";
//3.2 用户key
String userKey = "sk:"+prodid+":user";
//4. 获取库存,如果库存为null,秒杀还没又开始
String kc = jedis.get(kcKey);
if (kc == null){
System.out.println("秒杀还没有开始请等待");
jedis.close();
return false;
}
//5. 判断用户是否重复秒杀操作
if(jedis.sismember(userKey,uid)//命令判断成员元素是否是集合的成员
){
System.out.println("已经成功秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if (Integer.parseInt(kc) < 1){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
//7 秒杀过程
//7.1 库存-1
jedis.decr(kcKey);
//7.2 把秒杀成功的用户添加到清单里面
jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
}
redis 中添加库存
set sk:0101:qt 10
点击秒杀
查看控制台输出情况
查看redis,可以看到库存已清空,并且用户id添加到秒杀成功的集合中
ab工具模拟并发为了模拟并发的效果,我们使用工具ab模拟测测试
centos7 安装
yum install httpd-tools
ab模拟提交post请求
在linux中创建postfile文件
prodid=0101&
在postfile所在的目录执行命令,1000个请求100个并发
ab -n 1000 -c 100 -p /home/xm/postfile -T application/x-www-form-urlencoded http://192.168.2.2:8080/doseckill
查看控制台和redis中的数据,发现问题
还出现了连接超时的问题
超卖和超时问题解决 配置JedisPool连接池来解决超时问题编写工具类
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
public static JedisPool getJedisPoolInstance(){
if (null == jedisPool){
synchronized (JedisPoolUtil.class){
if (null == jedisPool){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig,"192.168.2.2",6379,60000,"password");
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis){
if (null != jedis){
jedisPool.close();
}
}
}
修改代码,doSecKill方法中通过连接池获取Jedis对象
// 秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException{
//1.uid和prodid非空判断
if (uid == null || prodid == null){
return false;
}
//2.通过连接池得到jedis对象
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();
利用乐观锁淘汰用户,解决超卖问题
// 秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException{
//1.uid和prodid非空判断
if (uid == null || prodid == null){
return false;
}
//2.通过连接池得到jedis对象
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();
//3.拼接Key
//3.1 库存key
String kcKey = "sk:"+prodid+":qt";
//3.2 用户key
String userKey = "sk:"+prodid+":user";
//监视库存
jedis.watch(kcKey);
//4. 获取库存,如果库存为null,秒杀还没又开始
String kc = jedis.get(kcKey);
if (kc == null){
System.out.println("秒杀还没有开始请等待");
jedis.close();
return false;
}
//5. 判断用户是否重复秒杀操作
if(jedis.sismember(userKey,uid)//命令判断成员元素是否是集合的成员
){
System.out.println("已经成功秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if (Integer.parseInt(kc) < 1){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
//7 秒杀过程
// 使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List
重新测试,观察控制台输出(太长就不截图了),和redis key的值
库存遗留问题解决在测试中增加库存量
2000个请求300个并发
ab -n 2000 -c 300 -p /home/xm/postfile -T application/x-www-form-urlencoded http://192.168.2.2:8080/doseckill
我们发现库存并没有清零
这是乐观锁造成的库存遗留问题,部分请求并没能成功执行秒杀,因为事务执行时,重新检测库存数量,发现和最初watch检测的库存数量不一致(乐观锁版本号的机制)
为了解决这个问题,我们使用Lua脚本解决这个问题
什么是Lua脚本Lua是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k ,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
https://www.w3cschool.cn/lual
public class SecKill_redisByscript {
static String secKillscript =
"local userid=KEYS[1];rn" +
"local prodid=KEYS[2];rn" +
"local qtkey='sk:'..prodid..":qt";rn" +
"local usersKey='sk:'..prodid..":user";rn" +
"local userExists=redis.call("sismember",usersKey,userid);rn" +
"if tonumber(userExists)==1 then rn" +
" return 2;rn" +
"endrn" +
"local num= redis.call("get" ,qtkey);rn" +
"if tonumber(num)<=0 then rn" +
" return 0;rn" +
"else rn" +
" redis.call("decr",qtkey);rn" +
" redis.call("sadd",usersKey,userid);rn" +
"endrn" +
"return 1" ;
public static boolean doSkillByscript(String userid,String prodid){
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();
String sha1 = jedis.scriptLoad(secKillscript);
Object result = jedis.evalsha(sha1, 2, userid, prodid);
String reString = String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
}
参考:
尚硅谷-Redis 6 入门到精通 超详细 教程



