分布式鎖之傳統鎖回顧(一)

1. 傳統鎖回顧

1.1. 從減庫存聊起

多線程并發安全問題最典型的代表就是超賣現象

庫存在并發量較大情況下很容易發生超賣現象,一旦發生超賣現象,就會出現多成交了訂單而發不了貨的情況。

場景:

商品S庫存余量為5時,用戶A和B同時來購買一個商品,此時查詢庫存數都為5,庫存充足則開始減庫存:

用戶A:update db_stock set stock = stock - 1 where id = 1

用戶B:update db_stock set stock = stock - 1 where id = 1

并發情況下,更新后的結果可能是4,而實際的最終庫存量應該是3才對

1.2. 環境準備

建表語句:

CREATE TABLE `db_stock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`product_code` varchar(255) DEFAULT NULL COMMENT '商品編號',`stock_code` varchar(255) DEFAULT NULL COMMENT '倉庫編號',`count` int(11) DEFAULT NULL COMMENT '庫存量',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

表中數據如下:

1001商品在001倉庫有5000件庫存。

創建分布式鎖demo工程:

創建好之后:

pom.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.11.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu</groupId><artifactId>distributed-lock</artifactId><version>0.0.1-SNAPSHOT</version><name>distributed-lock</name><description>分布式鎖demo工程</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.46</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.16</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

application.yml配置文件:

server:port: 6000
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://172.16.116.100:3306/testusername: rootpassword: rootredis:host: 172.16.116.100

DistributedLockApplication啟動類:

@SpringBootApplication
@MapperScan("com.atguigu.distributedlock.mapper")
public class DistributedLockApplication {public static void main(String[] args) {SpringApplication.run(DistributedLockApplication.class, args);}}

Stock實體類:

@Data
@TableName("db_stock")
public class Stock {@TableIdprivate Long id;private String productCode;private String stockCode;private Integer count;
}

StockMapper接口:

public interface StockMapper extends BaseMapper<Stock> {
}

1.3. 簡單實現減庫存

接下來咱們代碼實操一下。

StockController:

@RestController
public class StockController {@Autowiredprivate StockService stockService;@GetMapping("check/lock")public String checkAndLock(){this.stockService.checkAndLock();return "驗庫存并鎖庫存成功!";}
}

StockService:

@Service
public class StockService {@Autowiredprivate StockMapper stockMapper;public void checkAndLock() {// 先查詢庫存是否充足Stock stock = this.stockMapper.selectById(1L);// 再減庫存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}}
}

測試:

查看數據庫:

在瀏覽器中一個一個訪問時,每訪問一次,庫存量減1,沒有任何問題。

1.4. 演示超賣現象

接下來咱們使用jmeter壓力測試工具,高并發下壓測一下,添加線程組:并發100循環50次,即5000次請求。

給線程組添加HTTP Request請求:

填寫測試接口路徑如下:

再選擇你想要的測試報表,例如這里選擇聚合報告:

啟動測試,查看壓力測試報告:

  • Label 取樣器別名,如果勾選Include group name ,則會添加線程組的名稱作為前綴

  • # Samples 取樣器運行次數

  • Average 請求(事務)的平均響應時間

  • Median 中位數

  • 90% Line 90%用戶響應時間

  • 95% Line 90%用戶響應時間

  • 99% Line 90%用戶響應時間

  • Min 最小響應時間

  • Max 最大響應時間

  • Error 錯誤率

  • Throughput 吞吐率

  • Received KB/sec 每秒收到的千字節

  • Sent KB/sec 每秒收到的千字節

測試結果:請求總數5000次,平均請求時間37ms,中位數(50%)請求是在36ms內完成的,錯誤率0%,每秒鐘平均吞吐量2568.1次。

查看mysql數據庫剩余庫存數:還有4870

此時如果還有人來下單,就會出現超賣現象(別人購買成功,而無貨可發)。

1.5. jvm鎖問題演示

1.5.1. 添加jvm鎖

使用jvm鎖(synchronized關鍵字或者ReetrantLock)試試:

重啟tomcat服務,再次使用jmeter壓力測試,效果如下:

查看mysql數據庫:

并沒有發生超賣現象,完美解決。

1.5.2. 原理

添加synchronized關鍵字之后,StockService就具備了對象鎖,由于添加了獨占的排他鎖,同一時刻只有一個請求能夠獲取到鎖,并減庫存。此時,所有請求只會one-by-one執行下去,也就不會發生超賣現象。

1.6. 多服務問題

使用jvm鎖在單工程單服務情況下確實沒有問題,但是在集群情況下會怎樣?

接下啟動多個服務并使用nginx負載均衡,結構如下:

啟動三個服務(端口號分別8000 8100 8200),如下:

1.6.1. 安裝配置nginx

基于安裝nginx:

# 拉取鏡像
docker pull nginx:latest
# 創建nginx對應資源、日志及配置目錄
mkdir -p /opt/nginx/logs /opt/nginx/conf /opt/nginx/html
# 先在conf目錄下創建nginx.conf文件,配置內容參照下方
# 再運行容器
docker run -d -p 80:80 --name nginx -v /opt/nginx/html:/usr/share/nginx/html -v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v /opt/nginx/logs:/var/log/nginx nginx

nginx.conf配置如下:

user  nginx;
worker_processes  1;error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;events {worker_connections  1024;
}http {include       /etc/nginx/mime.types;default_type  application/octet-stream;log_format  main  '$remote_addr - $remote_user [$time_local] "$request" ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "$http_x_forwarded_for"';access_log  /var/log/nginx/access.log  main;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;#gzip  on;#include /etc/nginx/conf.d/*.conf;upstream distributed {server 172.16.116.1:8000;server 172.16.116.1:8100;server 172.16.116.1:8200;}server {listen       80;server_name  172.16.116.100;location / {proxy_pass http://distributed;}}}

在瀏覽器中測試:172.16.116.100是我的nginx服務器地址

經過測試,通過nginx訪問服務一切正常。

1.6.2. Jmeter壓力測試

注意:先把數據庫庫存量還原到5000。

參照之前的測試用例,再創建一個新的測試組:參數給之前一樣

配置nginx的地址及 服務的訪問路徑如下:

測試結果:性能只是略有提升。

數據庫庫存剩余量如下:

又出現了并發問題,即出現了超賣現象。

1.7. mysql鎖演示

除了使用jvm鎖之外,還可以使用數據鎖:悲觀鎖 或者 樂觀鎖

  1. 一個sql:直接更新時判斷,在更新中判斷庫存是否大于0

    update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0 ;

  2. 悲觀鎖:在讀取數據時鎖住那幾行,其他對這幾行的更新需要等到悲觀鎖結束時才能繼續 。

    select ... for update

  3. 樂觀鎖:讀取數據時不鎖,更新時檢查是否數據已經被更新過,如果是則取消當前更新進行重試。

    version 或者 時間戳(CAS思想)。

1.7.1. 一個sql

略。。

1.7.2. 悲觀鎖

在MySQL的InnoDB中,預設的Tansaction isolation level 為REPEATABLE READ(可重讀)

在SELECT 的讀取鎖定主要分為兩種方式:

  • SELECT ... LOCK IN SHARE MODE (共享鎖)

  • SELECT ... FOR UPDATE (悲觀鎖)

這兩種方式在事務(Transaction) 進行當中SELECT 到同一個數據表時,都必須等待其它事務數據被提交(Commit)后才會執行。

而主要的不同在于LOCK IN SHARE MODE 在有一方事務要Update 同一個表單時很容易造成死鎖。

簡單的說,如果SELECT 后面若要UPDATE 同一個表單,最好使用SELECT ... FOR UPDATE。

代碼實現

改造StockService:

在StockeMapper中定義selectStockForUpdate方法:

public interface StockMapper extends BaseMapper<Stock> {public Stock selectStockForUpdate(Long id);
}

在StockMapper.xml中定義對應的配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.distributedlock.mapper.StockMapper"><select id="selectStockForUpdate" resultType="com.atguigu.distributedlock.pojo.Stock">select * from db_stock where id = #{id} for update</select>
</mapper>

壓力測試

注意:測試之前,需要把庫存量改成5000。壓測數據如下:比jvm性能高很多,比無鎖要低將近1倍

mysql數據庫存:

1.7.3. 樂觀鎖

樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則重試。那么我們如何實現樂觀鎖呢

使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的實現 方式。一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄 的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新。

給db_stock表添加version字段:

對應也需要給Stock實體類添加version屬性。此處略。。。。

代碼實現

public void checkAndLock() {// 先查詢庫存是否充足Stock stock = this.stockMapper.selectById(1L);// 再減庫存if (stock != null && stock.getCount() > 0){// 獲取版本號Long version = stock.getVersion();stock.setCount(stock.getCount() - 1);// 每次更新 版本號 + 1stock.setVersion(stock.getVersion() + 1);// 更新之前先判斷是否是之前查詢的那個版本,如果不是重試if (this.stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version)) == 0) {checkAndLock();}}
}

重啟后使用jmeter壓力測試工具結果如下:

修改測試參數如下:

測試結果如下:

說明樂觀鎖在并發量越大的情況下,性能越低(因為需要大量的重試);并發量越小,性能越高。

1.7.4. mysql鎖總結

性能:一個sql > 悲觀鎖 > jvm鎖 > 樂觀鎖

如果追求極致性能、業務場景簡單并且不需要記錄數據前后變化的情況下。

優先選擇:一個sql

如果寫并發量較低(多讀),爭搶不是很激烈的情況下優先選擇:樂觀鎖

如果寫并發量較高,一般會經常沖突,此時選擇樂觀鎖的話,會導致業務代碼不間斷的重試。

優先選擇:mysql悲觀鎖

不推薦jvm本地鎖。

1.8. redis樂觀鎖

利用redis監聽 + 事務

watch stock
multi
set stock 5000
exec

如果執行過程中stock的值沒有被其他鏈接改變,則執行成功

如果執行過程中stock的值被改變,則執行失敗效果如下:

具體代碼實現,只需要改造對應的service方法:

public void deduct() {this.redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {operations.watch("stock");// 1. 查詢庫存信息Object stock = operations.opsForValue().get("stock");// 2. 判斷庫存是否充足int st = 0;if (stock != null && (st = Integer.parseInt(stock.toString())) > 0) {// 3. 扣減庫存operations.multi();operations.opsForValue().set("stock", String.valueOf(--st));List exec = operations.exec();if (exec == null || exec.size() == 0) {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}deduct();}return exec;}return null;}});
}

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

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

相關文章

python:可迭代的數據類型、可變的數據類型、不可變的數據類型

python&#xff1a;可迭代的數據類型、可變的數據類型、不可變的數據類型 文章目錄 python&#xff1a;可迭代的數據類型、可變的數據類型、不可變的數據類型可迭代的數據類型可變的數據類型不可變的數據類型 可迭代的數據類型 序列&#xff1a;str、bytes、tuple、list非序列…

PC8223(CC/CV控制)高耐壓輸入5V/3.4A同步降壓電路內建補償帶恒流恒壓輸出

概述 PC8233&#xff08;替代CX8853&#xff09;是一款同步降壓調節器,輸出電流高達3.4A,操作范圍從8V到32V的寬電源電壓。內部補償要求最低數量現成的標準外部組件。PC8233在CC&#xff08;恒定輸出電流&#xff09;模式或CV&#xff08;恒定輸出電壓&#xff09;模式&#x…

莫托曼機器人測溫程序

1機器程序 2.1 主程序 MAIN&#xff1a; NOP CALL JOB:ORG *1 JUMP *5 IF IN#(41)OFF CALL JOB:遠程 IF IN#(25)ON CALL JOB:本地 IF IN#(26)ON CALL JOB:測距判斷 CALL JOB:最后一支 *5 CALL JOB:PZ IF IN#(35)ON CALL JOB:PZ IF IN#(65)ON JUMP *1 END 1.2 本地程序 1、本地…

代碼隨想錄算法訓練營Day 60 || 84.柱狀圖中最大的矩形

84.柱狀圖中最大的矩形 力扣題目鏈接(opens new window) 給定 n 個非負整數&#xff0c;用來表示柱狀圖中各個柱子的高度。每個柱子彼此相鄰&#xff0c;且寬度為 1 。 求在該柱狀圖中&#xff0c;能夠勾勒出來的矩形的最大面積。 1 < heights.length <10^50 < hei…

CVE-2022-0543(Redis 沙盒逃逸漏洞)

簡介 CVE-2022-0543是一個與Redis相關的安全漏洞。在Redis中&#xff0c;用戶連接后可以通過eval命令執行Lua腳本&#xff0c;但在沙箱環境中腳本無法執行命令或讀取文件。然而&#xff0c;攻擊者可以利用Lua沙箱中遺留的變量package的loadlib函數來加載動態鏈接庫liblua5.1.s…

VirtualBox下win主機如何訪問linux虛擬機文件夾

目錄 ?編輯 方法1&#xff1a;通過VirtualBox自帶的共享文件夾&#xff08;Win->linux&#xff09; 方法2&#xff1a;通過Samba方法本地網絡訪問(Linux->win) 我使用的VirtualBox版本為7.0.4,主機是Window系統&#xff0c;虛擬機是Linux系統 方法1&#xff1a;通過Vir…

【SpringBoot篇】Spring_Task定時任務框架

文章目錄 &#x1f339;概述&#x1f33a;應用場景&#x1f384;cron表達式&#x1f6f8;入門案例&#x1f38d;實際應用 &#x1f339;概述 Spring Task 是 Spring 框架提供的一種任務調度和異步處理的解決方案。可以按照約定的時間自動執行某個代碼邏輯它可以幫助開發者在 S…

PTA-快速冪

要求實現一個遞歸函數&#xff0c;高效求ab(1≤a,b≤62,ab<263)。 函數接口定義&#xff1a; long long int pow(int a, int b); 其中a 、b 是用戶傳入的參數。 裁判測試程序樣例&#xff1a; #include<iostream> using namespace std; long long int pow(int a,…

數據結構 棧與隊列

棧 棧是一種 后進先出&#xff08; LIFO&#xff09; 的數據結構&#xff0c;它是一種線性的、有序的數據結構。棧的基本操作有兩個&#xff0c;即入棧和出棧。入棧指將元素放入棧頂&#xff0c;出棧指將棧頂元素取出。棧的本質是一個容器&#xff0c;它可以存儲任何類型的數…

String轉Date,Date轉String

源碼&#xff1a; Date currentTime new Date();System.out.println("currentTime:"currentTime);SimpleDateFormat formatter new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String dateString formatter.format(currentTime);System.out.println(&quo…

【深度學習】學習率及多種選擇策略

學習率是最影響性能的超參數之一&#xff0c;如果我們只能調整一個超參數&#xff0c;那么最好的選擇就是它。相比于其它超參數學習率以一種更加復雜的方式控制著模型的有效容量&#xff0c;當學習率最優時&#xff0c;模型的有效容量最大。本文從手動選擇學習率到使用預熱機制…

qt msvc2010 qdatetime.h:122: error: C2589: “(”:“::”右邊的非法標記

報錯內容&#xff1a; C:\Qt\Qt5.4.0\5.4.0\msvc2010_opengl\include\QtCore\qdatetime.h:114: error: C2589: “(”:“::”右邊的非法標記 C:\Qt\Qt5.4.0\5.4.0\msvc2010_opengl\include\QtCore\qdatetime.h:114: error: C2059: 語法錯誤:“::” 解決方法&#xff1a; 打開qd…

2023小紅書Android面試之旅

一面 自我介紹 看你寫了很多文章&#xff0c;拿你理解最深刻的一篇出來講一講 講了Binder相關內容 Binder大概分了幾層 哪些方法調用會涉及到Binder通信 大概講一下startActivity的流程&#xff0c;包括與AMS的交互 全頁面停留時長埋點是怎么做的 我在項目中做過的內容&am…

RocketMQ-NameServer詳解

前言 ? RocketMQ架構上主要分為四部分, Broker、Producer、Consumer、NameServer&#xff0c;其他三個都會與NameServer進行通信。 Producer: ? **消息發布的角色&#xff0c;可集群部署。**通過NameServer集群獲得Topic的路由信息&#xff0c;包括Topic下面有哪些Queue&a…

PTA-病毒感染檢測

人的DNA和病毒DNA均表示成由一些字母組成的字符串序列。然后檢測某種病毒DNA序列是否在患者的DNA序列中出現過&#xff0c;如果出現過&#xff0c;則此人感染了該病毒&#xff0c;否則沒有感染。例如&#xff0c;假設病毒的DNA序列為baa&#xff0c;患者1的DNA序列為aaabbba&am…

數據結構與算法編程題15

設計一個算法&#xff0c;通過遍歷一趟&#xff0c;將鏈表中所有結點的鏈接方向逆轉&#xff0c;仍利用原表的存儲空間。 #include <iostream> using namespace std;typedef int Elemtype; #define ERROR 0; #define OK 1;typedef struct LNode {Elemtype data; …

【從入門到起飛】JavaSE—多線程(3)(生命周期,線程安全問題,同步方法)

&#x1f38a;專欄【JavaSE】 &#x1f354;喜歡的詩句&#xff1a;路漫漫其修遠兮&#xff0c;吾將上下而求索。 &#x1f386;音樂分享【如愿】 &#x1f384;歡迎并且感謝大家指出小吉的問題&#x1f970; 文章目錄 &#x1f354;生命周期&#x1f384;線程的安全問題&#…

【Leetcode合集】1410. HTML 實體解析器

1410. HTML 實體解析器 1410. HTML 實體解析器 代碼倉庫地址&#xff1a; https://github.com/slience-me/Leetcode 個人博客 &#xff1a;https://slienceme.xyz 編寫一個函數來查找字符串數組中的最長公共前綴。 如果不存在公共前綴&#xff0c;返回空字符串 ""…

YOLOv7獨家改進: Inner-IoU基于輔助邊框的IoU損失,高效結合 GIoU, DIoU, CIoU,SIoU 等 | 2023.11

??????本文獨家改進:Inner-IoU引入尺度因子 ratio 控制輔助邊框的尺度大小用于計算損失,并與現有的基于 IoU ( GIoU, DIoU, CIoU,SIoU )損失進行有效結合 推薦指數:5顆星 新穎指數:5顆星 收錄: YOLOv7高階自研專欄介紹: http://t.csdnimg.cn/tYI0c …

開發抖音小游戲什么技術

開發抖音小游戲&#xff0c;使用以下技術可能會相對簡單&#xff1a; HTML5&#xff1a;HTML5 是一種用于創建網頁和應用程序的標準標記語言。它具有豐富的功能和靈活性&#xff0c;可以在各種設備和平臺上運行&#xff0c;包括移動設備和瀏覽器。HTML5 提供了許多游戲開發所需…