基于MySQL数据库排它锁(for update)实现的分布式锁

您所在的位置:网站首页 数据库排他锁与共享锁的区别 基于MySQL数据库排它锁(for update)实现的分布式锁

基于MySQL数据库排它锁(for update)实现的分布式锁

2024-07-14 22:46| 来源: 网络整理| 查看: 265

1、锁的概述

  锁是Java开发中一个非常重要的知识点。锁(lock)或互斥(mutex)是一种同步机制,用于在多线程环境中控制各线程对资源的访问权限。锁旨在强制实施互斥排他、并发控制策略。

1.1、单体应用锁

  JDK中的锁只能在一个JVM进程内有效,我们把这种锁叫做单体应用锁。在JAVA中常见的锁有:synchronized、ReentrantLock、ReadWriteLock等。

1.2、单体应用锁的局限性

  单体应用锁,在传统的单应用服务中是没有问题的,但是在现在集群高并发的场景下,就会出现问题,如下图所示: 在这里插入图片描述   在上图中,每一个Tomcat就是一个JVM。而两个Tomcat提供了同样的服务,每个Tomcat上的服务中的单体应用锁只会在自己的应用中生效,这样如果两个Tomcat上的服务,同时竞争一个资源时,就可能出现问题。

1.3、分布式锁

  针对单体应用锁的局限性,我们如何解决该问题呢?答案就是:借助第三方组件来实现分布式锁,多个服务可以通过第三方组件实现跨JVM、跨进程的分布式锁。

  常见的分布式锁的方案有:

基于数据库的分布式锁基于Redis的分布式锁基于Zookeeper的分布式锁 方式优点缺点数据库实现简单,易理解对数据库压力大Redis易理解自己实现,较复杂,不支持阻塞Zookeeper支持阻塞需要使用Zookeeper,实现复杂Curator(Zookeeper客户端)基于Zookeeper实现,提供了锁方法依赖Zookeeper,强一致Redisson(Redis客户端)基于Redis,提供锁方法,可阻塞 2、基于MySQL的排他锁(for update)实现分布式锁 2.1、排他锁(for update)

  for update是一种行级锁,又叫排它锁。一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行。

  行锁永远是独占方式锁。只有当出现如下的条件时,才会释放锁:1、执行提交(COMMIT)语句;2、退出数据库(LOG OFF);3、程序停止运行。

2.2、实现原理

  引入MySQL数据库作为实现分布式锁的第三方组件,创建一个数据表用于记录分布式锁(可以区分业务模块),然后在需要使用分布式锁的地方,通过select……for update获取对应业务模块的锁记录,如果获取成功,该记录行被锁定,其他线程将只能等待,当该线程执行结束后,就会释放锁,其他线程就可以获取锁并继续执行。

2.3、实现分布式锁 2.3.1、创建表

  其实,基于排他锁(for update)实现的分布式锁,只需要Id和module_code两个字段即可。

CREATE TABLE `t_sys_distributedlock` ( `id` int(11) NOT NULL AUTO_INCREMENT , `module_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `module_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `expiration_time` datetime NOT NULL , `creater` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `create_time` datetime NOT NULL , `modifier` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL , `modify_time` datetime NOT NULL , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci AUTO_INCREMENT=1 ROW_FORMAT=DYNAMIC ; 2.3.2、搭建项目

  这里主要基于Spring Boot、Mybatis、MySQL等搭建项目。其中,pom文件依赖如下:

org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.4 mysql mysql-connector-java runtime com.alibaba druid 1.1.11

  application.properties文件配置如下:

server.port=8080 #数据源配置 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql://localhost:3306/db_admin?useSSL=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource #mybatis配置 mybatis.mapper-locations=classpath:mappings/**/*Mapper.xml mybatis.type-aliases-package=com.qriver.distributedlock #日志 logging.level.com.qriver.distributedlock=debug

  最后,SpringBoot默认的启动类,如下:

@SpringBootApplication public class QriverDistributedLockApplication { public static void main(String[] args) { SpringApplication.run(QriverDistributedLockApplication.class, args); } } 2.3.3、DistributedLock

  分布式锁对应的实体类。

/** * 分布式锁 实体类 */ public class DistributedLock implements Serializable { private int id; private String moduleCode; private String moduleName; private Date expirationTime; private String creater; private Date createTime; private String modifier; private Date modifyTime; //省略getter or setter 方法 } 2.3.4、mapper文件

  这里主要是提供了id=getDistributedLock的select元素。这个语句中使用了排它锁(for update)。

SELECT T.* FROM t_sys_distributedlock T AND T.module_code = #{moduleCode} for update 2.3.5、DistributedLockMapper 接口

  DistributedLockMapper 中定义了一个getDistributedLock方法,根据返回结果是否为空判断是否获取到了数据库的行锁。

@Mapper public interface DistributedLockMapper { public List getDistributedLock(DistributedLock lock); } 2.3.6、DistributedLockService

  提供了获取锁的方法,通过判断返回值是否为空,然后转换成是否获取到锁的boolean类型的值。

@Service public class DistributedLockService { @Autowired private DistributedLockMapper distributedLockMapper; public boolean tryLock(String code){ DistributedLock distributedLock = new DistributedLock(); distributedLock.setModuleCode(code); List list = distributedLockMapper.getDistributedLock(distributedLock); if(list != null && list.size() > 0){ return true; } return false; } } 2.3.7、分布式锁的应用

  这里实现一个DemoController类,来应用分布式锁,实现如下:

@RestController public class DemoController { private Logger logger = LoggerFactory.getLogger(DemoController.class); @Autowired private DistributedLockService distributedLockService; @RequestMapping("mysqlLock") @Transactional(rollbackFor = Exception.class) public String testLock() throws Exception { logger.debug("进入testLock()方法;"); if(distributedLockService.tryLock("order")){ logger.debug("获取到分布式锁;"); Thread.sleep(30 * 1000); }else{ logger.debug("获取分布式锁失败;"); throw new Exception("获取分布式锁失败;"); } logger.debug("执行完成;"); return "返回结果"; } }

  在上述的方法上,添加了@Transactional注解,保证distributedLockService.tryLock(“order”)语句在方法执行完后才会提交该方法对应的数据,否则会直接进行commit,分布锁就会失效。

2.3.8、测试

  分别启动端口为8080、8081的两个实例,启动方法可以参考《IntelliJ Idea如何为一个项目启动多个项目实例》。然后,在浏览器分别访问http://localhost:8081/mysqlLock、 http://localhost:8080/mysqlLock 两个地址。

  8081服务实例日志:

2021-01-17 21:18:17.317 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.controller.DemoController : 进入testLock()方法; 2021-01-17 21:18:17.319 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.m.D.getDistributedLock : ==> Preparing: SELECT T.* FROM t_sys_distributedlock T WHERE T.module_code = ? for update 2021-01-17 21:18:17.320 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.m.D.getDistributedLock : ==> Parameters: order(String) 2021-01-17 21:18:17.323 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.m.D.getDistributedLock : Preparing: SELECT T.* FROM t_sys_distributedlock T WHERE T.module_code = ? for update 2021-01-17 21:18:18.914 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.m.D.getDistributedLock : ==> Parameters: order(String) 2021-01-17 21:18:47.327 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.m.D.getDistributedLock :


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3