Redis实现秒杀系统详解

为什么要使用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个)
      在这里插入图片描述
      在这里插入图片描述
      在看一下控制台
      在这里插入图片描述

    通过结果,我们发现了一个很严重的问题

    商品超卖了

    为什么会超卖呢?

    因为面对高并发我们没有加锁

版本二(乐观锁,解决超卖问题)

由于普通的悲观锁效率太低,而秒杀是极短时间内完成的,所以我们不考虑使用

  • 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
      
  • 此时我们发现库存正常,全部卖出(不多也不少)
    在这里插入图片描述
  • 并且是按先来后到的顺序
    在这里插入图片描述

以上仅仅是一个简单的秒杀系统,对于复杂的秒杀系统要考虑的东西还很多。如:使用反向代理实现负载均衡、数据库读写分离、分库分表…

淘宝为什么能抗住双 11 ?看完这篇文章你就明白了!

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值