Redis 秒杀案例
实现
写一个简单的springboot + thymeleaf页面示例
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<base th:href="@{/}"/>
<body>
<h1>
iPhone 13 Pro !!! 1元秒杀
</h1>
<form id="msfrom">
<input type="hidden" id="prodid" name="prodid" value="0101">
<input type="button" id="miaosha_btn" name="seckill_btn" value="秒杀点我">
</form>
</body>
<script type="text/javascript" src="jquery/jquery-3.5.1.js"></script>
<script type="text/javascript">
$(function () {
$("#miaosha_btn").click(function () {
var prodid = $("#prodid").val();
$.ajax({
url: "http://localhost:8080/doseckill",
type:"post",
data: {
"prodid":prodid
},
dataType: "json",
success:function (data){
if (data === "false"){
alert("抢光了");
$("#miaosha_btn").attr("disabled",true);
}
},
error:function (resp) {
}
})
})
})
</script>
</html>
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{
if (uid == null || prodid == null){
return false;
}
Jedis jedis = new Jedis("192.168.0.2",6379);
jedis.auth("password");
String kcKey = "sk:"+prodid+":qt";
String userKey = "sk:"+prodid+":user";
String kc = jedis.get(kcKey);
if (kc == null){
System.out.println("秒杀还没有开始请等待");
jedis.close();
return false;
}
if(jedis.sismember(userKey,uid)
){
System.out.println("已经成功秒杀");
jedis.close();
return false;
}
if (Integer.parseInt(kc) < 1){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
jedis.decr(kcKey);
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{
if (uid == null || prodid == null){
return false;
}
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();
利用乐观锁淘汰用户,解决超卖问题
public static boolean doSecKill(String uid,String prodid) throws IOException{
if (uid == null || prodid == null){
return false;
}
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();
String kcKey = "sk:"+prodid+":qt";
String userKey = "sk:"+prodid+":user";
jedis.watch(kcKey);
String kc = jedis.get(kcKey);
if (kc == null){
System.out.println("秒杀还没有开始请等待");
jedis.close();
return false;
}
if(jedis.sismember(userKey,uid)
){
System.out.println("已经成功秒杀");
jedis.close();
return false;
}
if (Integer.parseInt(kc) < 1){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
Transaction multi = jedis.multi();
multi.decr(kcKey);
multi.sadd(userKey,uid);
List<Object> results = multi.exec();
if (results == null || results.size()==0){
System.out.println("秒杀失败了");
jedis.close();
}
System.out.println("秒杀成功");
jedis.close();
return true;
}
重新测试,观察控制台输出(太长就不截图了),和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
Lua脚本在redis中的优势
编写Lua脚本
public class SecKill_redisByScript {
static String secKillScript =
"local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":user\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"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 入门到精通 超详细 教程
|