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"));