Redis 分布式鎖使用不當(dāng),釀成一個(gè)重大事故,超賣了100瓶飛天茅臺(tái)?。?!
來源:juejin.cn/post/6854573212831842311
事故現(xiàn)場(chǎng)
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {SeckillActivityRequestVO response;String key = "key:" + request.getSeckillId;try {Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);if (lockFlag) {// HTTP請(qǐng)求用戶服務(wù)進(jìn)行用戶相關(guān)的校驗(yàn)// 用戶活動(dòng)校驗(yàn)// 庫(kù)存校驗(yàn)Object stock = redisTemplate.opsForHash().get(key+":info", "stock");assert stock != null;if (Integer.parseInt(stock.toString()) <= 0) {// 業(yè)務(wù)異常} else {redisTemplate.opsForHash().increment(key+":info", "stock", -1);// 生成訂單// 發(fā)布訂單創(chuàng)建成功事件// 構(gòu)建響應(yīng)VO}}} finally {// 釋放鎖stringRedisTemplate.delete("key");// 構(gòu)建響應(yīng)VO}return response;}
事故原因
事故分析
仔細(xì)分析下來,可以發(fā)現(xiàn),這個(gè)搶購(gòu)接口在高并發(fā)場(chǎng)景下,是有嚴(yán)重的安全隱患的,主要集中在三個(gè)地方:
沒有其他系統(tǒng)風(fēng)險(xiǎn)容錯(cuò)處理
由于用戶服務(wù)吃緊,網(wǎng)關(guān)響應(yīng)延遲,但沒有任何應(yīng)對(duì)方式,這是超賣的導(dǎo)火索。
看似安全的分布式鎖其實(shí)一點(diǎn)都不安全
雖然采用了set key value [EX seconds] [PX milliseconds] [NX|XX]的方式,但是如果線程A執(zhí)行的時(shí)間較長(zhǎng)沒有來得及釋放,鎖就過期了,此時(shí)線程B是可以獲取到鎖的。當(dāng)線程A執(zhí)行完成之后,釋放鎖,實(shí)際上就把線程B的鎖釋放掉了。這個(gè)時(shí)候,線程C又是可以獲取到鎖的,而此時(shí)如果線程B執(zhí)行完釋放鎖實(shí)際上就是釋放的線程C設(shè)置的鎖。這是超賣的直接原因。
非原子性的庫(kù)存校驗(yàn)
解決方案
知道了原因之后,我們就可以對(duì)癥下藥了。
實(shí)現(xiàn)相對(duì)安全的分布式鎖
public void safedUnLock(String key, String val) {String luaScript = "local in = ARGV[1] local curr=redis.call('get', KEYS[1]) if in==curr then redis.call('del', KEYS[1]) end return 'OK'"";RedisScript<String> redisScript = RedisScript.of(luaScript);redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));}
我們通過LUA腳本來實(shí)現(xiàn)安全地解鎖。
實(shí)現(xiàn)安全的庫(kù)存校驗(yàn)
// redis會(huì)返回操作之后的結(jié)果,這個(gè)過程是原子性的Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);
改進(jìn)之后的代碼
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {SeckillActivityRequestVO response;String key = "key:" + request.getSeckillId();String val = UUID.randomUUID().toString();try {Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);if (!lockFlag) {// 業(yè)務(wù)異常}// 用戶活動(dòng)校驗(yàn)// 庫(kù)存校驗(yàn),基于redis本身的原子性來保證Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);if (currStock < 0) { // 說明庫(kù)存已經(jīng)扣減完了。// 業(yè)務(wù)異常。log.error("[搶購(gòu)下單] 無庫(kù)存");} else {// 生成訂單// 發(fā)布訂單創(chuàng)建成功事件// 構(gòu)建響應(yīng)}} finally {distributedLocker.safedUnLock(key, val);// 構(gòu)建響應(yīng)}return response;}
分布式鎖有必要么
改進(jìn)之后,其實(shí)可以發(fā)現(xiàn),我們借助于redis本身的原子性扣減庫(kù)存,也是可以保證不會(huì)超賣的。對(duì)的。但是如果沒有這一層鎖的話,那么所有請(qǐng)求進(jìn)來都會(huì)走一遍業(yè)務(wù)邏輯,由于依賴了其他系統(tǒng),此時(shí)就會(huì)造成對(duì)其他系統(tǒng)的壓力增大。這會(huì)增加的性能損耗和服務(wù)不穩(wěn)定性,得不償失?;诜植际芥i可以在一定程度上攔截一些流量。
分布式鎖的選型
有人提出用RedLock來實(shí)現(xiàn)分布式鎖。RedLock的可靠性更高,但其代價(jià)是犧牲一定的性能。在本場(chǎng)景,這點(diǎn)可靠性的提升遠(yuǎn)不如性能的提升帶來的性價(jià)比高。如果對(duì)于可靠性極高要求的場(chǎng)景,則可以采用RedLock來實(shí)現(xiàn)。
再次思考分布式鎖有必要么
// 通過消息提前初始化好,借助ConcurrentHashMap實(shí)現(xiàn)高效線程安全private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();// 通過消息提前設(shè)置好。由于AtomicInteger本身具備原子性,因此這里可以直接使用HashMapprivate static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();...public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {SeckillActivityRequestVO response;Long seckillId = request.getSeckillId();if(!SECKILL_FLAG_MAP.get(requestseckillId)) {// 業(yè)務(wù)異常}// 用戶活動(dòng)校驗(yàn)// 庫(kù)存校驗(yàn)if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {SECKILL_FLAG_MAP.put(seckillId, false);// 業(yè)務(wù)異常}// 生成訂單// 發(fā)布訂單創(chuàng)建成功事件// 構(gòu)建響應(yīng)return response;}
3、心態(tài)崩了!稅前2萬4,到手1萬4,年終獎(jiǎng)扣稅方式1月1日起施行~
4、雷軍做程序員時(shí)寫的博客,很強(qiáng)大!
5、人臉識(shí)別的時(shí)候,一定要穿上衣服??!
