您的位置:首页 > 数据库 > Redis

java 用redis如何处理电商平台,秒杀、抢购超卖

2017-04-05 22:34 423 查看
一、刚来公司时间不长,看到公司原来的同事写了这样一段代码,下面贴出来:
1、这是在一个方法调用下面代码的部分:

[java] view
plain copy

if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 标注10:  

                throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量");  

            }  

2、下面是判断超卖的方法:

[java] view
plain copy

/** 根据缓存数据查询是否卖超 */  

    //标注:1;synchronized   

    private synchronized boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate) {  

        boolean flag = false;  

        if (redisUtil.exists(key)) {//标注:2;redisUtil.exists(key)  

            Integer soldCount = (int) redisUtil.get(key);//标注:3;redisUtil.get(key)  

            Integer totalSoldCount = soldCount + buyCount;  

            if (limitCount > (totalSoldCount)) {  

                flag = false;//标注:4;flag = false  

            } else {  

                if (redisUtil.tryLock(key, 80)) {//标注:5;rdisUtil.tryLock(key, 80)  

  

                    redisUtil.remove(key);// 解锁 //标注:6;redisUtil.remove(key)  

  

                    redisUtil.set(key, totalSoldCount);//标注:7;redisUtil.set(key, totalSoldCount)  

  

                    flag = true;  

                } else {  

                    throw new ServiceException("活动太火爆啦,请稍后重试");  

                }  

            }  

        } else {  

            //标注:8;redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()))  

            redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()));  

            flag = false;  

        }  

        return flag;  

    }  


3、上面提到的redisUtil类中的方法,其中redisTemplate为org.springframework.data.redis.core.RedisTemplate;这个不了解的可以去网上找下,spring-data-redis.jar的相关文档,贴出来redisUtil用到的相关方法:

[java] view
plain copy

/** 

     * 判断缓存中是否有对应的value 

     *  

     * @param key 

     * @return 

     */  

    public boolean exists(final String key) {  

        return redisTemplate.hasKey(key);  

    }  

    /** 

     * 将键值对设定一个指定的时间timeout. 

     *  

     * @param key 

     * @param timeout 

     *            键值对缓存的时间,单位是毫秒 

     * @return 设置成功返回true,否则返回false 

     */  

    public boolean tryLock(String key, long timeout) {  

        boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, "");  

        if (isSuccess) {//标注:9;redisTemplate.expire  

  

            redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);  

        }  

        return isSuccess;  

    }  

    /** 

     * 读取缓存 

     *  

     * @param key 

     * @return 

     */  

    public Object get(final String key) {  

        Object result = null;  

        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();  

        result = operations.get(key);  

        return result;  

    }  

    /** 

     * 删除对应的value 

     *  

     * @param key 

     */  

    public void remove(final String key) {  

        if (exists(key)) {  

            redisTemplate.delete(key);  

        }  

    }  

    /** 

     * 写入缓存 

     *  

     * @param key 

     * @param value 

     * @return 

     */  

    public boolean set(final String key, Object value) {  

        return set(key, value, null);  

    }  

    /** 

     *  

     * @Title: set 

     * @Description: 写入缓存带有效期 

     * @param key 

     * @param value 

     * @param expireTime 

     * @return boolean    返回类型 

     * @throws 

     */  

    public boolean set(final String key, Object value, Long expireTime) {  

        boolean result = false;  

        try {  

            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();  

            operations.set(key, value);  

            if (expireTime != null) {  

                redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);  

            }  

            result = true;  

        } catch (Exception e) {  

            e.printStackTrace();  

        }  

        return result;  

    }  

4、上面提到的DateUtil类,我会在下面用文件的形式发出来!

二、现在我们来解读下这段代码,看看作者的意图,以及问题点在什么地方,这样帮助更多的人了解,在电商平台如何处理在抢购、秒杀时出现的超卖的情况处理
1、参数说明,上面checkSoldCountByRedisDate方法,有4个参数分别是:
 key:购买数量的计数,放于Redis缓存中的key;
 limitCount:查找源码发现,原注释为:总限购数量;
 buyCount:为当前一次请求下单要购买的数量;
 endDate:活动结束时间;
2、通过上面的标注,我们来解析原作者的意图:
标注1:想通过synchronized关键字实现同步,看似没问题
标注2:通过redisUtil.exists方法判断key是否存在,看似没什么问题
标注3:redisUtil.get(key)获取购买总数,似乎也没问题
标注4:当用户总购买数量<总限购量返回false,看起来只是一个简单的判断
标注5:想通过redisUtil.tryLock加锁,实现超卖的处理,后面的代码实现计数,好像也没什么问题
标注6:标注5加了锁,那么通过redisUtil.remove解锁,看起来顺理成章
标注7:通过redisUtil.set来记录用户购买量,原作者应该是这个意思了
标注8:如果标注2判断的key不存在,在这里创建一个key,看起来代码好像也是要这么写
标注9:我想原作者是不想出现死锁,用redisTemplate.expire做锁超时的方式来解除死锁,这样是可以的
3、针对上面作者意图的分析,我们来看下,看似没有问题的,是否真的就是没问题!呵呵。。,好贱!
下面看看每个标注,可能会出现的问题:
标注1:synchronized关键字,在分布式高并发的情况下,不能实现同步处理,不信测试下就知道了;
那么就可能会出现 的问题是:
现在同一用户发起请A、B或不同用户发起请求A、B,会同时进入checkSoldCountByRedisDate方法并执行

标注2:当抢购开始时,A、B请求同时率先抢购,进入checkSoldCountByRedisDate方法,
A、B请求被redisUtil.exists方法判断key不存在,
从而执行了标注8的部分,同时去执行一个创建key的动作;
真的是好坑啊!第一个开始抢购都抢不到!

标注3:当请求A、B同时到达时,假设:请求A、B当前购买buyCount参数为40,标注3得到的soldCount=50,limitCount=100,
此时请求A、B得到的totalSoldCount均为90,问题又来了

标注4:limitCount > (totalSoldCount):totalSoldCount=90,limitCount=100,些时flag就等于 false,
返回给标注10的位置抛出异常信息(throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量"););
请求A、B都没抢到商品。什么鬼?总共购买90,总限购量是100,这就抛出异常达到活动限购数,我开始看不懂了

标注5:在这里加锁的时候,如果当执行到标注9:isSuccess=true,客户端中断,不执行标注9以后的代码,
完蛋,死锁出现了!谁都别想抢到

下面我们假设A请求比B请求稍慢一点儿到达时,A、B请求的buyCount参数为40,标注3得到的soldCount=50、limitCount=100去执行的else里面的代码,
也就checkSoldCountByRedisDate方法中的:

[java] view
plain copy

else {  

                if (redisUtil.tryLock(key, 80)) {  

  

                    redisUtil.remove(key);// 解锁  

  

                    redisUtil.set(key, totalSoldCount);  

<span style="margin: 0px; padding:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: