没有理论分析,直接上生产代码
项目结构
分布锁的要求
- 同一时间只有一个线程可以获取到锁
- 正常情况下,只能是获取到锁的线程,在完成任务后释放锁
- 异常情况下,可以强制释放锁
- 锁定的任务在加锁时间内未完成,不能影响任务执行。这个要求和(3)相关。
引入redis maven依赖
1. maven依赖
<parent>
<artifactId>spring-boot-dependencies</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
2. application.yml中的redis配置
server:
port: 19002
spring:
application:
name: sign
#redis
redis:
host: 127.0.0.1
port: 6379
password: 123456
sign-server:
bean-name: sign-exception
RedisConfig配置类
package cn.com.soulfox.redis;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @create 2023/11/9 9:52
*/
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key的序列化类型
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
RedisLockUtil工具类
package cn.com.soulfox.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @create 2023/11/9 10:02
*/
@Component
public class RedisLockUtil {
private static RedisTemplate<String, String> redisTemplate;
//常用过期时间
public static final long EXPRTIME_600 = 600; //失效时间,单位秒
public static final long EXPRTIME_300 = 300; //失效时间,单位秒
public static final long EXPRTIME_1800 = 1800; //失效时间,单位秒
//
private static final Long SUCCESS = 1L;
/**
* 尝试锁定 lockKey 对应的数据/功能/操作
* @param lockKey 锁定的关键字,作为key放入redis
* @param locker 锁定者,作为value放入redis
* @param expireTime 锁定过期时间,用于强制解锁
* @return
*/
public static boolean lock(String lockKey, String locker, long expireTime){
if(expireTime <= 0){
expireTime = RedisLockUtil.EXPRTIME_300;
}
//setIfAbsent方法,是一个原子操作
//如果lockkey不存在,就保存一个 (lockkey -> locker) 键值对到redis中;
//expireTime: 键值对的过期时间,超过该时间redis会自动删除该键值对
//TimeUnit.SECONDS: 时间单位(秒)
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, locker, expireTime, TimeUnit.SECONDS);
if(result){
//保存键值对成功,代表成功获取锁
return true;
}
return false;
}
/**
* 解锁操作
* @param lockKey 锁定的关键字,作为key放入redis
* @param locker 锁定者,作为value放入redis
* @return
*/
public static boolean unLock(String lockKey, String locker){
//使用脚本,保证查看和删除操作的原子性
//脚本含义: 如果lockKey的对应值等于locker,则删除lockKey。删除这个lockKey就是解锁操作
//也就是只有锁定者才能解锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = 0L;
try {
result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), locker);
} catch (Exception e) {
e.printStackTrace();
}
if(SUCCESS.equals(result)){
//删除键值对成功,代表解锁成功
return true;
}
return false;
}
/**
* 获取 locker
* 线程名: 区分同一个jvm中的线程
* UUID: 区分不同机器的线程
* @return
*/
public static String getLocker(){
String threadName = Thread.currentThread().getName();
String uuid = UUID.randomUUID().toString();
return threadName + "@@" + uuid;
}
@Autowired
public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
RedisLockUtil.redisTemplate = redisTemplate;
}
}
使用方式
package cn.com.soulfox.redis;
import cn.com.soulfox.FactoryModelRun;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author jiyh
* @create 2023/11/9 10:21
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FactoryModelRun.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisLockUtilTest {
@Test
public void test() throws InterruptedException {
//被锁定的目标数据
String lockKey = "ProjectName::BizName::DataKey";
//锁定者
String locker = RedisLockUtil.getLocker();
//获取锁
boolean result = RedisLockUtil.lock(lockKey, locker,60);
System.out.println(result);
if(result){
System.out.println("获取到锁,开始执行业务代码");
try {
//处理业务数据
// doBussiness();
} catch (Exception e) {
e.printStackTrace();
} finally {
//正常执行业务代码后解锁
boolean delResult = RedisLockUtil.unLock(lockKey,locker);
System.out.println(delResult);
}
}else {
System.out.println("未获取到锁--------------------");
}
}
}
业务未处理结束,但锁过期怎么处理
若果业务代码未执行结束,但是锁过期。那么这条数据,是有可能被其他线程处理的。这样的场景是不允许的。我们可以在业务数据表里加上一个状态字段。
默认是“00-待处理”,只有是“00-待处理”的数据,才会被一个线程从表里读取处理。
线程获取锁之后,在执行业务代码之前,先把状态改为“01-处理中”。
处理完成结束之后把状态改为“02-处理成功”或“03-处理失败”。
这样即使锁过期时业务没处理完。因为此时数据状态是“01-处理中”,也不会被其他线程再次处理。
这样处理方式,就不需要去给锁续期。同时当数据需要重新处理时,把状态改为“00-待处理”就可以了,非常方便。