分享

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

 平民小白 2017-11-15

一、刚来公司时间不长,看到公司原来的同事写了这样一段代码,下面贴出来:

1、这是在一个方法调用下面代码的部分:

  1. if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 标注10:  
  2.                 throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量");  
  3.             }  
2、下面是判断超卖的方法:

  1. /** 根据缓存数据查询是否卖超 */  
  2.     //标注:1;synchronized   
  3.     private synchronized boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate) {  
  4.         boolean flag = false;  
  5.         if (redisUtil.exists(key)) {//标注:2;redisUtil.exists(key)  
  6.             Integer soldCount = (int) redisUtil.get(key);//标注:3;redisUtil.get(key)  
  7.             Integer totalSoldCount = soldCount + buyCount;  
  8.             if (limitCount > (totalSoldCount)) {  
  9.                 flag = false;//标注:4;flag = false  
  10.             } else {  
  11.                 if (redisUtil.tryLock(key, 80)) {//标注:5;rdisUtil.tryLock(key, 80)  
  12.   
  13.                     redisUtil.remove(key);// 解锁 //标注:6;redisUtil.remove(key)  
  14.   
  15.                     redisUtil.set(key, totalSoldCount);//标注:7;redisUtil.set(key, totalSoldCount)  
  16.   
  17.                     flag = true;  
  18.                 } else {  
  19.                     throw new ServiceException("活动太火爆啦,请稍后重试");  
  20.                 }  
  21.             }  
  22.         } else {  
  23.             //标注:8;redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()))  
  24.             redisUtil.set(key, new String("buyCount"), DateUtil.diffDateTime(endDate, new Date()));  
  25.             flag = false;  
  26.         }  
  27.         return flag;  
  28.     }  



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

  1. /** 
  2.      * 判断缓存中是否有对应的value 
  3.      *  
  4.      * @param key 
  5.      * @return 
  6.      */  
  7.     public boolean exists(final String key) {  
  8.         return redisTemplate.hasKey(key);  
  9.     }  
  10.     /** 
  11.      * 将键值对设定一个指定的时间timeout. 
  12.      *  
  13.      * @param key 
  14.      * @param timeout 
  15.      *            键值对缓存的时间,单位是毫秒 
  16.      * @return 设置成功返回true,否则返回false 
  17.      */  
  18.     public boolean tryLock(String key, long timeout) {  
  19.         boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(key, "");  
  20.         if (isSuccess) {//标注:9;redisTemplate.expire  
  21.   
  22.             redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);  
  23.         }  
  24.         return isSuccess;  
  25.     }  
  26.     /** 
  27.      * 读取缓存 
  28.      *  
  29.      * @param key 
  30.      * @return 
  31.      */  
  32.     public Object get(final String key) {  
  33.         Object result = null;  
  34.         ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();  
  35.         result = operations.get(key);  
  36.         return result;  
  37.     }  
  38.     /** 
  39.      * 删除对应的value 
  40.      *  
  41.      * @param key 
  42.      */  
  43.     public void remove(final String key) {  
  44.         if (exists(key)) {  
  45.             redisTemplate.delete(key);  
  46.         }  
  47.     }  
  48.     /** 
  49.      * 写入缓存 
  50.      *  
  51.      * @param key 
  52.      * @param value 
  53.      * @return 
  54.      */  
  55.     public boolean set(final String key, Object value) {  
  56.         return set(key, value, null);  
  57.     }  
  58.     /** 
  59.      *  
  60.      * @Title: set 
  61.      * @Description: 写入缓存带有效期 
  62.      * @param key 
  63.      * @param value 
  64.      * @param expireTime 
  65.      * @return boolean    返回类型 
  66.      * @throws 
  67.      */  
  68.     public boolean set(final String key, Object value, Long expireTime) {  
  69.         boolean result = false;  
  70.         try {  
  71.             ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();  
  72.             operations.set(key, value);  
  73.             if (expireTime != null) {  
  74.                 redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);  
  75.             }  
  76.             result = true;  
  77.         } catch (Exception e) {  
  78.             e.printStackTrace();  
  79.         }  
  80.         return result;  
  81.     }  
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方法中的:

  1. else {  
  2.                 if (redisUtil.tryLock(key, 80)) {  
  3.   
  4.                     redisUtil.remove(key);// 解锁  
  5.   
  6.                     redisUtil.set(key, totalSoldCount);  
  7.   
  8.                     flag = true;  
  9.                 } else {  
  10.                     throw new ServiceException("活动太火爆啦,请稍后重试");  
  11.                 }  
  12.             }  

标注6、7:A请求先到达,假设加锁成功,并成功释放锁,设置的key的值为90后,这里B请求也加锁成功,释放锁成功,设置key的值为90,

那么问题来了:

A、B各买40,原购买数为50,总限量数为100,40+40+50=130,大于最大限量数却成功执行,我了个去,公司怎么向客户交代!


凌晨了,废话不多说了,关键还要看问题怎么处理,直接上代码吧!调用的地方就不看了,其实,代码也没几行,有注释大家一看就明白了:

  1. /** 
  2.      *  
  3.      * 雷------2016年6月17日 
  4.      *  
  5.      * @Title: checkSoldCountByRedisDate 
  6.      * @Description: 抢购的计数处理(用于处理超卖) 
  7.      * @param @param key 购买计数的key 
  8.      * @param @param limitCount 总的限购数量 
  9.      * @param @param buyCount 当前购买数量 
  10.      * @param @param endDate 抢购结束时间 
  11.      * @param @param lock 锁的名称与unDieLock方法的lock相同 
  12.      * @param @param expire 锁占有的时长(毫秒) 
  13.      * @param @return 设定文件 
  14.      * @return boolean 返回类型 
  15.      * @throws 
  16.      */  
  17.     private boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate, String lock, int expire) {  
  18.         boolean check = false;  
  19.         if (this.lock(lock, expire)) {  
  20.             Integer soldCount = (Integer) redisUtil.get(key);  
  21.             Integer totalSoldCount = (soldCount == null ? 0 : soldCount) + buyCount;  
  22.             if (totalSoldCount <= limitCount) {  
  23.                 redisUtil.set(key, totalSoldCount, DateUtil.diffDateTime(endDate, new Date()));  
  24.                 check = true;  
  25.             }  
  26.             redisUtil.remove(lock);  
  27.         } else {  
  28.             if (this.unDieLock(lock)) {  
  29.                 logger.info("解决了出现的死锁");  
  30.             } else {  
  31.                 throw new ServiceException("活动太火爆啦,请稍后重试");  
  32.             }  
  33.         }  
  34.         return check;  
  35.     }  
  36.   
  37.     /** 
  38.      *  
  39.      * 雷------2016年6月17日 
  40.      *  
  41.      * @Title: lock 
  42.      * @Description: 加锁机制 
  43.      * @param @param lock 锁的名称 
  44.      * @param @param expire 锁占有的时长(毫秒) 
  45.      * @param @return 设定文件 
  46.      * @return Boolean 返回类型 
  47.      * @throws 
  48.      */  
  49.     @SuppressWarnings("unchecked")  
  50.     public Boolean lock(final String lock, final int expire) {  
  51.         return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {  
  52.             @Override  
  53.             public Boolean doInRedis(RedisConnection connection) throws DataAccessException {  
  54.                 boolean locked = false;  
  55.                 byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire));  
  56.                 byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);  
  57.                 locked = connection.setNX(lockName, lockValue);  
  58.                 if (locked)  
  59.                     connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));  
  60.                 return locked;  
  61.             }  
  62.         });  
  63.     }  
  64.   
  65.     /** 
  66.      *  
  67.      * 雷------2016年6月17日 
  68.      *  
  69.      * @Title: unDieLock 
  70.      * @Description: 处理发生的死锁 
  71.      * @param @param lock 是锁的名称 
  72.      * @param @return 设定文件 
  73.      * @return Boolean 返回类型 
  74.      * @throws 
  75.      */  
  76.     @SuppressWarnings("unchecked")  
  77.     public Boolean unDieLock(final String lock) {  
  78.         boolean unLock = false;  
  79.         Date lockValue = (Date) redisTemplate.opsForValue().get(lock);  
  80.         if (lockValue != null && lockValue.getTime() <= (new Date().getTime())) {  
  81.             redisTemplate.delete(lock);  
  82.             unLock = true;  
  83.         }  
  84.         return unLock;  
  85.     }  
下面会把上面方法中用到的相关DateUtil类的方法贴出来:
  1. /** 
  2.      * 日期相减(返回秒值) 
  3.      * @param date Date 
  4.      * @param date1 Date 
  5.      * @return int 
  6.      * @author  
  7.      */  
  8.     public static Long diffDateTime(Date date, Date date1) {  
  9.         return (Long) ((getMillis(date) - getMillis(date1))/1000);  
  10.     }  
  11.     public static long getMillis(Date date) {  
  12.         Calendar c = Calendar.getInstance();  
  13.         c.setTime(date);  
  14.         return c.getTimeInMillis();  
  15.     }  
  16.     /** 
  17.      * 获取 指定日期 后 指定毫秒后的 Date 
  18.      *  
  19.      * @param date 
  20.      * @param millSecond 
  21.      * @return 
  22.      */  
  23.     public static Date getDateAddMillSecond(Date date, int millSecond) {  
  24.         Calendar cal = Calendar.getInstance();  
  25.         if (null != date) {// 没有 就取当前时间  
  26.             cal.setTime(date);  
  27.         }  
  28.         cal.add(Calendar.MILLISECOND, millSecond);  
  29.         return cal.getTime();  
  30.     }  

到这里就结束!

新补充:

  1. import java.util.Calendar;  
  2. import java.util.Date;  
  3. import java.util.concurrent.TimeUnit;  
  4.   
  5. import org.slf4j.Logger;  
  6. import org.slf4j.LoggerFactory;  
  7. import org.springframework.beans.factory.annotation.Autowired;  
  8. import org.springframework.dao.DataAccessException;  
  9. import org.springframework.data.redis.connection.RedisConnection;  
  10. import org.springframework.data.redis.core.RedisCallback;  
  11. import org.springframework.data.redis.core.RedisTemplate;  
  12. import org.springframework.data.redis.core.TimeoutUtils;  
  13. import org.springframework.stereotype.Component;  
  14.   
  15. import cn.mindmedia.jeemind.framework.utils.redis.RedisUtils;  
  16. import cn.mindmedia.jeemind.utils.DateUtils;  
  17.   
  18. /** 
  19.  * @ClassName: LockRetry 
  20.  * @Description: 此功能只用于促销组 
  21.  * @author 雷 
  22.  * @date 2017年7月29日 上午11:54:54 
  23.  *  
  24.  */  
  25. @SuppressWarnings("rawtypes")  
  26. @Component("lockRetry")  
  27. public class LockRetry {  
  28.     private Logger logger = LoggerFactory.getLogger(getClass());  
  29.     @Autowired  
  30.     private RedisTemplate redisTemplate;  
  31.   
  32.     /** 
  33.      *  
  34.      * @Title: retry 
  35.      * @Description: 重入锁 
  36.      * @author 雷  
  37.      * @param @param lock 名称 
  38.      * @param @param expire 锁定时长(秒),建议10秒内 
  39.      * @param @param num 取锁重试试数,建议不大于3 
  40.      * @param @param interval 重试时长 
  41.      * @param @param forceLock 强制取锁,不建议; 
  42.      * @param @return 
  43.      * @param @throws Exception    设定文件 
  44.      * @return Boolean    返回类型 
  45.      * @throws 
  46.      */  
  47.     @SuppressWarnings("unchecked")  
  48.     public Boolean retryLock(final String lock, final int expire, final int num, final long interval, final boolean forceLock) throws Exception {  
  49.         Date lockValue = (Date) redisTemplate.opsForValue().get(lock);  
  50.         if (forceLock) {  
  51.             RedisUtils.remove(lock);  
  52.         }  
  53.         if (num <= 0) {  
  54.             if (null != lockValue && lockValue.getTime() >= (new Date().getTime())) {  
  55.                 logger.debug(String.valueOf((lockValue.getTime() - new Date().getTime())));  
  56.                 Thread.sleep(lockValue.getTime() - new Date().getTime());  
  57.                 RedisUtils.remove(lock);  
  58.                 return retryLock(lock, expire, 1, interval, forceLock);  
  59.             }  
  60.             return false;  
  61.         } else {  
  62.             return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {  
  63.                 @Override  
  64.                 public Boolean doInRedis(RedisConnection connection) throws DataAccessException {  
  65.                     boolean locked = false;  
  66.                     byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtils.getDateAdd(null, expire, Calendar.SECOND));  
  67.                     byte[] lockName = redisTemplate.getStringSerializer().serialize(lock);  
  68.                     logger.debug(lockValue.toString());  
  69.                     locked = connection.setNX(lockName, lockValue);  
  70.                     if (locked)  
  71.                         return connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.SECONDS));  
  72.                     else {  
  73.                         try {  
  74.                             Thread.sleep(interval);  
  75.                             return retryLock(lock, expire, num - 1, interval, forceLock);  
  76.                         } catch (Exception e) {  
  77.                             e.printStackTrace();  
  78.                             return locked;  
  79.                         }  
  80.   
  81.                     }  
  82.                 }  
  83.             });  
  84.         }  
  85.   
  86.     }  
  87. }  

  1. /** 
  2.  *  
  3.  * @Title: getDateAddMillSecond 
  4.  * @Description: (TODO)取将来时间 
  5.  * @author 雷  
  6.  * @param @param date 
  7.  * @param @param millSecond 
  8.  * @param @return    设定文件 
  9.  * @return Date    返回类型 
  10.  * @throws 
  11.  */  
  12. public static Date getDateAdd(Date date, int expire, int idate) {  
  13.     Calendar calendar = Calendar.getInstance();  
  14.     if (null != date) {// 默认当前时间  
  15.         calendar.setTime(date);  
  16.     }  
  17.     calendar.add(idate, expire);  
  18.     return calendar.getTime();  
  19. }  

  1. /** 
  2.  * 删除对应的value 
  3.  * @param key 
  4.  */  
  5. public static void remove(final String key) {  
  6.     if (exists(key)) {  
  7.         redisTemplate.delete(key);  
  8.     }  
  9. }  

  1. /** 
  2.  * 判断缓存中是否有对应的value 
  3.  * @param key 
  4.  * @return 
  5.  */  
  6. public static boolean exists(final String key) {  
  7.     return stringRedisTemplate.hasKey(key);  
  8. }  

  1. private static StringRedisTemplate stringRedisTemplate = ((StringRedisTemplate) SpringContextHolder.getBean("stringRedisTemplate"));  




    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多