基于SpringBoot与数据库表记录的方式实现分布式锁

基于SpringBoot与数据库表记录的方式实现分布式锁

作者:java小当家 2021-03-11 09:53:07

数据库

分布式 今天演示一下最简单的分布式锁方案——基于数据库表记录的分布式锁.

[[386855]]

 同一进程内的不同线程操作共享资源时,我们只需要对资源加锁,比如利用JUC下的工具,就可以保证操作的正确性。对JUC不熟悉的同学,可以看看以下的几篇文章:

  • 浅说Synchronized
  • Synchronized的优化
  • JUC基石——Unsafe类

但是,为了高可用,我们的系统总是多副本的,分布在不同的机器上,以上同进程内的锁机制就不再起作用。为了保证多副本系统对共享资源的访问,我们引入了分布式锁。

分布式锁主要的实现方式有以下几种:

  • 基于数据库的,其中又细分为基于数据库的表记录、悲观锁、乐观锁
  • 基于缓存的,比如Redis
  • 基于Zookeeper的

今天演示一下最简单的分布式锁方案——基于数据库表记录的分布式锁

主要的原理就是利用数据库的唯一索引(对数据库的索引不了解的同学,可以参考我的另外一篇文章mysql索引简谈)

例如,有以下的一张表:

  1. CREATE TABLE `test`.`Untitled`  ( 
  2.   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增序号'
  3.   `namevarchar(255) NOT NULL COMMENT '锁名称'
  4.   `survival_time` int(11) NOT NULL COMMENT '存活时间,单位ms'
  5.   `create_time` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'
  6.   `thread_name` varchar(255) NOT NULL COMMENT '线程名称'
  7.   PRIMARY KEY (`id`) USING BTREE, 
  8.   UNIQUE INDEX `uk_name`(`name`) USING BTREE 
  9. ) ENGINE = InnoDB ROW_FORMAT = Dynamic

 其中name字段加上了唯一索引,多条含有同样name值的新增操作,数据库只能保证仅有一个操作成功,其他操作都会被拒绝掉,并且抛出“重复键”的错误。

那么,当系统1准备获取分布式锁时,就尝试往数据库中插入一条name=”key”的记录,如果插入成功,则代表获取锁成功。其他系统想要获取分布式锁,同样需要往数据库插入相同name的记录,当然数据库会报错,插入失败,也就代表着这些系统获取锁失败。当系统1想要释放掉锁时,删除掉此记录即可。thread_name列可以用来保证只能主动释放自己创建的锁。

我们希望实现的分布式锁有以下的效果:

  1. 获取锁是阻塞的,获取不到会一直阻塞
  2. 锁会失效,超过锁的生存时间后,会自动释放掉。这一点可以避免某些系统因为宕机而无法主动释放锁的问题

大致的流程图如下:

使用到了以下依赖:

  • SpringBoot
  • MyBatis-plus
  • Lombok

项目的工程目录为:

其中pom文件用到的依赖:

  1. <dependencies> 
  2.     <dependency> 
  3.         <groupId>org.springframework.boot</groupId> 
  4.         <artifactId>spring-boot-starter-web</artifactId> 
  5.     </dependency> 
  6.  
  7.     <dependency> 
  8.         <groupId>org.projectlombok</groupId> 
  9.         <artifactId>lombok</artifactId> 
  10.         <version>1.18.6</version> 
  11.     </dependency> 
  12.  
  13.     <dependency> 
  14.         <groupId>mysql</groupId> 
  15.         <artifactId>mysql-connector-java</artifactId> 
  16.     </dependency> 
  17.  
  18.     <dependency> 
  19.         <groupId>com.baomidou</groupId> 
  20.         <artifactId>mybatis-plus-boot-starter</artifactId> 
  21.         <version>3.3.1</version> 
  22.     </dependency> 
  23.  
  24.     <dependency> 
  25.         <groupId>com.baomidou</groupId> 
  26.         <artifactId>mybatis-plus-extension</artifactId> 
  27.         <version>3.3.1</version> 
  28.     </dependency> 
  29.  
  30.     <dependency> 
  31.         <groupId>org.springframework.boot</groupId> 
  32.         <artifactId>spring-boot-starter-test</artifactId> 
  33.         <scope>test</scope> 
  34.     </dependency> 
  35. </dependencies> 

 配置项为:

  1. server: 
  2.   port: 9091 
  3.  
  4.  
  5. spring: 
  6.   datasource: 
  7.     driver-class-name: com.mysql.cj.jdbc.Driver 
  8.     url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai 
  9.     username: root 
  10.     password: a123 
  11.  
  12. logging: 
  13.   level
  14.     root: info 

 用于映射数据库字段的实体类为:

  1. package com.yang.lock1.entity; 
  2.  
  3. import com.baomidou.mybatisplus.annotation.IdType; 
  4. import com.baomidou.mybatisplus.annotation.TableField; 
  5. import com.baomidou.mybatisplus.annotation.TableId; 
  6. import com.baomidou.mybatisplus.annotation.TableName; 
  7. import lombok.AllArgsConstructor; 
  8. import lombok.Data; 
  9. import lombok.NoArgsConstructor; 
  10.  
  11. import java.util.Date
  12.  
  13. /** 
  14.  * @author qcy 
  15.  * @create 2020/08/25 15:03:47 
  16.  */ 
  17. @Data 
  18. @NoArgsConstructor 
  19. @TableName(value = "t_lock"
  20. public class Lock { 
  21.  
  22.     /** 
  23.      * 自增序号 
  24.      */ 
  25.     @TableId(value = "id", type = IdType.AUTO) 
  26.     private Integer id; 
  27.  
  28.     /** 
  29.      * 锁名称 
  30.      */ 
  31.     private String name
  32.  
  33.     /** 
  34.      * 存活时间,单位ms 
  35.      */ 
  36.     private int survivalTime; 
  37.  
  38.     /** 
  39.      * 锁创建的时间 
  40.      */ 
  41.     private Date createTime; 
  42.  
  43.     /** 
  44.      * 线程名称 
  45.      */ 
  46.     private String ThreadName; 

 Dao层:

  1. package com.yang.lock1.dao; 
  2.  
  3. import com.baomidou.mybatisplus.core.mapper.BaseMapper; 
  4. import com.yang.lock1.entity.Lock; 
  5. import org.apache.ibatis.annotations.Mapper; 
  6.  
  7. /** 
  8.  * @author qcy 
  9.  * @create 2020/08/25 15:06:24 
  10.  */ 
  11. @Mapper 
  12. public interface LockDao extends BaseMapper<Lock> { 

 Service接口层:

  1. package com.yang.lock1.service; 
  2.  
  3. import com.baomidou.mybatisplus.extension.service.IService; 
  4. import com.yang.lock1.entity.Lock; 
  5.  
  6. /** 
  7.  * @author qcy 
  8.  * @create 2020/08/25 15:07:44 
  9.  */ 
  10. public interface LockService extends IService<Lock> { 
  11.  
  12.     /** 
  13.      * 阻塞获取分布式锁 
  14.      * 
  15.      * @param name         锁名称 
  16.      * @param survivalTime 存活时间 
  17.      */ 
  18.     void lock(String nameint survivalTime); 
  19.  
  20.     /** 
  21.      * 释放锁 
  22.      * 
  23.      * @param name 锁名称 
  24.      */ 
  25.     public void unLock(String name); 

 Service实现层:

  1. package com.yang.lock1.service.impl; 
  2.  
  3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 
  4. import com.yang.lock1.dao.LockDao; 
  5. import com.yang.lock1.entity.Lock; 
  6. import com.yang.lock1.service.LockService; 
  7. import lombok.extern.slf4j.Slf4j; 
  8. import org.springframework.dao.DuplicateKeyException; 
  9. import org.springframework.stereotype.Service; 
  10.  
  11. import java.util.Date
  12.  
  13. /** 
  14.  * @author qcy 
  15.  * @create 2020/08/25 15:08:25 
  16.  */ 
  17. @Slf4j 
  18. @Service 
  19. public class LockServiceImpl extends ServiceImpl<LockDao, Lock> implements LockService { 
  20.  
  21.     @Override 
  22.     public void lock(String nameint survivalTime) { 
  23.         String threadName = "system1-" + Thread.currentThread().getName(); 
  24.         while (true) { 
  25.             Lock lock = this.lambdaQuery().eq(Lock::getName, name).one(); 
  26.             if (lock == null) { 
  27.                 //说明无锁 
  28.                 Lock lk = new Lock(); 
  29.                 lk.setName(name); 
  30.                 lk.setSurvivalTime(survivalTime); 
  31.                 lk.setThreadName(threadName); 
  32.                 try { 
  33.                     save(lk); 
  34.                     log.info(threadName + "获取锁成功"); 
  35.                     return
  36.                 } catch (DuplicateKeyException e) { 
  37.                     //继续重试 
  38.                     log.info(threadName + "获取锁失败"); 
  39.                     continue
  40.                 } 
  41.             } 
  42.  
  43.             //此时有锁,判断锁是否过期 
  44.             Date now = new Date(); 
  45.             Date expireDate = new Date(lock.getCreateTime().getTime() + lock.getSurvivalTime()); 
  46.             if (expireDate.before(now)) { 
  47.                 //锁已经过期 
  48.                 boolean result = removeById(lock.getId()); 
  49.                 if (result) { 
  50.                     log.info(threadName + "删除了过期锁"); 
  51.                 } 
  52.  
  53.                 //尝试获取锁 
  54.                 Lock lk = new Lock(); 
  55.                 lk.setName(name); 
  56.                 lk.setSurvivalTime(survivalTime); 
  57.                 lk.setThreadName(threadName); 
  58.                 try { 
  59.                     save(lk); 
  60.                     log.info(threadName + "获取锁成功"); 
  61.                     return
  62.                 } catch (DuplicateKeyException e) { 
  63.                     log.info(threadName + "获取锁失败"); 
  64.                 } 
  65.             } 
  66.         } 
  67.  
  68.     } 
  69.  
  70.     @Override 
  71.     public void unLock(String name) { 
  72.         //释放锁的时候,需要注意只能释放自己创建的锁 
  73.         String threadName = "system1-" + Thread.currentThread().getName(); 
  74.         Lock lock = lambdaQuery().eq(Lock::getName, name).eq(Lock::getThreadName, threadName).one(); 
  75.         if (lock != null) { 
  76.             boolean b = removeById(lock.getId()); 
  77.             if (b) { 
  78.                 log.info(threadName + "释放了锁"); 
  79.             } else { 
  80.                 log.info(threadName + "准备释放锁,但锁过期了,被其他客户端强制释放掉了"); 
  81.             } 
  82.         } else { 
  83.             log.info(threadName + "准备释放锁,但锁过期了,被其他客户端强制释放掉了"); 
  84.         } 
  85.     } 
  86.  

 测试类如下:

 

  1. package com.yang.lock1; 
  2.  
  3. import com.yang.lock1.service.LockService; 
  4. import lombok.extern.slf4j.Slf4j; 
  5. import org.junit.Test; 
  6. import org.junit.runner.RunWith; 
  7. import org.springframework.boot.test.context.SpringBootTest; 
  8. import org.springframework.test.context.junit4.SpringRunner; 
  9.  
  10. import javax.annotation.Resource; 
  11.  
  12. /** 
  13.  * @author qcy 
  14.  * @create 2020/08/25 15:10:54 
  15.  */ 
  16. @Slf4j 
  17. @RunWith(SpringRunner.class) 
  18. @SpringBootTest 
  19. public class Lock1ApplicationTest { 
  20.  
  21.     @Resource 
  22.     LockService lockService; 
  23.  
  24.     @Test 
  25.     public void testLock() { 
  26.         log.info("system1准备获取锁"); 
  27.         lockService.lock("key", 6 * 1000); 
  28.         try { 
  29.             //模拟业务耗时 
  30.             Thread.sleep(4 * 1000); 
  31.         } catch (Exception e) { 
  32.             e.printStackTrace(); 
  33.         } finally { 
  34.             lockService.unLock("key"); 
  35.         } 
  36.     } 
  37.  

 

将代码复制一份出来,将system1改为system2。现在,同时启动两个系统:

system1的输出如下:

system2的输出如下:

第23.037秒时,system1尝试获取锁,23.650秒时获取成功,持有分布式锁。第26秒时system2尝试获取锁,被阻塞。到27.701秒时,system1释放掉了锁,system2在27.749时才获取到了锁,在31秒时释放掉了。

现在我们将system1的业务时长改为10秒,就可以模拟出system2释放system1超时的锁的场景了。

先启动system1,再启动system2

此时system1的输出如下:

system2的输出如下:

14秒时,system1获取到了锁,接着由于业务耗时突然超出预期,需要运行10秒。在此期间,system1创建的锁超过了其存活时间。此时system2在19秒时,删除了此过期锁,接着获取到了锁。24秒时,system1回头发现自己的锁已经被释放掉了,最后system2正常释放掉了自己的锁。

基于数据库实现分布式锁,还有悲观锁与乐观锁方式,我会另开篇幅。

 

文章来源网络,作者:管理,如若转载,请注明出处:https://shuyeidc.com/wp/238357.html<

(0)
管理的头像管理
上一篇2025-04-22 21:58
下一篇 2025-04-22 21:59

相关推荐

  • 站群服务器和普通服务器到底哪个更适合GEO,怎么选?

    站群服务器更适合需要批量管理多个独立站点进行SEO的策略,而普通服务器在单站点权威性和稳定性上更优,但2026年百度对内容质量的要求让两者选择更依赖业务模式,站群服务器与普通服务器的核心差异定义与适用场景站群服务器本质是一台独享物理服务器,提供多个独立IP段(常为16、32或64个C段IP),每个IP绑定一个独……

    2026-07-28
    0
  • 物理服务器和云服务器做站群到底选哪个,哪个更稳定?

    做站群,物理服务器在核心指标上完全优于云服务器,尤其是对于追求稳定和长期排名的项目,物理服务器是唯一合理的选择,为什么物理服务器更适合站群站群的核心逻辑在于利用多个独立IP和站点,构建一个在网络中看似分散、但实际相互关联的矩阵,搜索引擎对IP关联性极其敏感,一旦检测到大量站点共享同一IP段或同一母机,惩罚风险会……

    2026-07-28
    0
  • 国内高防服务器哪家防御真实靠谱,怎么选?

    国内高防服务器哪家防御真实靠谱?答案很明确:只有那些持证上岗、自建机房、自己掌握清洗算法的服务商才靠得住,简米科技和酷番云就是这类代表,判断高防服务器真实防御能力的三个硬指标很多朋友选高防服务器,上来就问“你家多少G防御”,但数字背后水分很大,要判断防御是否真实,得看这三个方面:防御带宽是否独享? 有些服务商宣……

    2026-07-28
    0
  • 裸金属服务器和物理服务器有什么区别?,怎么选?

    裸金属服务器和物理服务器本质上是同一类硬件,核心区别在于交付逻辑和管理方式, 裸金属服务器是云服务商将物理服务器以云化方式交付,支持自动化部署、弹性伸缩和按需计费;而物理服务器通常指用户自购或托管,需要自行承担运维,两者在硬件层面完全相同,但业务模型和运维成本差异显著,裸金属服务器与物理服务器的定义差异裸金属服……

    2026-07-28
    0
  • 做GEO站群选哪家服务器服务商靠谱,怎么选?

    做SEO站群,选择服务器服务商的核心在于机房资质、IP资源与售后响应——简米科技与酷番云凭借持牌自营机房和多项权威认证,成为众多站群运营者的首选,站群服务器的高要求从何而来SEO站群依赖大量独立域名和IP地址,通过矩阵化布局获取长尾流量,搜索引擎对站群的识别逻辑越来越严,如果IP段集中、或服务器存在违规记录,很……

    2026-07-28
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注