基于redis手写一个分布式锁
简介
分布式锁,名字就看的出来是应用于分布式系统中的锁,我们平时熟知的synchronized等一系列锁是在一个jvm内实现的,而很多大型的系统通常都是采用分布式部署开发,不在一个jvm内运行所以synchronized等锁就失效了,分布式锁就是解决分布式系统中锁的问题,下面我们来手写一个基于redis的分布式锁来了解一下分布式锁的原理。本文源码地址:https://github.com/itwwj/frame.git 中的redis项目,其他的项目请忽略。
一、分布式锁原理
- 互斥性
- 高可用性
- 可重入性
- 阻塞
- 锁超时
二、java实现redis的分布式锁
依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
配置文件application.yml:
spring:
redis:
host: 192.168.1.177 #服务器地址
password: root # 密码
port: 6379 #端口
database: 0 #数据库索引默认为0
lettuce:
pool:
max-idle: 10
min-idle: 10
max-active: 100
定义分布式锁接口:
public interface DistributedLock {
/**
* 默认超时时间
* 单位:毫秒
*/
long TIMEOUT_MILLIS = 5000;
/**
* 重试次数
*/
int RETRY_TIMES = 100;
/**
* 每次重试后等待的时间
* 单位:毫秒
*/
long SLEEP_MILLIS = 100;
/**
* 获取锁
*
* @param key key
* @return 成功/失败
*/
default boolean lock(String key) {
return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
}
/**
* 获取锁
*
* @param key key
* @param retryTimes 重试次数
* @return 成功/失败
*/
default boolean lock(String key, int retryTimes) {
return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
}
/**
* 获取锁
*
* @param key key
* @param retryTimes 重试次数
* @param sleepMillis 获取锁失败的重试间隔 单位:毫秒
* @return 成功/失败
*/
default boolean lock(String key, int retryTimes, long sleepMillis) {
return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
}
/**
* 获取锁
*
* @param key key
* @param expire 获取锁超时时间
* @return 成功/失败
*/
default boolean lock(String key, long expire) {
return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
}
/**
* 获取锁
*
* @param key key
* @param expire 获取锁超时时间
* @param retryTimes 重试次数
* @return 成功/失败
*/
default boolean lock(String key, long expire, int retryTimes) {
return lock(key, expire, retryTimes, SLEEP_MILLIS);
}
/**
* 获取锁
*
* @param key key
* @param expire 获取锁超时时间
* @param retryTimes 重试次数
* @param sleepMillis 获取锁失败的重试间隔
* @return 成功/失败
*/
boolean lock(String key, long expire, int retryTimes, long sleepMillis);
/**
* 释放锁
*
* @param key key值
* @return 释放结果
*/
boolean releaseLock(String key);
}
redis分布式锁实现:
@Slf4j
public class RedisDistributedLock implements DistributedLock {
private static final String UNLOCK_LUA;
/*
* 通过lua脚本释放锁,来达到释放锁的原子操作
*/
static {
UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call(\"del\",KEYS[1]) " +
"else " +
" return 0 " +
"end ";
}
private final RedisTemplate<String, Object> redisTemplate;
private final ThreadLocal<String> lockFlag = new ThreadLocal<>();
public RedisDistributedLock(RedisTemplate<String, Object> redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}
@Override
public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
boolean result = setRedis(key, expire);
// 如果获取锁失败,按照传入的重试次数进行重试
while (!result && retryTimes-- > 0) {
try {
log.debug("get redisDistributeLock failed, retrying..." + retryTimes);
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
log.warn("Interrupted!", e);
Thread.currentThread().interrupt();
}
result = setRedis(key, expire);
}
return result;
}
private boolean setRedis(final String key, final long expire) {
//可重入性锁
if (lockFlag.get() != null && lockFlag.get().equals(redisTemplate.opsForValue().get(key))) {
return true;
}
try {
return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
String uuid = UUID.randomUUID().toString();
lockFlag.set(uuid);
byte[] keyByte = redisTemplate.getStringSerializer().serialize(key);
byte[] uuidByte = redisTemplate.getStringSerializer().serialize(uuid);
return (boolean) connection.set(keyByte, uuidByte, Expiration.from(expire, TimeUnit.MILLISECONDS),
RedisStringCommands.SetOption.ifAbsent());
});
} catch (Exception e) {
log.error("设置redis锁发生异常", e);
}
return false;
}
@Override
public boolean releaseLock(String key) {
// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
try {
// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
byte[] scriptByte = redisTemplate.getStringSerializer().serialize(UNLOCK_LUA);
return connection.eval(scriptByte, ReturnType.BOOLEAN, 1
, redisTemplate.getStringSerializer().serialize(key)
, redisTemplate.getStringSerializer().serialize(lockFlag.get()));
});
} catch (Exception e) {
log.error("释放redis锁发生异常", e);
} finally {
lockFlag.remove();
}
return false;
}
}
注册bean:
@Configuration
public class RedisConfig {
/**
* 分布式锁
*
* @param redisTemplate redis
* @return 分布式锁
*/
@Bean
@ConditionalOnMissingBean
public DistributedLock redisDistributedLock(RedisTemplate redisTemplate) {
return new RedisDistributedLock(redisTemplate);
}
}
测试:
这里我们使用jMeter来进行并发测试
先来测试没有使用分布式锁的代码
@RequestMapping("test")
public void testLock() {
if (testint > 0) {
testint = testint - 1;
log.info("testint :" + testint);
}
}
可以看出没有加锁会出现线程问题
接下来我们测试一下自己手写的分布式锁:
@Slf4j
@RestController
@RequiredArgsConstructor
public class DistributedLockController {
private final DistributedLock distributedLock;
private static Integer testint = 10;
private static final String KEY = "test";
@RequestMapping("distributedLock")
public void distributeLock() {
try {
boolean test = distributedLock.lock(KEY);
if (test && testint > 0) {
testint = testint - 1;
log.info("testint :" + testint);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
distributedLock.releaseLock(KEY);
}
}
}
这次的数据就规矩多了,手写分布式锁成功。
三、使用redisson实现分布式锁
项目中通常是不会使用手写的分布式锁,自己手写只是了解分布式锁的原理,当然要是不怕填坑可以冒险尝试,下面我们使用一下redisson的分布式锁
引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
配置文件application.yml:
spring:
redis:
host: 192.168.1.177 #服务器地址
password: root # 密码
port: 6379 #端口
database: 0 #数据库索引默认为0
lettuce:
pool:
max-idle: 10
min-idle: 10
max-active: 100
redisson:
address: redis://${spring.redis.host}:${spring.redis.port}
password: ${spring.redis.password}
配置类:
/**
* @author jie
*/
@Data
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
private int timeout = 3000;
private String address;
private String password;
private int database = 0;
private int connectionPoolSize = 64;
private int connectionMinimumIdleSize = 10;
private int slaveConnectionPoolSize = 250;
private int masterConnectionPoolSize = 250;
}
bean配置:
/**
* @author jie
*/
@EnableCaching
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@EnableConfigurationProperties({RedisProperties.class, RedissonProperties.class})
public class RedisConfig {
/**
* key 的生成策略
*/
@Bean
public KeyGenerator keyGenerator() {
return (target, method, objects) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(COLON);
sb.append(method.getName());
for (Object obj : objects) {
if (obj != null) {
sb.append(COLON);
sb.append(obj.toString());
}
}
return sb.toString();
};
}
/**
* 用于 @Cacheable 相关注解
*
* @param redisConnectionFactory 链接工厂
* @return 缓存管理器
*/
@Bean(name = "cacheManager")
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration defConfig = getDefConf();
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defConfig)
.build();
}
private RedisCacheConfiguration getDefConf() {
RedisCacheConfiguration def = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new RedisObjectSerializer()));
def.entryTtl(Duration.ofDays(1));
def.computePrefixWith(cacheName -> cacheName.concat(COLON));
return def;
}
/**
* RedisTemplate配置
*
* @param factory redis链接工厂
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
setSerializer(factory, template);
return template;
}
private void setSerializer(RedisConnectionFactory factory, RedisTemplate template) {
RedisObjectSerializer redisObjectSerializer = new RedisObjectSerializer();
RedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(redisObjectSerializer);
template.setValueSerializer(redisObjectSerializer);
template.setConnectionFactory(factory);
}
/**
* stringRedisTemplate 配置
*
* @param factory
* @return
*/
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate();
setSerializer(factory, template);
return template;
}
@Bean
@ConditionalOnMissingBean
public RedisUtils getRedisOps(RedisTemplate redisTemplate) {
return new RedisUtils(redisTemplate, true);
}
@Bean
@ConditionalOnMissingBean
public SuperBaseRedisOps redisPlusOps(RedisUtils redisUtils) {
return new RedisOpsImpl(redisUtils);
}
/**
* 分布式锁(自己实现的)
*
* @param redisTemplate redis
* @return 分布式锁
*/
// @Bean
// @ConditionalOnMissingBean
public DistributedLock redisDistributedLock(RedisTemplate redisTemplate) {
return new RedisDistributedLock(redisTemplate);
}
/**
* 分布式锁(redisson的)
*
* @param redissonClient redis
* @return 分布式锁
*/
@Bean
@ConditionalOnMissingBean
public DistributedLock redisDistributedLock(RedissonClient redissonClient) {
return new RedissonDistributedLock(redissonClient);
}
@Bean
@ConditionalOnMissingBean
public RedissonClient getredisson(RedissonProperties redissonProperties) {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(redissonProperties.getAddress())
.setTimeout(redissonProperties.getTimeout())
.setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
.setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize());
if (StrUtil.isNotBlank(redissonProperties.getPassword())) {
serverConfig.setPassword(redissonProperties.getPassword());
}
return Redisson.create(config);
}
}
测试:
由于我们只改变了实现,所以测试代码不需要修改(体现出了设计模式的强大!)
@Slf4j
@RestController
@RequiredArgsConstructor
public class DistributedLockController {
private final DistributedLock distributedLock;
private static Integer testint = 20;
private static final String KEY = "test";
@RequestMapping("distributedLock")
public void distributeLock() {
try {
boolean test = distributedLock.lock(KEY);
if (test && testint > 0) {
testint = testint - 1;
log.info("testint :" + testint);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
distributedLock.releaseLock(KEY);
}
}
}