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文件依赖如下:
<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