为什么要使用Redis
-
1.Redis是基于内存存储的,MySQL是基于磁盘存储的
-
2.Redis存储的是k-v格式的数据。时间复杂度是O(1),常数阶,而MySQL引擎的底层实现是B+Tree,时间复杂度是O(logn),对数阶。Redis会比MySQL快一点点。
-
3.MySQL数据存储是存储在表中,查找数据时要先对表进行全局扫描或者根据索引查找,这涉及到磁盘的查找,磁盘查找如果是按条点查找可能会快点,但是顺序查找就比较慢;而Redis不用这么麻烦,本身就是存储在内存中,会根据数据在内存的位置直接取出。
-
4.Redis是单线程的多路复用IO,单线程避免了线程切换的开销,而多路复用IO避免了IO等待的开销,在多核处理器下提高处理器的使用效率可以对数据进行分区,然后每个处理器处理不同的数据。
版本一(不考虑并发)
-
html
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h1>HUAWEI MATE 30 5G!!! 1元秒杀!!! </h1> <form id="msform" action="${pageContext.request.contextPath}/doseckill" method="post" > <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="${pageContext.request.contextPath}/script/jquery/jquery-3.1.0.js"></script> <script type="text/javascript"> $(function(){ $("#miaosha_btn").click(function(){ var url=$("#msform").attr("action"); $.post(url,$("#msform").serialize(),function(data){ if(data=="false"){ alert("抢光了" ); $("#miaosha_btn").attr("disabled",true); } } ); }) }) </script> </html>
-
java代码
-
public class SecKillServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userid = new Random().nextInt(50000) +"" ; String prodid =request.getParameter("prodid"); boolean if_success=SecKill_redis1.doSecKill(userid,prodid); response.getWriter().print(if_success); } }
-
public class SecKill_redis1 { //ab -n 1000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill public static boolean doSecKill(String uid, String prodid) throws IOException { Jedis jedis = new Jedis("192.168.137.136",6379); String qtKey = "sk:"+prodid+":qt" ; // sk:0101:qt => 10 String usrKey = "sk:"+prodid+":usr"; // sk:0101:usr => [11,22,33,44 ....] set集合 //判断用户如果秒杀成功,不能重复秒杀的。 if(jedis.sismember(usrKey, uid)) { System.err.println("不能重复秒杀"); jedis.close(); return false ; } //判断库存是否已经进行初始化 String qtStr = jedis.get(qtKey); if(qtStr==null || "".equals(qtStr.trim())) { System.err.println("未初始化库存"); jedis.close(); return false; } //判断库存是否大于0 int qt = Integer.parseInt(qtStr); if(qt<=0) { System.err.println("秒光了。。。"); jedis.close(); return false; } //减库存 jedis.decr(qtKey); //sk:0101:qt => 10 //加人 jedis.sadd(usrKey, uid); // sk:0101:usr => [11,22,33,44 ....] set集合 System.out.println("秒杀成功..."); jedis.close(); return true ; } }
-
首先我们在Redis中加入库存10个
-
我们先试一下手动
- 控制台没问题
- Redis中的结果也没问题
- 控制台没问题
-
接下来我们用测试工具试一下
- 因为需要传递参数prodid=0101,所以我们在linux中创建一个postfile文件
- 执行
- -n 发起的总请求数量
- -c 并发请求数量(同一时间发起的请求数量)
- -p 传递的参数
- -T post请求
ab -n 1000 -c 100 -p postfile -T application/x-www-form-urlencoded http://192.168.137.1:8080/seckill/doseckill
- 执行结果
我们发现此时的库存成了-87
看一下秒杀成功的用户(共97个)
在看一下控制台
通过结果,我们发现了一个很严重的问题
商品超卖了
为什么会超卖呢?
因为面对高并发我们没有加锁
- 因为需要传递参数prodid=0101,所以我们在linux中创建一个postfile文件
版本二(乐观锁,解决超卖问题)
由于普通的悲观锁效率太低,而秒杀是极短时间内完成的,所以我们不考虑使用
- java
public class SecKill_redis {
//ab -n 1000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill
public static boolean doSecKill(String uid, String prodid) throws IOException {
Jedis jedis = new Jedis("192.168.137.137",6379);
String qtKey = "sk:"+prodid+":qt" ; // sk:0101:qt => 10
String usrKey = "sk:"+prodid+":usr"; // sk:0101:usr => [11,22,33,44 ....] set集合
//增加乐观锁
jedis.watch(qtKey);
//判断用户如果秒杀成功,不能重复秒杀的。
if(jedis.sismember(usrKey, uid)) {
System.err.println("不能重复秒杀");
jedis.close();
return false ;
}
//判断库存是否已经进行初始化
String qtStr = jedis.get(qtKey);
if(qtStr==null || "".equals(qtStr.trim())) {
System.err.println("未初始化库存");
jedis.close();
return false;
}
//判断库存是否大于0
int qt = Integer.parseInt(qtStr);
if(qt<=0) {
System.err.println("秒光了。。。");
jedis.close();
return false;
}
Transaction multi = jedis.multi();
//减库存
multi.decr(qtKey); //sk:0101:qt => 10
//加人
multi.sadd(usrKey, uid); // sk:0101:usr => [11,22,33,44 ....] set集合
List<Object> result = multi.exec(); //[9,1] [8,1] []
if(result == null || result.size() == 0) {
System.err.println("秒杀失败~~");
jedis.close();
return false;
}
System.out.println("秒杀成功...");
jedis.close();
return true ;
}
}
- 我们加上了乐观锁,再试一次
- 结果貌似很完美,但是看控制台
- 结果: 虽然10件商品被成功的卖出去了,但是我们发现并不是没有遵循先来后到的原则,来的早的没抢到,来的晚的反而抢到了,这是显然不符合规则
为什么会这样呢?
我们通过上面这张图可以发现,乐观锁是通过控制版本号来保证数据的正确性,但这样导致了先来的秒杀失败,后来的秒杀成功,并不是很符合我们的想法。
那还有没有其他问题存在呢?
-
接下来我们增大请求数量与并发量
-
这时发现socket拒绝连接,这是因为Windows有限制,此时加一个参数-r
-
ab -n 5000 -r -c 300 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill
-
-
然后我们发现控制台报错了:连接超时
-
超时好说,我们加个连接池
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(400);//最大连接
poolConfig.setMaxIdle(32);//最大空闲连接
poolConfig.setMaxWaitMillis(100 * 1000);//最长等待时间
poolConfig.setBlockWhenExhausted(true);//如果拿不到连接是否堵塞,是
poolConfig.setTestOnBorrow(true);//测试连接是否正常
jedisPool = new JedisPool(poolConfig, "192.168.137.136", 6379, 60000);
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
public class SecKill_redis {
//ab -n 1000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill
public static boolean doSecKill(String uid, String prodid) throws IOException {
// Jedis jedis = new Jedis("192.168.137.137",6379);
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPool.getResource();
String qtKey = "sk:"+prodid+":qt" ; // sk:0101:qt => 10
String usrKey = "sk:"+prodid+":usr"; // sk:0101:usr => [11,22,33,44 ....] set集合
//增加乐观锁
jedis.watch(qtKey);
//判断用户如果秒杀成功,不能重复秒杀的。
if(jedis.sismember(usrKey, uid)) {
System.err.println("不能重复秒杀");
jedis.close();
return false ;
}
//判断库存是否已经进行初始化
String qtStr = jedis.get(qtKey);
if(qtStr==null || "".equals(qtStr.trim())) {
System.err.println("未初始化库存");
jedis.close();
return false;
}
//判断库存是否大于0
int qt = Integer.parseInt(qtStr);
if(qt<=0) {
System.err.println("秒光了。。。");
jedis.close();
return false;
}
Transaction multi = jedis.multi();
//减库存
multi.decr(qtKey); //sk:0101:qt => 10
//加人
multi.sadd(usrKey, uid); // sk:0101:usr => [11,22,33,44 ....] set集合
List<Object> result = multi.exec(); //[9,1] [8,1] []
if(result == null || result.size() == 0) {
System.err.println("秒杀失败~~");
jedis.close();
return false;
}
System.out.println("秒杀成功...");
jedis.close();
return true ;
}
}
- 再次测试,一切正常
但是真的没有问题了吗?
- 我们改变一下商品数量(100)、请求数量(1000)和并发量(300)
-
ab -n 1000 -r -c 300 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill
-
- 最终我们发现,一共来了1000参与秒杀,我们有100件商品,却只卖出去了9件,库存残留,这显然不符合我们的需求
为什么会这样呢?
问题还在于乐观锁,因为并发量太高,而乐观锁是通过版本号来保证数据的合法性,这就导致了大量用户同时请求,却只有一个用户能成功经过测试,显然乐观锁并不能很好的解决秒杀问题
版本三(LUA脚本)
- 但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
- redis 2.6版本以后,通过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..\":usr\";\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";
static String secKillScript2 = "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n"
+ " return 1";
public static boolean doSecKill(String uid, String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedispool.getResource();
// String sha1= .secKillScript;
String sha1 = jedis.scriptLoad(secKillScript);
Object result = jedis.evalsha(sha1, 2, uid, 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;
}
}
public class SecKillServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String userid = new Random().nextInt(50000) +"" ;
String prodid =request.getParameter("prodid");
// boolean if_success=SecKill_redis.doSecKill(userid,prodid);
boolean if_success=SecKill_redisByScript.doSecKill(userid,prodid);
response.getWriter().print(if_success);
}
}
- 执行刚才的测试
-
ab -n 1000 -r -c 300 -p postfile -T 'application/x-www-form-urlencoded' http://192.168.137.1:8080/seckill/doseckill
-
- 此时我们发现库存正常,全部卖出(不多也不少)
- 并且是按先来后到的顺序