1. 程式人生 > >java 用redis如何處理電商平臺,秒殺、搶購超賣

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

一、剛來公司時間不長,看到公司原來的同事寫了這樣一段程式碼,下面貼出來:

1、這是在一個方法呼叫下面程式碼的部分:

if (!this.checkSoldCountByRedisDate(key, limitCount, buyCount, endDate)) {// 標註10:
				throw new ServiceException("您購買的商品【" + commodityTitle + "】,數量已達到活動限購量");
			}
2、下面是判斷超賣的方法:
/** 根據快取資料查詢是否賣超 */
	//標註: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用到的相關方法:
/**
	 * 判斷快取中是否有對應的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方法中的:

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

					redisUtil.remove(key);// 解鎖

					redisUtil.set(key, totalSoldCount);

					flag = true;
				} else {
					throw new ServiceException("活動太火爆啦,請稍後重試");
				}
			}

標註6、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);
		}
	}

	/**
	 * 判斷快取中是否有對應的value
	 * @param key
	 * @return
	 */
	public static boolean exists(final String key) {
		return stringRedisTemplate.hasKey(key);
	}

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