1、刚来公司时间不长,看到公司原来的同事写了这样一段代码,下面贴出来:java
一、这是在一个方法调用下面代码的部分:redis
if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 标注10: throw new ServiceException("您购买的商品【" + commodityTitle + "】,数量已达到活动限购量"); }
二、下面是判断超卖的方法:spring
/** 根据缓存数据查询是否卖超 */ //标注: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; }
三、上面提到的redisUtil类中的方法,其中redisTemplate为org.springframework.data.redis.core.RedisTemplate;这个不了解的能够去网上找下,spring-data-redis.jar的相关文档,贴出来redisUtil用到的相关方法:缓存
/** * 判断缓存中是否有对应的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; }
四、上面提到的DateUtil类,我会在下面用文件的形式发出来!
并发
2、如今咱们来解读下这段代码,看看做者的意图,以及问题点在什么地方,这样帮助更多的人了解,在电商平台如何处理在抢购、秒杀时出现的超卖的状况处理分布式
一、参数说明,上面checkSoldCountByRedisDate方法,有4个参数分别是:ide
key:购买数量的计数,放于redis缓存中的key;高并发
limitCount:查找源码发现,原注释为:总限购数量;测试
buyCount:为当前一次请求下单要购买的数量;this
endDate:活动结束时间;
二、经过上面的标注,咱们来解析原做者的意图:
标注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作锁超时的方式来解除死锁,这样是能够的
三、针对上面做者意图的分析,咱们来看下,看似没有问题的,是否真的就是没问题!呵呵。。,好贱!
下面看看每一个标注,可能会出现的问题:
标注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方法中的:
else { if (redisUtil.tryLock(key, 80)) { redisUtil.remove(key);// 解锁 redisUtil.set(key, totalSoldCount); flag = true; } else { throw new ServiceException("活动太火爆啦,请稍后重试"); } }
标注六、7:A请求先到达,假设加锁成功,并成功释放锁,设置的key的值为90后,这里B请求也加锁成功,释放锁成功,设置key的值为90,
那么问题来了:
A、B各买40,原购买数为50,总限量数为100,40+40+50=130,大于最大限量数却成功执行,我了个去,公司怎么向客户交代!
凌晨了,废话很少说了,关键还要看问题怎么处理,直接上代码吧!调用的地方就不看了,其实,代码也没几行,有注释你们一看就明白了:
/** * * 雷------2016年6月17日 * * @Title: checkSoldCountByRedisDate * @Description: 抢购的计数处理(用于处理超卖) * @param @param key 购买计数的key * @param @param limitCount 总的限购数量 * @param @param buyCount 当前购买数量 * @param @param endDate 抢购结束时间 * @param @param lock 锁的名称与unDieLock方法的lock相同 * @param @param expire 锁占有的时长(毫秒) * @param @return 设定文件 * @return boolean 返回类型 * @throws */ private boolean checkSoldCountByRedisDate(String key, int limitCount, int buyCount, Date endDate, String lock, int expire) { boolean check = false; if (this.lock(lock, expire)) { Integer soldCount = (Integer) redisUtil.get(key); Integer totalSoldCount = (soldCount == null ? 0 : soldCount) + buyCount; if (totalSoldCount <= limitCount) { redisUtil.set(key, totalSoldCount, DateUtil.diffDateTime(endDate, new Date())); check = true; } redisUtil.remove(lock); } else { if (this.unDieLock(lock)) { logger.info("解决了出现的死锁"); } else { throw new ServiceException("活动太火爆啦,请稍后重试"); } } return check; } /** * * 雷------2016年6月17日 * * @Title: lock * @Description: 加锁机制 * @param @param lock 锁的名称 * @param @param expire 锁占有的时长(毫秒) * @param @return 设定文件 * @return Boolean 返回类型 * @throws */ @SuppressWarnings("unchecked") public Boolean lock(final String lock, final int expire) { return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { boolean locked = false; byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtil.getDateAddMillSecond(null, expire)); byte[] lockName = redisTemplate.getStringSerializer().serialize(lock); locked = connection.setNX(lockName, lockValue); if (locked) connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS)); return locked; } }); } /** * * 雷------2016年6月17日 * * @Title: unDieLock * @Description: 处理发生的死锁 * @param @param lock 是锁的名称 * @param @return 设定文件 * @return Boolean 返回类型 * @throws */ @SuppressWarnings("unchecked") public Boolean unDieLock(final String lock) { boolean unLock = false; Date lockValue = (Date) redisTemplate.opsForValue().get(lock); if (lockValue != null && lockValue.getTime() <= (new Date().getTime())) { redisTemplate.delete(lock); unLock = true; } return unLock; }
下面会把上面方法中用到的相关DateUtil类的方法贴出来:
/** * 日期相减(返回秒值) * @param date Date * @param date1 Date * @return int * @author */ public static Long diffDateTime(Date date, Date date1) { return (Long) ((getMillis(date) - getMillis(date1))/1000); } public static long getMillis(Date date) { Calendar c = Calendar.getInstance(); c.setTime(date); return c.getTimeInMillis(); } /** * 获取 指定日期 后 指定毫秒后的 Date * * @param date * @param millSecond * @return */ public static Date getDateAddMillSecond(Date date, int millSecond) { Calendar cal = Calendar.getInstance(); if (null != date) {// 没有 就取当前时间 cal.setTime(date); } cal.add(Calendar.MILLISECOND, millSecond); return cal.getTime(); }
到这里就结束!
新补充:
import java.util.Calendar; import java.util.Date; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.TimeoutUtils; import org.springframework.stereotype.Component; import cn.mindmedia.jeemind.framework.utils.redis.RedisUtils; import cn.mindmedia.jeemind.utils.DateUtils; /** * @ClassName: LockRetry * @Description: 此功能只用于促销组 * @author 雷 * @date 2017年7月29日 上午11:54:54 * */ @SuppressWarnings("rawtypes") @Component("lockRetry") public class LockRetry { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private RedisTemplate redisTemplate; /** * * @Title: retry * @Description: 重入锁 * @author 雷 * @param @param lock 名称 * @param @param expire 锁定时长(秒),建议10秒内 * @param @param num 取锁重试试数,建议不大于3 * @param @param interval 重试时长 * @param @param forceLock 强制取锁,不建议; * @param @return * @param @throws Exception 设定文件 * @return Boolean 返回类型 * @throws */ @SuppressWarnings("unchecked") public Boolean retryLock(final String lock, final int expire, final int num, final long interval, final boolean forceLock) throws Exception { Date lockValue = (Date) redisTemplate.opsForValue().get(lock); if (forceLock) { RedisUtils.remove(lock); } if (num <= 0) { if (null != lockValue && lockValue.getTime() >= (new Date().getTime())) { logger.debug(String.valueOf((lockValue.getTime() - new Date().getTime()))); Thread.sleep(lockValue.getTime() - new Date().getTime()); RedisUtils.remove(lock); return retryLock(lock, expire, 1, interval, forceLock); } return false; } else { return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { boolean locked = false; byte[] lockValue = redisTemplate.getValueSerializer().serialize(DateUtils.getDateAdd(null, expire, Calendar.SECOND)); byte[] lockName = redisTemplate.getStringSerializer().serialize(lock); logger.debug(lockValue.toString()); locked = connection.setNX(lockName, lockValue); if (locked) return connection.expire(lockName, TimeoutUtils.toSeconds(expire, TimeUnit.SECONDS)); else { try { Thread.sleep(interval); return retryLock(lock, expire, num - 1, interval, forceLock); } catch (Exception e) { e.printStackTrace(); return locked; } } } }); } } }
/** * * @Title: getDateAddMillSecond * @Description: (TODO)取未来时间 * @author 雷 * @param @param date * @param @param millSecond * @param @return 设定文件 * @return Date 返回类型 * @throws */ public static Date getDateAdd(Date date, int expire, int idate) { Calendar calendar = Calendar.getInstance(); if (null != date) {// 默认当前时间 calendar.setTime(date); } calendar.add(idate, expire); return calendar.getTime(); }
/** * 删除对应的value * @param key */ public static void remove(final String key) { if (exists(key)) { redisTemplate.delete(key); } }
/** * * @Title: getDateAddMillSecond * @Description: (TODO)取未来时间 * @author 雷 * @param @param date * @param @param millSecond * @param @return 设定文件 * @return Date 返回类型 * @throws */ public static Date getDateAdd(Date date, int expire, int idate) { Calendar calendar = Calendar.getInstance(); if (null != date) {// 默认当前时间 calendar.setTime(date); } calendar.add(idate, expire); return calendar.getTime(); }
/** * 删除对应的value * @param key */ public static void remove(final String key) { if (exists(key)) { redisTemplate.delete(key); } }
/** * 判断缓存中是否有对应的value * @param key * @return */ public static boolean exists(final String key) { return stringRedisTemplate.hasKey(key); }
private static StringRedisTemplate stringRedisTemplate = ((StringRedisTemplate) SpringContextHolder.getBean("stringRedisTemplate"));