面試經常遇到寫一個搶購實例,或者講講搶購實現想法,而後老是講不明白,由於目前工做沒作搶購這一起。java
可是這個想法今天終於搞明白了,其中也參照了一些大佬的作法。程序員
這篇文章直接使用redis,其中註釋也寫的挺明白的,直接上代碼:面試
junit測試類:redis
Log log = LogFactory.getLog(getClass()); @Autowired private RedisTemplate<String, Object> redisTemplate; @Test public void testSys() throws Exception{ log.info("開始"); MsService service = new MsService(); for (int i = 0; i < 10; i++) { ThreadB threadA = new ThreadB(service, redisTemplate, "MSKEY"); threadA.start(); log.info("*******************結束"); } }
threadB類:算法
private MsService service; private RedisTemplate<String,Object> redisTemplate; private String key; public ThreadB(MsService service,RedisTemplate<String,Object> redisTemplate,String key) { this.service = service; this.redisTemplate=redisTemplate; this.key=key; } @Override public void run() { try { service.seckill(redisTemplate, key); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }
msService類:緩存
Log log = LogFactory.getLog(getClass()); /*** * 搶購代碼 * @param redisTemplate * @param key pronum 首先用客戶端設置數量 * @return * @throws InterruptedException */ public boolean seckill(RedisTemplate<String,Object> redisTemplate, String key) throws Exception { RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000); try { if (lock.lock()) { // 須要加鎖的代碼 String pronum=lock.get("pronum"); //修改庫存 if(Integer.parseInt(pronum)-1>=0) { lock.set("pronum",String.valueOf(Integer.parseInt(pronum)-1)); log.info("庫存數量:"+pronum+" 成功!!!"+Thread.currentThread().getName()); }else { log.info("已經被搶光了,請參與下輪搶購"); } log.info("++++++++++++++++++++++++++++++++++++++參加了搶購"); return true; } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 爲了讓分佈式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖以前應該再檢查一次本身的鎖是否已經超時,再去作DEL操做,由於可能客戶端由於某個耗時的操做而掛起, // 操做完的時候鎖由於超時已經被別人得到,這時就沒必要解鎖了。 ————這裏沒有作 lock.unlock(); } return false; }
redisLock類:app
public class RedisLock { private static Logger logger = LoggerFactory.getLogger(RedisLock.class); private RedisTemplate<String,Object> redisTemplate; private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100; /** * Lock key path. */ private String lockKey; /** * 鎖超時時間,防止線程在入鎖之後,無限的執行等待 */ private int expireMsecs = 60 * 1000; /** * 鎖等待時間,防止線程飢餓 */ private int timeoutMsecs = 10 * 1000; private volatile boolean locked = false; /** * Detailed constructor with default acquire timeout 10000 msecs and lock * expiration of 60000 msecs. * * @param lockKey * lock key (ex. account:1, ...) */ public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey) { this.redisTemplate = redisTemplate; this.lockKey = lockKey + "_lock"; } /** * Detailed constructor with default lock expiration of 60000 msecs. * */ public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs) { this(redisTemplate, lockKey); this.timeoutMsecs = timeoutMsecs; } /** * Detailed constructor. * */ public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) { this(redisTemplate, lockKey, timeoutMsecs); this.expireMsecs = expireMsecs; } /** * @return lock key */ public String getLockKey() { return lockKey; } public String get(final String key) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); byte[] data = connection.get(serializer.serialize(key)); connection.close(); if (data == null) { return null; } return serializer.deserialize(data); } }); } catch (Exception e) { logger.error("get redis error, key : {}", key); } return obj != null ? obj.toString() : null; } public String set(final String key,final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); connection.set(serializer.serialize(key), serializer.serialize(value)); return serializer; } }); } catch (Exception e) { logger.error("get redis error, key : {}", key); } return obj != null ? obj.toString() : null; } public boolean setNX(final String key, final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value)); connection.close(); return success; } }); } catch (Exception e) { logger.error("setNX redis error, key : {}", key); } return obj != null ? (Boolean) obj : false; } private String getSet(final String key, final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value)); connection.close(); return serializer.deserialize(ret); } }); } catch (Exception e) { logger.error("setNX redis error, key : {}", key); } return obj != null ? (String) obj : null; } /** * 得到 lock. 實現思路: 主要是使用了redis 的setnx命令,緩存了鎖. reids緩存的key是鎖的key,全部的共享, * value是鎖的到期時間(注意:這裏把過時時間放在value了,沒有時間上設置其超時時間) 執行過程: * 1.經過setnx嘗試設置某個key的值,成功(當前沒有這個鎖)則返回,成功得到鎖 * 2.鎖已經存在則獲取鎖的到期時間,和當前時間比較,超時的話,則設置新的值 * * @return true if lock is acquired, false acquire timeouted * @throws InterruptedException * in case of thread interruption */ public synchronized boolean lock() throws InterruptedException { int timeout = timeoutMsecs; while (timeout >= 0) { long expires = System.currentTimeMillis() + expireMsecs + 1; String expiresStr = String.valueOf(expires); // 鎖到期時間 if (this.setNX(lockKey, expiresStr)) { // lock acquired locked = true; return true; } String currentValueStr = this.get(lockKey); // redis裏的時間 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 判斷是否爲空,不爲空的狀況下,若是被其餘線程設置了值,則第二個條件判斷是過不去的 // lock is expired String oldValueStr = this.getSet(lockKey, expiresStr); // 獲取上一個鎖到期時間,並設置如今的鎖到期時間, // 只有一個線程才能獲取上一個線上的設置時間,由於jedis.getSet是同步的 if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 防止誤刪(覆蓋,由於key是相同的)了他人的鎖——這裏達不到效果,這裏值會被覆蓋,可是由於什麼相差了不多的時間,因此能夠接受 // [分佈式的狀況下]:如過這個時候,多個線程剛好都到了這裏,可是隻有一個線程的設置值和當前值相同,他纔有權利獲取鎖 // lock acquired locked = true; return true; } } timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS; /* * 延遲100 毫秒, 這裏使用隨機時間可能會好一點,能夠防止飢餓進程的出現,即,當同時到達多個進程, * 只會有一個進程得到鎖,其餘的都用一樣的頻率進行嘗試,後面有來了一些進行,也以一樣的頻率申請鎖,這將可能致使前面來的鎖得不到知足. * 使用隨機的等待時間能夠必定程度上保證公平性 */ Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS); } return false; } /** * Acqurired lock release. */ public synchronized void unlock() { if (locked) { redisTemplate.delete(lockKey); locked = false; } }
至此基於redis的搶購簡單實現。大佬若是以爲有不妥的地方請指正,小弟也能夠進步一點點。分佈式
感謝你們看到這裏,文章有不足,歡迎你們指出;若是你以爲寫得不錯,那就給我一個贊吧。ide
也歡迎你們關注個人公衆號:程序員麥冬,天天更新行業資訊!測試