Redis分布式鎖介紹與使用
首先,使用idea模擬搭建一個(gè)tomcat服務(wù)器集群,并使用Nginx對(duì)集群中的服務(wù)器實(shí)現(xiàn)負(fù)載均衡
配置完負(fù)載均衡之后,發(fā)送兩次請(qǐng)求就會(huì)在idea的運(yùn)行窗口中發(fā)現(xiàn),兩次請(qǐng)求的運(yùn)行是分別在兩個(gè)服務(wù)器中完成,這就是集群的輪詢機(jī)制
分布式鎖
業(yè)務(wù)邏輯分析
在單JVM虛擬機(jī)多線程執(zhí)行的情況下,可以使用JVM內(nèi)部的鎖機(jī)制來(lái)控制多進(jìn)程的并發(fā)執(zhí)行,借此可以保證一個(gè)用戶只能下一個(gè)優(yōu)惠券訂單。但是在分布式的情況下,每一個(gè)JVM虛擬機(jī)都有一個(gè)鎖監(jiān)視器,不同JVM里的不同線程之間的訪問(wèn)的并不是同一個(gè)鎖監(jiān)視器,所以說(shuō)此時(shí)再使用synchronized鎖就無(wú)法滿足一個(gè)用戶限買一單的業(yè)務(wù)情況了,于是就需要使用分布式鎖
分布式鎖就是滿足分布式系統(tǒng)或集群模式下多進(jìn)程可見(jiàn)并且互斥的鎖。一般實(shí)現(xiàn)分布式鎖的技術(shù)主要就是MySQL、Redis和ZooKeeper,但是綜合對(duì)比來(lái)看的話,Redis作分布式鎖的性能更高一些,Redis是在JVM虛擬機(jī)之外的一種應(yīng)用可以滿足多線程都可見(jiàn),互斥可以使用setnx這種的互斥命令來(lái)實(shí)現(xiàn),但是使用Redis會(huì)存在安全性問(wèn)題,如果Redis崩潰的話會(huì)導(dǎo)致鎖無(wú)法釋放而出現(xiàn)死鎖現(xiàn)象,解決這一問(wèn)題的方案就是使用TTL過(guò)期時(shí)間,就算崩潰也可以實(shí)現(xiàn)到期自動(dòng)釋放。
Redis命令
使用Redis實(shí)現(xiàn)分布式鎖的步驟主要就是使用setnx體現(xiàn)互斥鎖,然后expire過(guò)期時(shí)間防止宕機(jī)死鎖,但是如果服務(wù)在setnx之后expire之前宕機(jī)的話,依舊會(huì)造成死鎖現(xiàn)象。于是我們可以使用以下命令在互斥的同時(shí)設(shè)置超時(shí)時(shí)間,這樣的話即是在設(shè)置鎖之后宕機(jī),依舊可以憑借超時(shí)時(shí)間釋放鎖
SET lock thread NX EX ttl超時(shí)時(shí)間
代碼實(shí)現(xiàn)
將獲取鎖和釋放鎖業(yè)務(wù)抽取出來(lái),使用接口和實(shí)現(xiàn)類來(lái)完成
public interface ILock { /** * 嘗試獲取鎖 * @param timeoutSec 鎖的超時(shí)時(shí)間 * @return 是否成功獲取鎖 */ boolean tryLock(long timeoutSec); /** * 釋放鎖 */ void unLock(); }
public class SimpleRedisLock implements ILock { private String name; /** *先獲取StringRedisTemplate對(duì)象,才能使用代碼操作Redis */ private StringRedisTemplate stringRedisTemplate; public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { this.name = name; this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean tryLock(long timeoutSec) { // 獲取當(dāng)前操作線程的標(biāo)識(shí) long threadId = Thread.currentThread().getId(); // 獲取鎖 Boolean res = stringRedisTemplate.opsForValue() .setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); // res是Boolean的包裝類,返回結(jié)果的時(shí)候涉及到拆箱問(wèn)題,有可能存在結(jié)果為null的情況,此時(shí)就需要返回結(jié)果與true的比較,避免了空指針風(fēng)險(xiǎn) return Boolean.TRUE.equals(res); } @Override public void unLock() { // 釋放鎖 stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name); } }
定義了分布式鎖的獲取和釋放,接下來(lái)就是在一人一單業(yè)務(wù)代碼中將鎖機(jī)制升級(jí)成多線程鎖了,主要修改的代碼為就是5~14行,由單體的synchronized鎖改為使用自定義的Redis鎖,并根據(jù)不同線程獲取鎖的不同結(jié)果定義了不同的業(yè)務(wù)
public Result secKillVoucher(Long voucherId) { // 單用戶id(攔截器中做登錄驗(yàn)證的用戶id) Long userId = UserHolder.getUser().getId(); // 創(chuàng)建鎖對(duì)象 SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate); // 獲取鎖 boolean isLock = lock.tryLock(1200); // 判斷是否獲取鎖成功 if (!isLock) { // 獲取鎖失敗,返回錯(cuò)誤或者重試 return Result.fail("不允許重復(fù)下單!" ); } // 獲取鎖成功,繼續(xù)下單的業(yè)務(wù)邏輯 try { // 查詢優(yōu)惠券 SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId); // 獲取時(shí)間 判斷秒殺活動(dòng)是否開(kāi)始或者結(jié)束 if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) { return Result.fail("活動(dòng)暫未開(kāi)始"); } else if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) { return Result.fail("活動(dòng)已經(jīng)結(jié)束"); } // 判斷庫(kù)存是否充足 if (seckillVoucher.getStock() < 1) { return Result.fail("庫(kù)存不足,活動(dòng)結(jié)束"); } // user_id和voucher_id聯(lián)合查詢訂單數(shù) int count = query().eq("user_id", userId) .eq("voucher_id", voucherId) .count(); // 訂單數(shù)為1 就說(shuō)明已經(jīng)下過(guò)單了 if (count > 0) { return Result.fail("您已經(jīng)購(gòu)買過(guò)該商品了"); } // 扣減庫(kù)存 boolean update = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId) .gt("stock", 0) .update(); if (!update) { return Result.fail("庫(kù)存不足!"); } // 創(chuàng)建訂單 并返回id VoucherOrder order = new VoucherOrder(); // 訂單id(redis全局唯一id) 下單用戶id(攔截器中做登錄驗(yàn)證的用戶id) 優(yōu)惠券id(直接傳過(guò)來(lái)的id) long orderId = generator.nextId("order"); order.setId(orderId); order.setUserId(userId); order.setVoucherId(voucherId); save(order); return Result.ok(orderId); } finally { // 釋放鎖 lock.unLock(); } }
分布式鎖誤刪問(wèn)題
問(wèn)題原因分析
這個(gè)問(wèn)題出現(xiàn)在Redis鎖設(shè)置的超時(shí)時(shí)間上,由于設(shè)置了超時(shí)時(shí)間,所以可能出現(xiàn)一下情況:即當(dāng)線程1獲取到鎖之后執(zhí)行下單業(yè)務(wù),但是由于業(yè)務(wù)堵塞鎖已經(jīng)超出TTL時(shí)間自動(dòng)釋放;此時(shí)線程2趁機(jī)獲取Redis鎖成功執(zhí)行下單業(yè)務(wù),線程2的下單業(yè)務(wù)執(zhí)行到一半時(shí)線程1完成下單使用del命令釋放鎖;此時(shí)線程1釋放的是線程2的鎖,于是現(xiàn)在鎖又處于閑置狀態(tài),于是線程3來(lái)獲取Redis鎖成功執(zhí)行下單業(yè)務(wù);此時(shí),一共有同一個(gè)用戶的兩個(gè)線程在同時(shí)操作
為了解決以上出現(xiàn)的問(wèn)題,需要在每次釋放鎖之前都通過(guò)鎖的線程標(biāo)識(shí)(Redis鎖對(duì)應(yīng)的值)判斷一下是不是自己的鎖,如果是就使用del命令釋放鎖,否則就不做操作。但是有一點(diǎn)值得注意,之前鎖的線程標(biāo)識(shí)使用的是線程的name,這樣的話很容易就造成不同JVM虛擬機(jī)里的線程name沖突影響判斷,于是可以使用UUID隨機(jī)生成一組數(shù)字加上線程name作為線程的標(biāo)識(shí),這樣更能確保唯一性
代碼實(shí)現(xiàn)
綜上所述,一共有兩處需要改進(jìn)的地方,一個(gè)是使用UUID加線程name作為線程標(biāo)識(shí)(主要修改的是獲取鎖方法加上UUID的獲取),一個(gè)是在使用del釋放鎖之前判斷一下是否是自己的鎖
public static final String ID_PREFIX = UUID.randomUUID(true) + "-"; public boolean tryLock(long timeoutSec) { // 獲取當(dāng)前操作線程的標(biāo)識(shí) String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId(); // 獲取鎖 Boolean res = stringRedisTemplate.opsForValue() .setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); // res是Boolean的包裝類,返回結(jié)果的時(shí)候涉及到拆箱問(wèn)題,有可能存在結(jié)果為null的情況,此時(shí)就需要返回結(jié)果與true的比較,避免了空指針風(fēng)險(xiǎn) return Boolean.TRUE.equals(res); }
public void unLock() { // 獲取當(dāng)前操作線程的標(biāo)識(shí) String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId(); // 通過(guò)鎖名 獲取redis中存儲(chǔ)的鎖對(duì)應(yīng)的標(biāo)識(shí) String rid = stringRedisTemplate.opsForValue().get(RedisConstants.KEY_PREFIX + name); if (threadId.equals(rid)) { // 釋放鎖 stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name); } }
Lua腳本
Redis提供了Lua腳本功能,在一個(gè)腳本中編寫(xiě)多條Redis命令,確保多條命令執(zhí)行時(shí)的原子性。Lua是一種編程語(yǔ)言,它的基本語(yǔ)法大家可以參考網(wǎng)站:傳送門(mén)
使用Redis命令調(diào)用腳本的常見(jiàn)命令可以是:
EVAL “redis.call(‘set’, ‘key’, ‘value’)” num
上述命令解釋為EVAL是調(diào)用,后面雙引號(hào)中就是所調(diào)用的腳本語(yǔ)句,而最后的num即腳本語(yǔ)句中的KEYS類型參數(shù)的個(gè)數(shù),num之外的就是ARGV(value)類型的參數(shù)。比如說(shuō),接下來(lái)這一個(gè)語(yǔ)句就代表著:setname為Rose,其中KEYS類型的參數(shù)有1個(gè),就是num后面的第一個(gè)name,剩下的都是ARGV(value)類型的數(shù)據(jù),其中調(diào)用的是KEYS[1]和ARGV[2],也就是name和Rose
EVAL “redis.call(‘set’, ‘KEYS[1]’, ‘ARGV[2]’)” 1 name age Rose
到此這篇關(guān)于Redis分布式鎖介紹與使用的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud?Gateway實(shí)現(xiàn)API接口加解密
這篇文章主要為大家介紹了SpringCloud?Gateway如何實(shí)現(xiàn)API接口加解密的,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)有一定的幫助,需要的可以參考一下2022-06-06Spring Cloud Gateway不同頻率限流的解決方案(每分鐘,每小時(shí),每天)
SpringCloud Gateway 是 Spring Cloud 的一個(gè)全新項(xiàng)目,它旨在為微服務(wù)架構(gòu)提供一種簡(jiǎn)單有效的統(tǒng)一的 API 路由管理方式。這篇文章主要介紹了Spring Cloud Gateway不同頻率限流(每分鐘,每小時(shí),每天),需要的朋友可以參考下2020-10-10java實(shí)現(xiàn)雷霆戰(zhàn)機(jī)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)雷霆戰(zhàn)機(jī),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06詳解Spring MVC的異步模式(高性能的關(guān)鍵)
本篇文章主要介紹了詳解Spring MVC的異步模式(高性能的關(guān)鍵),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02ActiveMQ簡(jiǎn)單入門(mén)(新手必看篇)
下面小編就為大家?guī)?lái)一篇ActiveMQ簡(jiǎn)單入門(mén)(新手必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06java實(shí)現(xiàn)jdbc查詢結(jié)果集result轉(zhuǎn)換成對(duì)應(yīng)list集合
本文給大家匯總介紹了java實(shí)現(xiàn)jdbc查詢結(jié)果集result轉(zhuǎn)換成對(duì)應(yīng)list集合,十分的簡(jiǎn)單,有相同需求的小伙伴可以參考下。2015-12-12