java 用redis如何處理電商平臺,秒殺、搶購超賣

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"));
相關文章
相關標籤/搜索