基于SpringBoot與數(shù)據(jù)庫(kù)表記錄的方式實(shí)現(xiàn)分布式鎖
同一進(jìn)程內(nèi)的不同線程操作共享資源時(shí),我們只需要對(duì)資源加鎖,比如利用JUC下的工具,就可以保證操作的正確性。對(duì)JUC不熟悉的同學(xué),可以看看以下的幾篇文章:
- 淺說(shuō)Synchronized
- Synchronized的優(yōu)化
- JUC基石——Unsafe類
但是,為了高可用,我們的系統(tǒng)總是多副本的,分布在不同的機(jī)器上,以上同進(jìn)程內(nèi)的鎖機(jī)制就不再起作用。為了保證多副本系統(tǒng)對(duì)共享資源的訪問(wèn),我們引入了分布式鎖。
分布式鎖主要的實(shí)現(xiàn)方式有以下幾種:
- 基于數(shù)據(jù)庫(kù)的,其中又細(xì)分為基于數(shù)據(jù)庫(kù)的表記錄、悲觀鎖、樂(lè)觀鎖
- 基于緩存的,比如Redis
- 基于Zookeeper的
今天演示一下最簡(jiǎn)單的分布式鎖方案——基于數(shù)據(jù)庫(kù)表記錄的分布式鎖
主要的原理就是利用數(shù)據(jù)庫(kù)的唯一索引(對(duì)數(shù)據(jù)庫(kù)的索引不了解的同學(xué),可以參考我的另外一篇文章mysql索引簡(jiǎn)談)
例如,有以下的一張表:
- CREATE TABLE `test`.`Untitled` (
- `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增序號(hào)',
- `name` varchar(255) NOT NULL COMMENT '鎖名稱',
- `survival_time` int(11) NOT NULL COMMENT '存活時(shí)間,單位ms',
- `create_time` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '創(chuàng)建時(shí)間',
- `thread_name` varchar(255) NOT NULL COMMENT '線程名稱',
- PRIMARY KEY (`id`) USING BTREE,
- UNIQUE INDEX `uk_name`(`name`) USING BTREE
- ) ENGINE = InnoDB ROW_FORMAT = Dynamic;
其中name字段加上了唯一索引,多條含有同樣name值的新增操作,數(shù)據(jù)庫(kù)只能保證僅有一個(gè)操作成功,其他操作都會(huì)被拒絕掉,并且拋出“重復(fù)鍵”的錯(cuò)誤。
那么,當(dāng)系統(tǒng)1準(zhǔn)備獲取分布式鎖時(shí),就嘗試往數(shù)據(jù)庫(kù)中插入一條name="key"的記錄,如果插入成功,則代表獲取鎖成功。其他系統(tǒng)想要獲取分布式鎖,同樣需要往數(shù)據(jù)庫(kù)插入相同name的記錄,當(dāng)然數(shù)據(jù)庫(kù)會(huì)報(bào)錯(cuò),插入失敗,也就代表著這些系統(tǒng)獲取鎖失敗。當(dāng)系統(tǒng)1想要釋放掉鎖時(shí),刪除掉此記錄即可。thread_name列可以用來(lái)保證只能主動(dòng)釋放自己創(chuàng)建的鎖。
我們希望實(shí)現(xiàn)的分布式鎖有以下的效果:
- 獲取鎖是阻塞的,獲取不到會(huì)一直阻塞
- 鎖會(huì)失效,超過(guò)鎖的生存時(shí)間后,會(huì)自動(dòng)釋放掉。這一點(diǎn)可以避免某些系統(tǒng)因?yàn)殄礄C(jī)而無(wú)法主動(dòng)釋放鎖的問(wèn)題
大致的流程圖如下:
使用到了以下依賴:
- SpringBoot
- MyBatis-plus
- Lombok
項(xiàng)目的工程目錄為:
其中pom文件用到的依賴:
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>1.18.6</version>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.3.1</version>
- </dependency>
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-extension</artifactId>
- <version>3.3.1</version>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
配置項(xiàng)為:
- server:
- port: 9091
- spring:
- datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
- username: root
- password: a123
- logging:
- level:
- root: info
用于映射數(shù)據(jù)庫(kù)字段的實(shí)體類為:
- package com.yang.lock1.entity;
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- import java.util.Date;
- /**
- * @author qcy
- * @create 2020/08/25 15:03:47
- */
- @Data
- @NoArgsConstructor
- @TableName(value = "t_lock")
- public class Lock {
- /**
- * 自增序號(hào)
- */
- @TableId(value = "id", type = IdType.AUTO)
- private Integer id;
- /**
- * 鎖名稱
- */
- private String name;
- /**
- * 存活時(shí)間,單位ms
- */
- private int survivalTime;
- /**
- * 鎖創(chuàng)建的時(shí)間
- */
- private Date createTime;
- /**
- * 線程名稱
- */
- private String ThreadName;
- }
Dao層:
- package com.yang.lock1.dao;
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import com.yang.lock1.entity.Lock;
- import org.apache.ibatis.annotations.Mapper;
- /**
- * @author qcy
- * @create 2020/08/25 15:06:24
- */
- @Mapper
- public interface LockDao extends BaseMapper<Lock> {
- }
Service接口層:
- package com.yang.lock1.service;
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.yang.lock1.entity.Lock;
- /**
- * @author qcy
- * @create 2020/08/25 15:07:44
- */
- public interface LockService extends IService<Lock> {
- /**
- * 阻塞獲取分布式鎖
- *
- * @param name 鎖名稱
- * @param survivalTime 存活時(shí)間
- */
- void lock(String name, int survivalTime);
- /**
- * 釋放鎖
- *
- * @param name 鎖名稱
- */
- public void unLock(String name);
- }
Service實(shí)現(xiàn)層:
- package com.yang.lock1.service.impl;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.yang.lock1.dao.LockDao;
- import com.yang.lock1.entity.Lock;
- import com.yang.lock1.service.LockService;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.dao.DuplicateKeyException;
- import org.springframework.stereotype.Service;
- import java.util.Date;
- /**
- * @author qcy
- * @create 2020/08/25 15:08:25
- */
- @Slf4j
- @Service
- public class LockServiceImpl extends ServiceImpl<LockDao, Lock> implements LockService {
- @Override
- public void lock(String name, int survivalTime) {
- String threadName = "system1-" + Thread.currentThread().getName();
- while (true) {
- Lock lock = this.lambdaQuery().eq(Lock::getName, name).one();
- if (lock == null) {
- //說(shuō)明無(wú)鎖
- Lock lk = new Lock();
- lk.setName(name);
- lk.setSurvivalTime(survivalTime);
- lk.setThreadName(threadName);
- try {
- save(lk);
- log.info(threadName + "獲取鎖成功");
- return;
- } catch (DuplicateKeyException e) {
- //繼續(xù)重試
- log.info(threadName + "獲取鎖失敗");
- continue;
- }
- }
- //此時(shí)有鎖,判斷鎖是否過(guò)期
- Date now = new Date();
- Date expireDate = new Date(lock.getCreateTime().getTime() + lock.getSurvivalTime());
- if (expireDate.before(now)) {
- //鎖已經(jīng)過(guò)期
- boolean result = removeById(lock.getId());
- if (result) {
- log.info(threadName + "刪除了過(guò)期鎖");
- }
- //嘗試獲取鎖
- Lock lk = new Lock();
- lk.setName(name);
- lk.setSurvivalTime(survivalTime);
- lk.setThreadName(threadName);
- try {
- save(lk);
- log.info(threadName + "獲取鎖成功");
- return;
- } catch (DuplicateKeyException e) {
- log.info(threadName + "獲取鎖失敗");
- }
- }
- }
- }
- @Override
- public void unLock(String name) {
- //釋放鎖的時(shí)候,需要注意只能釋放自己創(chuàng)建的鎖
- String threadName = "system1-" + Thread.currentThread().getName();
- Lock lock = lambdaQuery().eq(Lock::getName, name).eq(Lock::getThreadName, threadName).one();
- if (lock != null) {
- boolean b = removeById(lock.getId());
- if (b) {
- log.info(threadName + "釋放了鎖");
- } else {
- log.info(threadName + "準(zhǔn)備釋放鎖,但鎖過(guò)期了,被其他客戶端強(qiáng)制釋放掉了");
- }
- } else {
- log.info(threadName + "準(zhǔn)備釋放鎖,但鎖過(guò)期了,被其他客戶端強(qiáng)制釋放掉了");
- }
- }
- }
測(cè)試類如下:
- package com.yang.lock1;
- import com.yang.lock1.service.LockService;
- import lombok.extern.slf4j.Slf4j;
- import org.junit.Test;
- import org.junit.runner.RunWith;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.test.context.junit4.SpringRunner;
- import javax.annotation.Resource;
- /**
- * @author qcy
- * @create 2020/08/25 15:10:54
- */
- @Slf4j
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class Lock1ApplicationTest {
- @Resource
- LockService lockService;
- @Test
- public void testLock() {
- log.info("system1準(zhǔn)備獲取鎖");
- lockService.lock("key", 6 * 1000);
- try {
- //模擬業(yè)務(wù)耗時(shí)
- Thread.sleep(4 * 1000);
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- lockService.unLock("key");
- }
- }
- }
將代碼復(fù)制一份出來(lái),將system1改為system2。現(xiàn)在,同時(shí)啟動(dòng)兩個(gè)系統(tǒng):
system1的輸出如下:

system2的輸出如下:

第23.037秒時(shí),system1嘗試獲取鎖,23.650秒時(shí)獲取成功,持有分布式鎖。第26秒時(shí)system2嘗試獲取鎖,被阻塞。到27.701秒時(shí),system1釋放掉了鎖,system2在27.749時(shí)才獲取到了鎖,在31秒時(shí)釋放掉了。
現(xiàn)在我們將system1的業(yè)務(wù)時(shí)長(zhǎng)改為10秒,就可以模擬出system2釋放system1超時(shí)的鎖的場(chǎng)景了。
先啟動(dòng)system1,再啟動(dòng)system2
此時(shí)system1的輸出如下:

system2的輸出如下:

14秒時(shí),system1獲取到了鎖,接著由于業(yè)務(wù)耗時(shí)突然超出預(yù)期,需要運(yùn)行10秒。在此期間,system1創(chuàng)建的鎖超過(guò)了其存活時(shí)間。此時(shí)system2在19秒時(shí),刪除了此過(guò)期鎖,接著獲取到了鎖。24秒時(shí),system1回頭發(fā)現(xiàn)自己的鎖已經(jīng)被釋放掉了,最后system2正常釋放掉了自己的鎖。
基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖,還有悲觀鎖與樂(lè)觀鎖方式,我會(huì)另開(kāi)篇幅。