引言
在分布式业务开发中,很多场景都会需要实现分布式锁,而在具体的开发实践中,程序员需要自行实现分布式锁,导致实现方式不统一,难以维护。
在 MyBatis-Plus 生态中, Lock4j 提供了例如 redission、redisTemplate、zookeeper 分布式锁组件,功能强大,可扩展性强。
一、核心概念
** 1.1分布式锁定义:**
分布式锁是一种可以在分布式环境下实现互斥访问的技术,分布式锁可以保证在任何情况下只能有一个节点可以访问到共享资源。
1.2分布式锁特点
** ** 互斥性:分布式锁可以保证在同一时间只能用一个节点可以访问的共享资源,其他节点处于等待状态。
优先等待性:分布式锁要求节点在访问共享资源前,必须先获取锁,节点获取锁失败时则需要进行有限等待,然后才能再次尝试获取锁。
不可撤销性:分布式锁不允许在锁被释放前撤销锁。
二、分布式锁的实现方式
- 基于数据库乐观锁/悲观锁
- Redis分布式锁(本文):利用
setnx
命令。此命令是原子性操作,只有key不存在的情况下,才能set
,就意味着线程获取到了锁 - Zookeeper分布式锁:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的
- Memcached:利用
add
命令。此命令是原子性操作,只有key不存在的情况下,才能add
,也就意味着线程获取到了锁
redis是如何实现加锁的?
使用Redis的SETNX命令实现分布式锁,并提供了一系列的方法来操作分布式锁
在redis中,有一条命令,实现锁
SETNX key value
该命令的作用是将
key
的值设为
value
,仅当
key
不存在。若给定的
key
已经存在,则 SETNX 不做任何动作。设置成功,返回
1
;设置失败,返回
0
使用 redis 来实现锁的逻辑就是这样的
线程 1 获取锁 -- > setnx lockKey lockvalue
-- > 1 获取锁成功
线程 2 获取锁 -- > setnx lockKey lockvalue
-- > 0 获取锁失败 (继续等待,或者其他逻辑)
线程 1 释放锁 -- >
线程 2 获取锁 -- > setnx lockKey lockvalue
-- > 1 获取成功
接下来我们将基于springboot实现redis分布式锁
**1. 引入
redis
、springmvc、
lombok
依赖**
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.miao.redis</groupId>
<artifactId>springboot-caffeine-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-redis-lock-demo</name>
<description>Demo project for Redis Distribute Lock</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<!--springMvc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 新建RedisDistributedLock.java并书写加锁解锁逻辑
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import java.nio.charset.StandardCharsets;
/**
* @author miao
* redis 加锁工具类
*/
@Slf4j
public class RedisDistributedLock {
/**
* 超时时间
*/
private static final long TIMEOUT_MILLIS = 15000;
/**
* 重试次数
*/
private static final int RETRY_TIMES = 10;
/***
* 睡眠时间
*/
private static final long SLEEP_MILLIS = 500;
/**
* 用来加锁的lua脚本
* 因为新版的redis加锁操作已经为原子性操作
* 所以放弃使用lua脚本
*/
private static final String LOCK_LUA =
"if redis.call(\"setnx\",KEYS[1],ARGV[1]) == 1 " +
"then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
/**
* 用来释放分布式锁的lua脚本
* 如果redis.get(KEYS[1]) == ARGV[1],则redis delete KEYS[1]
* 否则返回0
* KEYS[1] , ARGV[1] 是参数,我们只调用的时候 传递这两个参数就可以了
* KEYS[1] 主要用來传递在redis 中用作key值的参数
* ARGV[1] 主要用来传递在redis中用做 value值的参数
*/
private static final String UNLOCK_LUA =
"if redis.call(\"get\",KEYS[1]) == ARGV[1] "
+ "then "
+ " return redis.call(\"del\",KEYS[1]) "
+ "else "
+ " return 0 "
+ "end ";
/**
* 检查 redisKey 是否上锁
*
* @param redisKey redisKey
* @param template template
* @return Boolean
*/
public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template) {
return lock(redisKey, value, template, RETRY_TIMES);
}
private static Boolean lock(String redisKey,
String value,
RedisTemplate<Object, Object> template,
int retryTimes) {
boolean result = lockKey(redisKey, value, template);
while (!(result) && retryTimes-- > 0) {
try {
log.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(RedisDistributedLock.SLEEP_MILLIS);
} catch (InterruptedException e) {
return false;
}
result = lockKey(redisKey, value, template);
}
return result;
}
private static Boolean lockKey(final String key,
final String value,
RedisTemplate<Object, Object> template) {
try {
RedisCallback<Boolean> callback = (connection) -> connection.set(
key.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8),
Expiration.milliseconds(RedisDistributedLock.TIMEOUT_MILLIS),
RedisStringCommands.SetOption.SET_IF_ABSENT
);
return template.execute(callback);
} catch (Exception e) {
log.info("lock key fail because of ", e);
}
return false;
}
/**
* 释放分布式锁资源
*
* @param redisKey key
* @param value value
* @param template redis
* @return Boolean
*/
public static Boolean releaseLock(String redisKey,
String value,
RedisTemplate<Object, Object> template) {
try {
RedisCallback<Boolean> callback = (connection) -> connection.eval(
UNLOCK_LUA.getBytes(),
ReturnType.BOOLEAN,
1,
redisKey.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8)
);
return template.execute(callback);
} catch (Exception e) {
log.info("release lock fail because of ", e);
}
return false;
}
}
注:
spring-data-redis 有StringRedisTempla和RedisTemplate两种,但是我选择了RedisTemplate,因为他比较万能。他们的区别是:当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可, 但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是 更好的选择。
选择lua脚本是因为,脚本运行是原子性的,在脚本运行期间没有客户端可以操作,所以在释放锁的时候用了lua脚本,
而redis最新版加锁时保证了Redis值和自动过期时间的原子性,所用没用lua脚本
**3. 创建测试类
TestController
**
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author miao
*/
@RestController
@Slf4j
public class TestController {
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@PostMapping("/order")
public String createOrder() throws InterruptedException {
log.info("开始创建订单");
Boolean isLock = RedisDistributedLock.isLock("testLock", "456789", redisTemplate);
if (!isLock) {
log.info("锁已经被占用");
return "fail";
} else {
//.....处理逻辑
}
Thread.sleep(10000);
//一定要记得释放锁,否则会出现问题
RedisDistributedLock.releaseLock("testLock", "456789", redisTemplate);
return "success";
}
}
4. 使用postman进行测试
工具和资源推荐
- Redis官方文档:redis.io/documentati…
- SpringBoot官方文档:spring.io/projects/sp…
- SpringBoot Redis官方文档:spring.io/projects/sp…
总结
分布式锁是一种在分布式环境下实现互斥访问的技术,它已经广泛应用于分布式系统中。未来,分布式锁的发展趋势将继续向简单、高效、可靠的方向发展。挑战之一是在分布式环境下实现高可用的分布式锁,以确保分布式锁的可靠性。挑战之二是在分布式环境下实现低延迟的分布式锁,以提高分布式系统的性能。
版权归原作者 Java技术前沿 所有, 如有侵权,请联系我们删除。