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

导读:本篇文章讲解 基于MySQL数据库排它锁(for update)实现的分布式锁,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、锁的概述

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

1.1、单体应用锁

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

1.2、单体应用锁的局限性

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

1.3、分布式锁

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

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

  1. 基于数据库的分布式锁
  2. 基于Redis的分布式锁
  3. 基于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文件依赖如下:

<dependency>
   <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>2.1.4</version>
 </dependency>

 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <scope>runtime</scope>
 </dependency>
 <!--集成druid连接池-->
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid</artifactId>
     <version>1.1.11</version>
 </dependency>

  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)。

<?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.qriver.distributedlock.mapper.DistributedLockMapper">

       <select id="getDistributedLock" resultType="com.qriver.distributedlock.entity.DistributedLock">
            SELECT
                T.*
            FROM
                t_sys_distributedlock T
            <where>
                 <if test="moduleCode != null">
                    AND T.module_code = #{moduleCode}
                 </if>
            </where>

            for update
       </select>

</mapper>
2.3.5、DistributedLockMapper 接口

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

@Mapper
public interface DistributedLockMapper {

    public List<DistributedLock> 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<DistributedLock> 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             : <==      Total: 1
2021-01-17 21:18:17.323 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.controller.DemoController          : 获取到分布式锁;
2021-01-17 21:18:47.323 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.controller.DemoController          : 执行完成;

  8080服务实例日志:

2021-01-17 21:18:18.913 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.controller.DemoController          : 进入testLock()方法;
2021-01-17 21:18:18.913 DEBUG 47436 --- [nio-8080-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             : <==      Total: 1
2021-01-17 21:18:47.327 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.controller.DemoController          : 获取到分布式锁;
2021-01-17 21:19:17.328 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.controller.DemoController          : 执行完成;

  在访问上述两个地址时,第一个先获取到了锁,然后执行业务逻辑,当业务执行完成后(30s),第二个服务获取到了锁,然后继续执行业务逻辑。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/68757.html

(0)
小半的头像小半

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!