看完該文預(yù)計(jì)用時:15分鐘
看之前應(yīng)具體的技術(shù)棧:springboot mysql nginx(了解即可)
目錄
0.寫在前面
1. 從減庫存聊起
1.1. 環(huán)境準(zhǔn)備
??1.2. 簡單實(shí)現(xiàn)減庫存
?1.3. 演示超賣現(xiàn)象
1.4. jvm鎖問題演示?
1.4.2. 原理
1.5. 多服務(wù)問題?
1.5.1. 安裝配置nginx
1.5.2. 壓力測試
?1.6. mysql鎖演示
1.6.1. mysql悲觀鎖
1.6.2. mysql樂觀鎖?
?1.6.3. mysql鎖缺陷
?2. 基于mysql實(shí)現(xiàn)分布式鎖
2.1. 基本思路?
2.2. 代碼實(shí)現(xiàn)
2.3. 缺陷及解決方案?
0.寫在前面
在多線程高并發(fā)場景下,為了保證資源的線程安全問題,jdk為我們提供了synchronized關(guān)鍵字和
ReentrantLock可重入鎖,但是它們只能保證一個jvm內(nèi)的線程安全。在分布式集群、微服務(wù)、云原生橫行的當(dāng)下,如何保證不同進(jìn)程、不同服務(wù)、不同機(jī)器的線程安全問題,jdk并沒有給我們提供既有的解決方案。此時,我們就必須借助于相關(guān)技術(shù)手動實(shí)現(xiàn)了。目前主流的實(shí)現(xiàn)有三種方式:
1. 基于mysql關(guān)系型實(shí)現(xiàn)
2. 基于redis非關(guān)系型數(shù)據(jù)實(shí)現(xiàn)
3. 基于zookeeper實(shí)現(xiàn)
這篇文章主要講解的是基于基于mysql關(guān)系型實(shí)現(xiàn)分布式鎖
1. 從減庫存聊起
庫存在并發(fā)量較大情況下很容易發(fā)生超賣現(xiàn)象,一旦發(fā)生超賣現(xiàn)象,就會出現(xiàn)多成交了訂單而發(fā)不了貨的情況。
場景:
????????商品S庫存余量為5時,用戶A和B同時來購買一個商品S,此時查詢庫存數(shù)都為5,庫存充足則開始減庫存:
用戶A:update db_stock set stock = stock - 1 where id = 1
用戶B:update db_stock set stock = stock - 1 where id = 1
并發(fā)情況下,更新后的結(jié)果可能是4,而實(shí)際的最終庫存量應(yīng)該是3才對
1.1. 環(huán)境準(zhǔn)備
為了模擬具體場景我們需要準(zhǔn)備開發(fā)環(huán)境
首先需要在mysql數(shù)據(jù)庫中準(zhǔn)備一張表:
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=2 DEFAULT CHARSET=utf8;
?表中數(shù)據(jù)如下:
?創(chuàng)建分布式鎖demo工程:
?建立以下工具目錄結(jié)構(gòu):
?pom依賴文件:
<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.Driver
url: jdbc:mysql://172.16.116.100:3306/test
username: root
password: root
DistributedLockApplication啟動類:
@SpringBootApplication
@MapperScan("com.atguigu.distributedlock.mapper")
public class DistributedLockApplication {
? ?public static void main(String[] args) {
? ? ? ?SpringApplication.run(DistributedLockApplication.class, args);
? }
}
Stock實(shí)體類:
@Data
@TableName("db_stock")
public class Stock {
? @TableId
? private Long id;
? private String productCode;
? private String stockCode;
? private Integer count;
}
StockMapper接口:
public interface StockMapper extends BaseMapper<Stock> {
}
??1.2. 簡單實(shí)現(xiàn)減庫存
接下來咱們代碼實(shí)操一下
StockController:
@RestController
public class StockController {
@Autowired
private StockService stockService;
@GetMapping("check/lock")
public String checkAndLock(){
this.stockService.checkAndLock();
return "驗(yàn)庫存并鎖庫存成功!";
}
}
StockService:
@Service
public class StockService {
@Autowired
private 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);
}
}
}
測試:
?
?查看數(shù)據(jù)庫:
在瀏覽器中一個一個訪問時,每訪問一次,庫存量減1,沒有任何問題。
?1.3. 演示超賣現(xiàn)象
接下來咱們使用jmeter壓力測試工具,高并發(fā)下壓測一下,添加線程組:并發(fā)100循環(huán)50次,即5000次請求。
?
?給線程組添加HTTP Request請求:
填寫測試接口路徑如下:
再選擇你想要的測試報(bào)表,例如這里選擇聚合報(bào)告:
啟動測試,查看壓力測試報(bào)告:
測試結(jié)果:請求總數(shù)5000次,平均請求時間202ms,中位數(shù)(50%)請求是在173ms內(nèi)完成的,90%請求是在344ms內(nèi)完成的,最小耗時12ms,最大耗時1125ms,錯誤率0%,每秒鐘平均473.8次。
查看mysql數(shù)據(jù)庫剩余庫存數(shù):還有4870
此時如果還有人來下單,就會出現(xiàn)超賣現(xiàn)象(別人購買成功,而無貨可發(fā))。
1.4. jvm鎖問題演示?
使用jvm鎖(synchronized關(guān)鍵字或者ReetrantLock)試試:
?重啟tomcat服務(wù),再次使用jmeter壓力測試,效果如下:
查看mysql數(shù)據(jù)庫:
?并沒有發(fā)生超賣現(xiàn)象,完美解決。 ?
1.4.2. 原理
添加synchronized關(guān)鍵字之后,StockService就具備了對象鎖,由于添加了獨(dú)占的排他鎖,同一時刻只 有一個請求能夠獲取到鎖,并減庫存。此時,所有請求只會one-by-one執(zhí)行下去,也就不會發(fā)生超賣現(xiàn)象。
1.5. 多服務(wù)問題?
?使用jvm鎖在單工程單服務(wù)情況下確實(shí)沒有問題,但是在集群情況下會怎樣? 接下啟動多個服務(wù)并使用nginx負(fù)載均衡,結(jié)構(gòu)如下:
啟動三個服務(wù)(端口號分別8000 8100 8200),如下:
1.5.1. 安裝配置nginx
基于安裝nginx:
# 拉取鏡像
docker pull nginx:latest
# 創(chuàng)建nginx對應(yīng)資源、日志及配置目錄
mkdir -p /opt/nginx/logs /opt/nginx/conf /opt/nginx/html
# 先在conf目錄下創(chuàng)建nginx.conf文件,配置內(nèi)容參照下方
# 再運(yùn)行容器
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
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服務(wù)器地址
?經(jīng)過測試,通過nginx訪問服務(wù)一切正常。
1.5.2. 壓力測試
?注意:先把數(shù)據(jù)庫庫存量還原到5000。
參照之前的測試用例,再創(chuàng)建一個新的測試組:參數(shù)給之前一樣
配置nginx的地址及 服務(wù)的訪問路徑如下:
?測試結(jié)果:性能只是略有提升。
?數(shù)據(jù)庫庫存剩余量如下:
?又出現(xiàn)了并發(fā)問題,即出現(xiàn)了超賣現(xiàn)象。
?1.6. mysql鎖演示
除了使用jvm鎖之外,還可以使用數(shù)據(jù)鎖:悲觀鎖 或者 樂觀鎖
悲觀鎖:在讀取數(shù)據(jù)時鎖住那幾行,其他對這幾行的更新需要等到悲觀鎖結(jié)束時才能繼續(xù) 。 樂觀所:讀取數(shù)據(jù)時不鎖,更新時檢查是否數(shù)據(jù)已經(jīng)被更新過,如果是則取消當(dāng)前更新,一般在悲觀鎖 的等待時間過長而不能接受時我們才會選擇樂觀鎖。
1.6.1. mysql悲觀鎖
在MySQL的InnoDB中,預(yù)設(shè)的Tansaction isolation level 為REPEATABLE READ(可重讀)
在SELECT 的讀取鎖定主要分為兩種方式:
- SELECT ... LOCK IN SHARE MODE (共享鎖)
- SELECT ... FOR UPDATE (悲觀鎖)
這兩種方式在事務(wù)(Transaction) 進(jìn)行當(dāng)中SELECT 到同一個數(shù)據(jù)表時,都必須等待其它事務(wù)數(shù)據(jù)被提交(Commit)后才會執(zhí)行。 而主要的不同在于LOCK IN SHARE MODE 在有一方事務(wù)要Update 同一個表單時很容易造成死鎖。簡單的說,如果SELECT 后面若要UPDATE 同一個表單,最好使用SELECT ... FOR UPDATE。
代碼實(shí)現(xiàn)改造StockService:
在StockeMapper中定義selectStockForUpdate方法:
public interface StockMapper extends BaseMapper<Stock> {
? ?public Stock selectStockForUpdate(Long id);
}
在StockMapper.xml中定義對應(yīng)的配置: ?
<?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。壓測數(shù)據(jù)如下:比jvm性能高很多,比無鎖要低將近1倍
mysql數(shù)據(jù)庫存:
1.6.2. mysql樂觀鎖?
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設(shè)認(rèn)為數(shù)據(jù)一般情況下不會造成沖突,所 以在數(shù)據(jù)進(jìn)行提交更新的時候,才會正式對數(shù)據(jù)的沖突與否進(jìn)行檢測,如果發(fā)現(xiàn)沖突了,則重試。那么 我們?nèi)绾螌?shí)現(xiàn)樂觀鎖呢?
使用數(shù)據(jù)版本(Version)記錄機(jī)制實(shí)現(xiàn),這是樂觀鎖最常用的實(shí)現(xiàn) 方式。一般是通過為數(shù)據(jù)庫表增加 一個數(shù)字類型的 “version” 字段來實(shí)現(xiàn)。當(dāng)讀取數(shù)據(jù)時,將version字段的值一同讀出,數(shù)據(jù)每更新一 次,對此version值加一。當(dāng)我們提交更新的時候,判斷數(shù)據(jù)庫表對應(yīng)記錄 的當(dāng)前版本信息與第一次取 出來的version值進(jìn)行比對,如果數(shù)據(jù)庫表當(dāng)前版本號與第一次取出來的version值相等,則予以更新。
給db_stock表添加version字段:
?對應(yīng)也需要給Stock實(shí)體類添加version屬性。此處略。
代碼實(shí)現(xiàn)
public void checkAndLock() {
? ?// 先查詢庫存是否充足
? ?Stock stock = this.stockMapper.selectById(1L);
? ?// 再減庫存
? ?if (stock != null && stock.getCount() > 0){
? ? ? ?// 獲取版本號
? ? ? ?Long version = stock.getVersion();
? ? ? ?stock.setCount(stock.getCount() - 1);
? ? ? ?// 每次更新 版本號 + 1
? ? ? ?stock.setVersion(stock.getVersion() + 1);
? ? ? ?// 更新之前先判斷是否是之前查詢的那個版本,如果不是重試
? ? ? ?if (this.stockMapper.update(stock, new UpdateWrapper<Stock>
().eq("id", stock.getId()).eq("version", version)) == 0) {
? ? ? ? ? ?checkAndLock();
? ? ? }
? }
}
?重啟后使用jmeter壓力測試工具結(jié)果如下:
修改測試參數(shù)如下:
?測試結(jié)果如下:
說明樂觀鎖在并發(fā)量越大的情況下,性能越低(因?yàn)樾枰罅康闹卦嚕?;并發(fā)量越小,性能越高。
?1.6.3. mysql鎖缺陷
在數(shù)據(jù)庫集群情況下會導(dǎo)致數(shù)據(jù)庫鎖失效,并且很多數(shù)據(jù)庫集群的中間件壓根就不支持悲觀鎖。例如:mycat,在讀寫分離的場景下可能會導(dǎo)致樂觀鎖不可靠。 這把鎖強(qiáng)依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個單點(diǎn),一旦數(shù)據(jù)庫掛掉,會導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
?2. 基于mysql實(shí)現(xiàn)分布式鎖
?不管是jvm鎖還是mysql鎖,為了保證線程的并發(fā)安全,都提供了悲觀獨(dú)占排他鎖。所以獨(dú)占排他也是 分布式鎖的基本要求。 可以利用唯一鍵索引不能重復(fù)插入的特點(diǎn)實(shí)現(xiàn)。設(shè)計(jì)表如下:
CREATE TABLE `db_lock` (
?`id` bigint(20) NOT NULL AUTO_INCREMENT,
?`lock_name` varchar(50) NOT NULL COMMENT '鎖名',
?`class_name` varchar(100) DEFAULT NULL COMMENT '類名',
?`method_name` varchar(50) DEFAULT NULL COMMENT '方法名',
?`server_name` varchar(50) DEFAULT NULL COMMENT '服務(wù)器ip',
?`thread_name` varchar(50) DEFAULT NULL COMMENT '線程名',
?`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
COMMENT '獲取鎖時間',
?`desc` varchar(100) DEFAULT NULL COMMENT '描述',
?PRIMARY KEY (`id`),
?UNIQUE KEY `idx_unique` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;
Lock實(shí)體類: ?
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("db_lock")
public class Lock {
? ?private Long id;
? ?private String lockName;
? ?private String className;
? ?private String methodName;
? ?private String serverName;
? ?private String threadName;
? ?private Date createTime;
? ?private String desc;
}
LockMapper接口:
public interface LockMapper extends BaseMapper<Lock> {
}
2.1. 基本思路?
synchronized關(guān)鍵字和ReetrantLock鎖都是獨(dú)占排他鎖,即多個線程爭搶一個資源時,同一時刻只有 一個線程可以搶占該資源,其他線程只能阻塞等待,直到占有資源的線程釋放該資源。
- 線程同時獲取鎖(insert)
- 獲取成功,執(zhí)行業(yè)務(wù)邏輯,執(zhí)行完成釋放鎖(delete)
- 其他線程等待重試
2.2. 代碼實(shí)現(xiàn)
改造StockService:
@Service
public class StockService {
? ?@Autowired
? ?private StockMapper stockMapper;
? ?@Autowired
? ?private LockMapper lockMapper;
? ?/**
? ? * 數(shù)據(jù)庫分布式鎖
? ? */
? ?public void checkAndLock() {
? ? ? ?// 加鎖
? ? ? ?Lock lock = new Lock(null, "lock", this.getClass().getName(), new
Date(), null);
? ? ? ?try {
? ? ? ? ? ?this.lockMapper.insert(lock);
? ? ? } catch (Exception ex) {
? ? ? ? ? ?// 獲取鎖失敗,則重試
? ? ? ? ? ?try {
? ? ? ? ? ? ? ?Thread.sleep(50);
? ? ? ? ? ? ? ?this.checkAndLock();
? ? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? ? ?e.printStackTrace();
? ? ? ? ? }
? ? ? }
? ? ? ?// 先查詢庫存是否充足
? ? ? ?Stock stock = this.stockMapper.selectById(1L);
? ? ? ?// 再減庫存
? ? ? ?if (stock != null && stock.getCount() > 0){
? ? ? ? ? ?stock.setCount(stock.getCount() - 1);
? ? ? ? ? ?this.stockMapper.updateById(stock);
? ? ? }
? ? ? ?// 釋放鎖
? ? ? ?this.lockMapper.deleteById(lock.getId());
? }
}
加鎖: ?
// 加鎖
Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
try {
? ?this.lockMapper.insert(lock);
} catch (Exception ex) {
? ?// 獲取鎖失敗,則重試
? ?try {
? ? ? ?Thread.sleep(50);
? ? ? ?this.checkAndLock();
? } catch (InterruptedException e) {
? ? ? ?e.printStackTrace();
? }
}
解鎖:
// 釋放鎖
this.lockMapper.deleteById(lock.getId());
使用Jmeter壓力測試結(jié)果:
?可以看到性能感人。mysql數(shù)據(jù)庫庫存余量為0,可以保證線程安全。?
2.3. 缺陷及解決方案?
1. 這把鎖強(qiáng)依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個單點(diǎn),一旦數(shù)據(jù)庫掛掉,會導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
解決方案:給鎖數(shù)據(jù)庫 搭建主備
2. 這把鎖沒有失效時間,一旦解鎖操作失敗,就會導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖。
解決方案:只要做一個定時任務(wù),每隔一定時間把數(shù)據(jù)庫中的超時數(shù)據(jù)清理一遍。
3. 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
解決方案:記錄獲取鎖的主機(jī)信息和線程信息,如果相同線程要獲取鎖,直接重入。
4. 受制于數(shù)據(jù)庫性能,并發(fā)能力有限。文章來源:http://www.zghlxwxcb.cn/news/detail-446493.html
解決方案:無法解決。文章來源地址http://www.zghlxwxcb.cn/news/detail-446493.html
到了這里,關(guān)于?【五一創(chuàng)作】基于mysql關(guān)系型實(shí)現(xiàn)分布式鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!