Redis 秒杀案例

Redis 秒杀案例

实现

image-20220204122953018

写一个简单的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{
        //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添加到秒杀成功的集合中

image-20220204183820055

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中的数据,发现问题

在这里插入图片描述

image-20220204214024900

还出现了连接超时的问题

image-20220204214624205

超卖和超时问题解决

配置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<Object> results = multi.exec();

      
        if (results == null || results.size()==0){
            System.out.println("秒杀失败了");
            jedis.close();
        }
        System.out.println("秒杀成功");
        jedis.close();
        return true;
    }

重新测试,观察控制台输出(太长就不截图了),和redis key的值

image-20220204230339593

库存遗留问题解决

在测试中增加库存量

image-20220204230810836

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脚本

在这里插入图片描述

  1. Lua是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k ,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

  2. 很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

  3. 这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。

    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 入门到精通 超详细 教程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值