Redis 秒杀案例

Redis 秒杀案例,第1张

Redis 秒杀案例 Redis 秒杀案例

文章目录

Redis 秒杀案例

实现ab工具模拟并发超卖和超时问题解决

配置JedisPool连接池来解决超时问题利用乐观锁淘汰用户,解决超卖问题 库存遗留问题解决

什么是Lua脚本Lua脚本在redis中的优势编写Lua脚本

实现

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




    
    Title




    iPhone 13 Pro !!! 1元秒杀






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添加到秒杀成功的集合中

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

还出现了连接超时的问题

超卖和超时问题解决 配置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 results = multi.exec();

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

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

库存遗留问题解决

在测试中增加库存量

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

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

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

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

    https://www.w3cschool.cn/lual

Lua脚本在redis中的优势

编写Lua脚本
public class SecKill_redisByscript {

    static String secKillscript =
            "local userid=KEYS[1];rn" +
            "local prodid=KEYS[2];rn" +
            "local qtkey='sk:'..prodid..":qt";rn" +
            "local usersKey='sk:'..prodid..":user";rn" +
            "local userExists=redis.call("sismember",usersKey,userid);rn" +
            "if tonumber(userExists)==1 then rn" +
            "   return 2;rn" +
            "endrn" +
            "local num= redis.call("get" ,qtkey);rn" +
            "if tonumber(num)<=0 then rn" +
            "   return 0;rn" +
            "else rn" +
            "   redis.call("decr",qtkey);rn" +
            "   redis.call("sadd",usersKey,userid);rn" +
            "endrn" +
            "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 入门到精通 超详细 教程

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5721854.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)