Redis 秒杀案例


Redis 秒杀案例

实现

image-20220204122953018

写一个简单的springboot + thymeleaf页面示例


<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

点击秒杀

image-20220204183356621查看控制台输出情况

image-20220204183726491查看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-20220204213911363image-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();

利用乐观锁淘汰用户,解决超卖问题

image-20220204222739348```java // 秒杀过程 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 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](http://img-md-js.linjsblog.top/img/image-20220204230339593.png)

## 库存遗留问题解决

在测试中增加库存量

![image-20220204230810836](http://img-md-js.linjsblog.top/img/image-20220204230810836.png)

2000个请求300个并发

```shell
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不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

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

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


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

Author: qwq小小舒
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source qwq小小舒 !
  TOC