<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在單體應用中,如果我們對共用資料不進行加鎖操作,會出現資料一致性問題,我們的解決辦法通常是加鎖。
在分散式架構中,我們同樣會遇到資料共用操作問題,本文章使用Redis
來解決分散式架構中的資料一致性問題。
單機資料一致性架構如下圖所示:多個可客戶存取同一個伺服器,連線同一個資料庫。
場景描述:使用者端模擬購買商品過程,在Redis
中設定庫存總數剩100個
,多個使用者端同時並行購買。
@RestController public class IndexController1 { @Autowired StringRedisTemplate template; @RequestMapping("/buy1") public String index(){ // Redis中存有goods:001號商品,數量為100 String result = template.opsForValue().get("goods:001"); // 獲取到剩餘商品數 int total = result == null ? 0 : Integer.parseInt(result); if( total > 0 ){ // 剩餘商品數大於0 ,則進行扣減 int realTotal = total -1; // 將商品數回寫資料庫 template.opsForValue().set("goods:001",String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:"+realTotal +"件, 伺服器埠為8001"); return "購買商品成功,庫存還剩:"+realTotal +"件, 伺服器埠為8001"; }else{ System.out.println("購買商品失敗,伺服器埠為8001"); } return "購買商品失敗,伺服器埠為8001"; } }
使用Jmeter
模擬高並行場景,測試結果如下:
測試結果出現多個使用者購買同一商品,發生了資料不一致問題!
解決辦法:單體應用的情況下,對並行的操作進行加鎖操作,保證對資料的操作具有原子性
synchronized
ReentrantLock
@RestController public class IndexController2 { // 使用ReentrantLock鎖解決單體應用的並行問題 Lock lock = new ReentrantLock(); @Autowired StringRedisTemplate template; @RequestMapping("/buy2") public String index() { lock.lock(); try { String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"; } else { System.out.println("購買商品失敗,伺服器埠為8001"); } } catch (Exception e) { lock.unlock(); } finally { lock.unlock(); } return "購買商品失敗,伺服器埠為8001"; } }
上面解決了單體應用的資料一致性問題,但如果是分散式架構部署呢,架構如下:
提供兩個服務,埠分別為8001
、8002
,連線同一個Redis
服務,在服務前面有一臺Nginx
作為負載均衡
兩臺服務程式碼相同,只是埠不同
將8001
、8002
兩個服務啟動,每個服務依然用ReentrantLock
加鎖,用Jmeter
做並行測試,發現會出現資料一致性問題!
取消單機鎖,下面使用redis
的set
命令來實現分散式加鎖
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
@RestController public class IndexController4 { // Redis分散式鎖的key public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy4") public String index(){ // 每個人進來先要進行加鎖,key值為"good_lock",value隨機生成 String value = UUID.randomUUID().toString().replace("-",""); try{ // 加鎖 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value); // 加鎖失敗 if(!flag){ return "搶鎖失敗!"; } System.out.println( value+ " 搶鎖成功"); String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); // 如果在搶到所之後,刪除鎖之前,發生了異常,鎖就無法被釋放, // 釋放鎖操作不能在此操作,要在finally處理 // template.delete(REDIS_LOCK); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"; } else { System.out.println("購買商品失敗,伺服器埠為8001"); } return "購買商品失敗,伺服器埠為8001"; }finally { // 釋放鎖 template.delete(REDIS_LOCK); } } }
上面的程式碼,可以解決分散式架構中資料一致性問題。但再仔細想想,還是會有問題,下面進行改進。
在上面的程式碼中,如果程式在執行期間,部署了微服務jar
包的機器突然掛了,程式碼層面根本就沒有走到finally
程式碼塊,也就是說在宕機前,鎖並沒有被刪除掉,這樣的話,就沒辦法保證解鎖
所以,這裡需要對這個key
加一個過期時間,Redis
中設定過期時間有兩種方法:
template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)
第一種方法需要單獨的一行程式碼,且並沒有與加鎖放在同一步操作,所以不具備原子性,也會出問題
第二種方法在加鎖的同時就進行了設定過期時間,所有沒有問題,這裡採用這種方式
調整下程式碼,在加鎖的同時,設定過期時間:
// 為key加一個過期時間,其餘程式碼不變 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
這種方式解決了因服務突然宕機而無法釋放鎖的問題。但再仔細想想,還是會有問題,下面進行改進。
方式二設定了key
的過期時間,解決了key
無法刪除的問題,但問題又來了
上面設定了key
的過期時間為10
秒,如果業務邏輯比較複雜,需要呼叫其他微服務,處理時間需要15
秒(模擬場
景,別較真),而當10
秒鐘過去之後,這個key
就過期了,其他請求就又可以設定這個key
,此時如果耗時15
秒
的請求處理完了,回來繼續執行程式,就會把別人設定的key
給刪除了,這是個很嚴重的問題!
所以,誰上的鎖,誰才能刪除
@RestController public class IndexController6 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy6") public String index(){ // 每個人進來先要進行加鎖,key值為"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ // 為key加一個過期時間 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS); // 加鎖失敗 if(!flag){ return "搶鎖失敗!"; } System.out.println( value+ " 搶鎖成功"); String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此處需要呼叫其他微服務,處理時間較長。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"; } else { System.out.println("購買商品失敗,伺服器埠為8001"); } return "購買商品失敗,伺服器埠為8001"; }finally { // 誰加的鎖,誰才能刪除!!!! if(template.opsForValue().get(REDIS_LOCK).equals(value)){ template.delete(REDIS_LOCK); } } } }
這種方式解決了因服務處理時間太長而釋放了別人鎖的問題。這樣就沒問題了嗎?
在上面方式三下,規定了誰上的鎖,誰才能刪除,但finally
快的判斷和del
刪除操作不是原子操作,並行的時候也會出問題,並行嘛,就是要保證資料的一致性,保證資料的一致性,最好要保證對資料的操作具有原子性。
在Redis
的set
命令介紹中,最後推薦Lua
指令碼進行鎖的刪除,地址
@RestController public class IndexController7 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @RequestMapping("/buy7") public String index(){ // 每個人進來先要進行加鎖,key值為"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ // 為key加一個過期時間 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS); // 加鎖失敗 if(!flag){ return "搶鎖失敗!"; } System.out.println( value+ " 搶鎖成功"); String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此處需要呼叫其他微服務,處理時間較長。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"; } else { System.out.println("購買商品失敗,伺服器埠為8001"); } return "購買商品失敗,伺服器埠為8001"; }finally { // 誰加的鎖,誰才能刪除,使用Lua指令碼,進行鎖的刪除 Jedis jedis = null; try{ jedis = RedisUtils.getJedis(); String script = "if redis.call('get',KEYS[1]) == ARGV[1] " + "then " + "return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if("1".equals(eval.toString())){ System.out.println("-----del redis lock ok...."); }else{ System.out.println("-----del redis lock error ...."); } }catch (Exception e){ }finally { if(null != jedis){ jedis.close(); } } } } }
在方式四下,規定了誰上的鎖,誰才能刪除,並且解決了刪除操作沒有原子性問題。但還沒有考慮快取續命,以及Redis
叢集部署下,非同步複製造成的鎖丟失:主節點沒來得及把剛剛set
進來這條資料給從節點,就掛了。所以直接上RedLock
的Redisson
落地實現。
@RestController public class IndexController8 { public static final String REDIS_LOCK = "good_lock"; @Autowired StringRedisTemplate template; @Autowired Redisson redisson; @RequestMapping("/buy8") public String index(){ RLock lock = redisson.getLock(REDIS_LOCK); lock.lock(); // 每個人進來先要進行加鎖,key值為"good_lock" String value = UUID.randomUUID().toString().replace("-",""); try{ String result = template.opsForValue().get("goods:001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此處需要呼叫其他微服務,處理時間較長。。。 int realTotal = total - 1; template.opsForValue().set("goods:001", String.valueOf(realTotal)); System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"); return "購買商品成功,庫存還剩:" + realTotal + "件, 伺服器埠為8001"; } else { System.out.println("購買商品失敗,伺服器埠為8001"); } return "購買商品失敗,伺服器埠為8001"; }finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
分析問題的過程,也是解決問題的過程,也能鍛鍊自己編寫程式碼時思考問題的方式和角度。
以上就是Redis實現分散式鎖的五種方法詳解的詳細內容,更多關於Redis分散式鎖的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45