【Redis 6】秒殺業務——分布式鎖

在這里插入圖片描述

??各位小伙伴們大家好,歡迎來到這個小扎扎的Redis 6專欄,在這個系列專欄中我對B站黑馬的Redis教程進行一個總結,鑒于 看到就是學到、學到就是賺到 精神,這波依然是血賺 ┗|`O′|┛

💡Redis知識點速覽

  • 🍖 分布式鎖
    • 🥩 業務邏輯分析
    • 🥩 Redis命令
    • 🥩 代碼實現
  • 🍖 分布式鎖誤刪問題
    • 🥩 問題原因分析
    • 🥩 代碼實現
  • 🍖 Lua腳本

??講過上一節的分析可知,服務器集群項目中的鎖是無法精準的鎖住線程資源的,于是我們就是需要使用分布式鎖,分布式鎖該如何使用又有什么注意點呢?就讓我們進入接下來的學習

??首先,使用idea模擬搭建一個tomcat服務器集群,并使用Nginx對集群中的服務器實現負載均衡在這里插入圖片描述在這里插入圖片描述配置完負載均衡之后,發送兩次請求就會在idea的運行窗口中發現,兩次請求的運行是分別在兩個服務器中完成,這就是集群的輪詢機制

🍖 分布式鎖

🥩 業務邏輯分析

??在單JVM虛擬機多線程執行的情況下,可以使用JVM內部的鎖機制來控制多進程的并發執行,借此可以保證一個用戶只能下一個優惠券訂單。但是在分布式的情況下,每一個JVM虛擬機都有一個鎖監視器,不同JVM里的不同線程之間的訪問的并不是同一個鎖監視器,所以說此時再使用synchronized鎖就無法滿足一個用戶限買一單的業務情況了,于是就需要使用分布式鎖

??分布式鎖就是滿足分布式系統或集群模式下多進程可見并且互斥的鎖。一般實現分布式鎖的技術主要就是MySQL、Redis和ZooKeeper,但是綜合對比來看的話,Redis作分布式鎖的性能更高一些,Redis是在JVM虛擬機之外的一種應用可以滿足多線程都可見,互斥可以使用setnx這種的互斥命令來實現,但是使用Redis會存在安全性問題,如果Redis崩潰的話會導致鎖無法釋放而出現死鎖現象,解決這一問題的方案就是使用TTL過期時間,就算崩潰也可以實現到期自動釋放。

🥩 Redis命令

??使用Redis實現分布式鎖的步驟主要就是使用setnx體現互斥鎖,然后expire過期時間防止宕機死鎖,但是如果服務在setnx之后expire之前宕機的話,依舊會造成死鎖現象。于是我們可以使用以下命令在互斥的同時設置超時時間,這樣的話即是在設置鎖之后宕機,依舊可以憑借超時時間釋放鎖

SET lock thread NX EX ttl超時時間

🥩 代碼實現

??將獲取鎖和釋放鎖業務抽取出來,使用接口和實現類來完成

/*** @author : mereign* @date : 2022/6/10 - 12:01* @description : 分布式鎖*/
public interface ILock {/*** 嘗試獲取鎖* @param timeoutSec 鎖的超時時間* @return 是否成功獲取鎖*/boolean tryLock(long timeoutSec);/*** 釋放鎖*/void unLock();
}
/*** @author : mereign* @date : 2022/6/10 - 13:31* @description : 分布式鎖的實現類*/
public class SimpleRedisLock implements ILock {private String name;/***先獲取StringRedisTemplate對象,才能使用代碼操作Redis*/private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {// 獲取當前操作線程的標識long threadId = Thread.currentThread().getId();// 獲取鎖Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);// res是Boolean的包裝類,返回結果的時候涉及到拆箱問題,有可能存在結果為null的情況,此時就需要返回結果與true的比較,避免了空指針風險return Boolean.TRUE.equals(res);}@Overridepublic void unLock() {// 釋放鎖stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name);}
}

??定義了分布式鎖的獲取和釋放,接下來就是在一人一單業務代碼中將鎖機制升級成多線程鎖了,主要修改的代碼為就是5~14行,由單體的synchronized鎖改為使用自定義的Redis鎖,并根據不同線程獲取鎖的不同結果定義了不同的業務

public Result secKillVoucher(Long voucherId) {// 單用戶id(攔截器中做登錄驗證的用戶id)Long userId = UserHolder.getUser().getId();// 創建鎖對象SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);// 獲取鎖boolean isLock = lock.tryLock(1200);// 判斷是否獲取鎖成功if (!isLock) {// 獲取鎖失敗,返回錯誤或者重試return Result.fail("不允許重復下單!" );}// 獲取鎖成功,繼續下單的業務邏輯try {// 查詢優惠券SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);// 獲取時間 判斷秒殺活動是否開始或者結束if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("活動暫未開始");} else if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("活動已經結束");}// 判斷庫存是否充足if (seckillVoucher.getStock() < 1) {return Result.fail("庫存不足,活動結束");}// user_id和voucher_id聯合查詢訂單數int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 訂單數為1 就說明已經下過單了if (count > 0) {return Result.fail("您已經購買過該商品了");}// 扣減庫存boolean update = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!update) {return Result.fail("庫存不足!");}// 創建訂單 并返回idVoucherOrder order = new VoucherOrder();// 訂單id(redis全局唯一id) 下單用戶id(攔截器中做登錄驗證的用戶id) 優惠券id(直接傳過來的id)long orderId = generator.nextId("order");order.setId(orderId);order.setUserId(userId);order.setVoucherId(voucherId);save(order);return Result.ok(orderId);} finally {// 釋放鎖lock.unLock();}
}

🍖 分布式鎖誤刪問題

🥩 問題原因分析

??這個問題出現在Redis鎖設置的超時時間上,由于設置了超時時間,所以可能出現一下情況:即當線程1獲取到鎖之后執行下單業務,但是由于業務堵塞鎖已經超出TTL時間自動釋放;此時線程2趁機獲取Redis鎖成功執行下單業務,線程2的下單業務執行到一半時線程1完成下單使用del命令釋放鎖;此時線程1釋放的是線程2的鎖,于是現在鎖又處于閑置狀態,于是線程3來獲取Redis鎖成功執行下單業務;此時,一共有同一個用戶的兩個線程在同時操作

??為了解決以上出現的問題,需要在每次釋放鎖之前都通過鎖的線程標識(Redis鎖對應的值)判斷一下是不是自己的鎖,如果是就使用del命令釋放鎖,否則就不做操作。但是有一點值得注意,之前鎖的線程標識使用的是線程的name,這樣的話很容易就造成不同JVM虛擬機里的線程name沖突影響判斷,于是可以使用UUID隨機生成一組數字加上線程name作為線程的標識,這樣更能確保唯一性

🥩 代碼實現

??綜上所述,一共有兩處需要改進的地方,一個是使用UUID加線程name作為線程標識(主要修改的是獲取鎖方法加上UUID的獲取),一個是在使用del釋放鎖之前判斷一下是否是自己的鎖

public static final String ID_PREFIX = UUID.randomUUID(true) + "-";public boolean tryLock(long timeoutSec) {// 獲取當前操作線程的標識String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId();// 獲取鎖Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);// res是Boolean的包裝類,返回結果的時候涉及到拆箱問題,有可能存在結果為null的情況,此時就需要返回結果與true的比較,避免了空指針風險return Boolean.TRUE.equals(res);
}
public void unLock() {// 獲取當前操作線程的標識String threadId = RedisConstants.ID_PREFIX + Thread.currentThread().getId();// 通過鎖名 獲取redis中存儲的鎖對應的標識String rid = stringRedisTemplate.opsForValue().get(RedisConstants.KEY_PREFIX + name);if (threadId.equals(rid)) {// 釋放鎖stringRedisTemplate.delete(RedisConstants.KEY_PREFIX + name);}
}

🍖 Lua腳本

??Redis提供了Lua腳本功能,在一個腳本中編寫多條Redis命令,確保多條命令執行時的原子性。Lua是一種編程語言,它的基本語法大家可以參考網站:https://www.runoob.com/lua/lua-tutorial.html
使用Redis命令調用腳本的常見命令可以是:

EVAL “redis.call(‘set’, ‘key’, ‘value’)” num

??上述命令解釋為EVAL是調用,后面雙引號中就是所調用的腳本語句,而最后的num即腳本語句中的KEYS類型參數的個數,num之外的就是ARGV(value)類型的參數。比如說,接下來這一個語句就代表著:setname為Rose,其中KEYS類型的參數有1個,就是num后面的第一個name,剩下的都是ARGV(value)類型的數據,其中調用的是KEYS[1]和ARGV[2],也就是name和Rose

EVAL “redis.call(‘set’, ‘KEYS[1]’, ‘ARGV[2]’)” 1 name age Rose

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/534995.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/534995.shtml
英文地址,請注明出處:http://en.pswp.cn/news/534995.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python讀取文件最后幾行_如何用python獲取文件的最后一行,文件可能會比較大

展開全部 #!/usr/bin/env python import os import sys def get_last_n_lines(logfile, n): blk_size_max 4096 n_lines [] with open(logfile, rb) as fp: fp.seek(0, os.SEEK_END) cur_pos fp.tell() while cur_pos > 0 and len(n_lines) < n: blk_size min(blk_si…

360oauth token是什么意思_Oauth/access token

oauth/access_token第三放應用使用開始的request_token來換取用戶授權過的Access_tokenURL格式標準的OAuth http返回格式HTTP請求方式POST請求數限制false請求參數oauth_consumer_key: 創建應用時生成的APP KEY。oauth_token:經過用戶授權的Request Token。oauth_signature_met…

【Swagger】看這一篇就夠了

各位小伙伴們大家好&#xff0c;歡迎跟著小扎扎一起學習【Swagger】這門技術&#xff0c;在本片博客中我對B站狂神的Swagger教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;Swagger知識點…

python mockito arg_that_wqingxiao

偶然間在脈脈上看到了一道頭條的算法面試題按照題目的理解&#xff0c;簡單的寫了一個html網頁pool開始{{index}}{{index}}var vm new Vue({el: #vue_det,data: {list: [],i: 0},methods: {details: function () {return this.site " - 學的不僅是技術&#xff0c;更是夢…

用python畫大雄_python制作斗圖生成器

網上各種帶文字的表情圖片都被大家玩壞了&#xff0c;今天就和大家一起用 python 親自做一個帶字表情圖片生成器。 不知道大家有沒有看到網上有很多人都在說 "人生苦短&#xff0c;我用 python"&#xff0c;這句話我之前也不是很理解&#xff0c;覺得人生苦短和用pyt…

【spring cloud】(一)使用idea創建可相互調用的多模塊應用

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的spring cloud專欄&#xff0c;在這個系列專欄中我對B站尚硅谷陽哥的spring cloud教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;spr…

epson彩色打印機加墨水_愛普生打印機墨盒如何加墨?

展開全部掌握以下幾點步驟&#xff0c;即可輕松加墨。1、首先&#xff0c;從打印機上取下墨盒&#xff0c;32313133353236313431303231363533e4b893e5b19e31333365646234這里就不好做介紹了&#xff0c;取下墨盒的方法因機而異。將墨盒上的一層貼紙撕掉&#xff0c;并拿出所需材…

python字符串百分號_Python字符串格式化的2種方法

本文介紹了Python字符串格式化&#xff0c;主要有兩種方法&#xff0c;分享給大家&#xff0c;具體如下 用于字符串的拼接&#xff0c;性能更優。 字符串格式化有兩種方式&#xff1a;百分號方式、format方式。 百分號方式比較老&#xff0c;而format方式是比較先進的&#xff…

【踩坑,已解決】spring cloud刪除子模塊后創建同名模塊時遇到的刪除不凈,java、resources包失效,java、resources包被標記為模塊等問題

&#x1f4a1;問題速覽&#x1f4cc; 問題復現&#x1f4cc; 沒有任何操作&#xff0c;model又出現了&#x1f4cc; 模塊重建后java、resources包失效&#x1f4cc; 模塊重建后java、resources包被標記為了模塊&#x1f4cc; 問題復現 本人是在跟著B站尚硅谷的視頻練習Spring c…

藍牙掃描過程解析_智慧定位系統之藍牙網關在室內定位技術的原理淺析-新導智能...

藍牙室內定位技術是利用在室內安裝的若干個藍牙局域網接入點&#xff0c;把網絡維持成根據多用戶的基礎網絡連接形式&#xff0c;并確保藍牙局域網接入點始終是這個微網的主設備&#xff0c;然后經過丈量信號強度對新加入的盲節點進行三角定位。蘇州新導實時定位體系(RTLS)和室…

【spring cloud】(二)服務的注冊發現——Eureka

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的spring cloud專欄&#xff0c;在這個系列專欄中我對B站尚硅谷陽哥的spring cloud教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;spr…

python選擇題題庫百度文庫_大學Python程序題題庫

程序設計 題目&#xff1a;補充 fun 函數&#xff0c;其功能是判斷一個整數是否是素數&#xff0c;在主 函數 main 中輸入一個整數&#xff0c;調用該 fun 函數進行判斷并輸 出結果。 要求&#xff1a;使用 math 相關函數 import math def fun(n): i,w2,0 if n<1: w1 while …

新手攻略熔爐_我的世界攻略:生存模式新手攻略

《我的世界》(英文:《minecraft》)是一款風靡全球的高自由度沙盒游戲&#xff0c;由瑞典MojangAB和4J Studios開發。國際版由微軟Mojang工作室開發&#xff0c;中國版由網易代理。Minecraft著重于讓玩家去探索、交互&#xff0c;并且改變一個由一立方米大小的方塊動態生成的地圖…

【spring cloud】(三)服務調用——Ribbon、OpenFeign

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的spring cloud專欄&#xff0c;在這個系列專欄中我對B站尚硅谷陽哥的spring cloud教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;服務…

c語言斐波那契數列_劍指Offer-10-I.斐波那契數列

題目題目描述寫一個函數&#xff0c;輸入 n &#xff0c;求斐波那契&#xff08;Fibonacci&#xff09;數列的第 n 項。斐波那契數列的定義如下&#xff1a; F(0) 0, F(1) 1F(N) F(N - 1) F(N - 2), 其中 N > 1. 斐波那契數列由 0 和 1 開始&#xff0c;之后的斐波那契數…

mysql 導入 mssql_MySQL(csv,text)導入mssql使用方法

MySQL(csv,text)導入mssql是非常的簡單了但是在導入過程中會碰到text字段問題了&#xff0c;下面我們就來看一篇關于MySQL(csv,text)導入mssql使用方法吧&#xff0c;具體的操作細節如下所示。分兩步處理&#xff0c;第一步是將csv導入到mysql。沒有使用mssql自帶客戶端的導入功…

c# mvvm模式獲取當前窗口_AWTK-MVVM 介紹

MVVM(Model-View-ViewModel)介紹8.1 分離用戶界面和業務邏輯在開發應用程序時&#xff0c;要把用戶界面和業務邏輯分離開來&#xff0c;這是每個程序員都知道的常識。分離用戶界面和業務邏輯有幾個重要的好處&#xff1a;有利于隔離變化。用戶界面是最容易變化的&#xff0c;易…

【spring cloud】(三)服務降級——Hystrix

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的spring cloud專欄&#xff0c;在這個系列專欄中我對B站尚硅谷陽哥的spring cloud教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;服務…

mysql高級查詢教程_MYSQL高級查詢

實際開發中&#xff0c;經常需要對某些數據進行統計&#xff0c;比如&#xff0c;統計某個字段的最大值、最小值、平均值等。MySQL中&#xff0c;提供了一些函數來實現這些功能聚合函數COUNT()——返回某列的行數SUM()——返回某列值的和AVG()——返回某列的平均值MAX()——返回…

【dubbo】(一) dubbo是什么?

各位小伙伴們大家好&#xff0c;歡迎來到這個小扎扎的dubbo專欄&#xff0c;在這個系列專欄中我對B站尚硅谷雷神的dubbo教程進行一個總結&#xff0c;鑒于 看到就是學到、學到就是賺到 精神&#xff0c;這波依然是血賺 ┗|&#xff40;O′|┛ &#x1f4a1;dubbo知識點速覽&a…