Redis第三篇-秒杀案例

Redis第三篇-秒杀案例

  • 案例分析
  • 搭建项目实现秒杀基本功能
  • 秒杀测试
  • 超卖问题分析
  • 利用Redis乐观锁+事务解决超卖问题
  • 库存剩余问题分析
  • Lua脚本解决超卖库存剩余问题
  • jedis连接池的使用

案例分析

秒杀项目用户访问量大
考虑:秒杀商品库存的存储、秒杀成功用户列表的存储(数据可以存在Redis中)
sk:商品id:qt 库存的数量 string[incr decr]
sk:商品id:usr 用户id列表 set

Redis中的数据:一个用户会对应多条数据需要存储,需要保证存储的数据键的唯一性,见名知意。
用户登录成功的信息
用户的会话数据
用户获取的验证码
用户获取验证码的数量

实现:
1、创建项目,准备秒杀的页面和后台处理秒杀的Servlet。
在pom中引入依赖


    <dependency>
      <groupId>javax.servletgroupId>
      <artifactId>jsp-apiartifactId>
      <version>2.0version>
    dependency>
    <dependency>
      <groupId>javax.servletgroupId>
      <artifactId>servlet-apiartifactId>
      <version>2.5version>
    dependency>
    
    <dependency>
      <groupId>redis.clientsgroupId>
      <artifactId>jedisartifactId>
      <version>2.9.0version>
    dependency>
  dependencies>

在webapp下删除自带的index.jsp,自己创建index页面。

<h2>一元秒杀糖果</h2>
    <form action="${pageContext.request.contextPath}/doSeckill" method="post">
        <input type="hidden" name="prodid" value="1001"/>
        <input type="submit" value="点我秒杀"/>
    </form>

修改mavenweb项目的目录:需要有main/java main/resources test/java
在java下创建com.atguigu.sk.DoSeckillServlet处理秒杀的请求:

public class DoSecKillServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //处理秒杀的业务
        System.out.println(req.getParameter("prodid"));
    }
}

在项目的web.xml中注册servlet:

 <servlet>
    <servlet-name>DoSeckillServletservlet-name>
    <servlet-class>com.atguigu.sk.DoSeckillServletservlet-class>
  servlet>
  <servlet-mapping>
    <servlet-name>DoSeckillServletservlet-name>
    <url-pattern>/doSeckillurl-pattern>
  servlet-mapping>

2、修改秒杀的请求为异步方式提交:
拷贝众筹项目的static目录到项目的webapp目录下:jquery
修改inde页面为异步提交

<h2>一元秒杀糖果</h2>
    <form action="${pageContext.request.contextPath}/doSeckill" method="post">
        <input type="hidden" name="prodid" value="1001"/>
        <input type="submit" value="点我秒杀"/>
    </form>
    <script type="text/javascript">
        $("form input:last").click(function () {
           $.ajax({
               "type":"post",
               "url":$("form").prop("action"),
               "data":$("form").serialize(),
               "success":function (result) {
                   if(result=="200"){
                       alert("秒杀成功");
                   }else if(result=="1001"){
                       alert("请勿重复秒杀");
                   }else if(result=="1002"){
                       alert("秒杀还未开始");
                   }else if(result=="1003"){
                       alert("库存不足。。")
                   }else{
                       alert("服务器繁忙,请稍后重试");
                   }
               }
           });
        });
    </script>

3、修改Servlet处理秒杀的业务:

public class DoSeckillServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //处理秒杀业务
        //System.out.println(req.getParameter("prodid"));
        //1、获取请求参数 秒杀id
        String prodid = req.getParameter("prodid");
        //随机生成用户id
        String userid = (int)(10000*Math.random())+"";
        //拼接redis中存储数据的键
        String prodKey = "sk:"+prodid+":qt";
        String usersKey = "sk:"+prodid+":usr";
        //2、秒杀业务
        Jedis jedis = new Jedis("192.168.200.130", 6379);
        // 2.1 判断该用户是否秒杀成功过(在Redis的秒杀用户列表set中判断)
        Boolean sismember = jedis.sismember(usersKey, userid);
        if(sismember){
            //用户已经秒杀过
            System.err.println(userid+"用户重复秒杀。。。");
            resp.getWriter().write("1001");
            jedis.close();
            return;
        }
        //2、2判断库存是否充足
        String qtStr = jedis.get(prodKey);
        if(qtStr==null||qtStr==""){
            //秒杀尚未开始
            System.err.println("秒杀尚未开始");
            resp.getWriter().write("1002");
            jedis.close();
            return;
        }
        int qt=Integer.parseInt(qtStr);
        if(qt<=0){
            //库存不足
            System.err.println("库存不足");
            resp.getWriter().write("1003");
            jedis.close();
            return;
        }
        //库存足够用户可以秒杀
        //减少库存将用户添加到秒杀成功的列表中
        System.out.println("秒杀成功。。。"+qt);
        jedis.decr(prodKey);
        jedis.sadd(usersKey,userid);

        resp.getWriter().write("200");
    }
}

4、测试:

​ 在redis中存入库存: set sk:1001:qt 10

​ 重启项目访问

搭建项目实现秒杀基本功能

秒杀测试

以后java程序员需要使用的性能测试工具:
​ apache ab :linux系统中可以安装使用,用来做高并发测试
​ apache jemeter: java程序,用来做高并发的测试,功能比ab多
​ postman : 测试java程序的接口是否可以访问,参数返回值是否正常,可以做持续的压力测试
ab工具的安装使用
安装:

yum  install -y  httpd-tools

使用ab测试秒杀:
​ 初始化redis中的商品库存:

set sk:1001:qt 100

​ 在linux中使用ab进行测试:

ab -n 2000 -c 200 -p postfile.txt -T application/x-www-form-urlencoded -r http://192.168.1.46:8080/SecondKill/doSeckill

【注意:这里的ip地址不可以写成localhost或者127.0.0.1,因为此时在虚拟机中,虚拟机访问】
​ -n 请求总数量
​ -c 并发提交的请求数量
​ -p post方式提交请求的请求参数
​ postfile.txt内容:

vim  postfile.txt
prodid=1001&

​ -T post方式提交的请求体数据的处理方式
​ -r 出错不会退出
​ 最后是要访问的项目地址[由于秒杀项目在idea主机中运行,ip地址需要些主机的ip]

超卖问题分析

在这里插入图片描述
商品库存负数。
Redis第三篇-秒杀案例_第1张图片

利用Redis乐观锁+事务解决超卖问题

public class DoSeckillServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //处理秒杀业务
        //System.out.println(req.getParameter("prodid"));
        //1、获取请求参数 秒杀id
        String prodid = req.getParameter("prodid");
        //随机生成用户id
        String userid = (int)(10000*Math.random())+"";
        //拼接redis中存储数据的键
        String prodKey = "sk:"+prodid+":qt";
        String usersKey = "sk:"+prodid+":usr";
        //2、秒杀业务
        Jedis jedis = new Jedis("192.168.200.130", 6379);
        // 2.1 判断该用户是否秒杀成功过(在Redis的秒杀用户列表set中判断)
        Boolean sismember = jedis.sismember(usersKey, userid);
        if(sismember){
            //用户已经秒杀过
            System.err.println(userid+"用户重复秒杀。。。");
            resp.getWriter().write("1001");
            jedis.close();
            return;
        }
        //2、2判断库存是否充足
        //给库存添加乐观锁
        jedis.watch(prodKey);
        String qtStr = jedis.get(prodKey);
        if(qtStr==null||qtStr==""){
            //秒杀尚未开始
            System.err.println("秒杀尚未开始");
            resp.getWriter().write("1002");
            jedis.close();
            return;
        }
        int qt=Integer.parseInt(qtStr);
        if(qt<=0){
            //库存不足
            System.err.println("库存不足");
            resp.getWriter().write("1003");
            jedis.close();
            return;
        }
        //库存足够用户可以秒杀
        //减少库存将用户添加到秒杀成功的列表中
        System.out.println("秒杀成功。。。"+qt);
        Transaction multi = jedis.multi();
        multi.decr(prodKey);
        multi.sadd(usersKey,userid);
        //执行队列
        multi.exec();//只要执行队列的watch会自动删除
//        jedis.unwatch();
        jedis.close();

        resp.getWriter().write("200");
    }
}

库存剩余问题分析

Redis第三篇-秒杀案例_第2张图片

乐观锁机制,如果请求不能更新数据则直接执行结束
多个并发的请求如果获取数据时的版本号不一样,最多只有一个请求给更新成功,其他全部失败。

Lua脚本解决超卖库存剩余问题

​ redis从2.6版本后开始支持LUA脚本,内置了LUA的解释器
​ LUA脚本也是一个轻量级的高级语言,一般不会单独编程,一般在其他的语言中当做脚本语言使用。
​ 在java代码中可以将操作redis的逻辑和命令封装为lua脚本,将lua脚本一次性提交给redis,redis可以通过Lua脚本解析 逻辑判断+命令并执行

public class DoSeckillServlet extends HttpServlet {
    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";

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //处理秒杀业务
        //System.out.println(req.getParameter("prodid"));
        //1、获取请求参数 秒杀id
        String prodid = req.getParameter("prodid");
        //随机生成用户id
        String userid = (int) (10000 * Math.random()) + "";
        //拼接redis中存储数据的键
        String prodKey = "sk:" + prodid + ":qt";
        String usersKey = "sk:" + prodid + ":usr";
        //2、秒杀业务
        Jedis jedis = new Jedis("192.168.200.130", 6379);
        //通过lua脚本封装业务和命令一次性交给Redis执行,就可以强行将所有的请求进行排序按照提交顺序执行
        String str = jedis.scriptLoad(secKillScript);
         //jedis执行LUA脚本并传入参数:参数1:lua脚本的加密字符串,参数2:lua脚本参数的数量,参数3.。。:参数列表
        Object evalsha = jedis.evalsha(str, 2, userid, prodid);
        int result =(int)((long)evalsha);
        if(result==1){
            resp.getWriter().write("200");
        }else if(result==2){
            resp.getWriter().write("1001");
        }else if(result==3){
            resp.getWriter().write("1003");
        }
        jedis.close();
    }
}

jedis连接池的使用

之前jedis的连接都是在service方法中直接创建关闭,比较耗时。
1、在项目的pom文件中引入commons-pool2依赖

    
      org.apache.commons
      commons-pool2
      2.7.0
    
  

2、拷贝jedis的连接池工具类

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.1.168", 6379, 60000);
				}
			}
		}
		return jedisPool;
	}
}

3、修改代码中获取jedis连接的方式

 Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();

Redis第三篇-秒杀案例_第3张图片

你可能感兴趣的:(众筹网,redis,java)