自研redis分布式锁存在的问题以及面试切入点
lock加锁关键逻辑
unlock解锁的关键逻辑
使用Redis的分布式锁
之前手写的redis分布式锁有什么缺点??
Redis之父的RedLock算法
Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用
官网
Redis分布式锁
RedLock的设计理念
该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁客户端执行以下操作:
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。
本次教学演示用3台实例来做说明。客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。
解决方案与容错公式
RedLock的落地实现Redisson
github地址
java
https://github.com/redisson/redisson
java
https://redisson.pro/docs/configuration/#cluster-mode

pom文件
java
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.1</version>
</dependency>
RedissonConfig配置类
java
package com.atguigu.redislock.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig
{
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public Redisson redisson()
{
Config config = new Config();
config.useSingleServer().setAddress("redis://172.18.8.229:6379").setDatabase(0).setPassword("root");
return (Redisson) Redisson.create(config);
}
}
业务方法的改造
java
@Autowired
private Redisson redisson;
//V9版本
public String saleV9(){
String retMessage="";
RLock redissonLock = redisson.getLock("redisLock");
redissonLock.lock();
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
Integer inventory =result==null?0: Integer.valueOf(result);
//扣减库存
if(inventory>0){
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventory));
retMessage="成功卖出一个商品,库存剩余:"+inventory;
System.out.println(retMessage+"\t"+"服务端口号"+port);
try {
TimeUnit.SECONDS.sleep(120);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
retMessage="商品卖完了";
}
}finally {
redissonLock.unlock();
}
return retMessage+"\t"+"服务端口号"+port;
}
Jemeter压测
这样直接删除锁是有bug的
解决方案
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
{
redissonLock.unlock();
} }
java
@Autowired
private Redisson redisson;
//V9版本
public String saleV9(){
String retMessage="";
RLock redissonLock = redisson.getLock("redisLock");
redissonLock.lock();
try {
//查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//判断库存是否足够
Integer inventory =result==null?0: Integer.valueOf(result);
//扣减库存
if(inventory>0){
stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventory));
retMessage="成功卖出一个商品,库存剩余:"+inventory;
System.out.println(retMessage+"\t"+"服务端口号"+port);
}else{
retMessage="商品卖完了";
}
}finally {
if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
{
redissonLock.unlock();
} }
return retMessage+"\t"+"服务端口号"+port;
}
Redisson源码解析
- 加锁
- 可重入
- 续命
- 解锁
- 分析步骤
Redis分布式锁过期了,但是业务逻辑还没处理完怎么办?(还记得之前说过的缓存续命么)
守护线程续命
在获取锁成功后,给锁加一个watch dog,watchdog会启动一个定时任务,在锁没有被释放且快要过期的时候会续期。

源码分析
通过redissson新建出来的锁key,默认是30s
加锁的核心代码
Lua脚本加锁
- 通过 exists 判断,如果锁不存在,则设置值和过期时间,加锁成功。
- 通过 hexists 判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功。
- 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了锁 key 的剩余生存时间),加锁失败。
看门狗的锁续期
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
Lua脚本执行看门狗的锁续期
解锁方法
多机案例
理论参考
实战演示:
docker启动三个redis实例
java
server.port=9090
spring.application.name=redlock
spring.swagger2.enabled=true
spring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=single
spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10
spring.redis.single.address1=172.18.8.229:6382
spring.redis.single.address2=172.18.8.229:6383
spring.redis.single.address3=172.18.8.229:6384
redis三个实例对应的配置类
java
package com.atguigu.redis.redlock.config;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {
@Autowired
RedisProperties redisProperties;
@Bean
RedissonClient redissonClient1() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress1();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient2() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress2();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient3() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress3();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
}
controller的演示方法
java
@RestController
@Slf4j
public class RedLockController
{
public static final String CACHE_KEY_REDLOCK = "ATGUIGU_REDLOCK";
@Autowired RedissonClient redissonClient1;
@Autowired RedissonClient redissonClient2;
@Autowired RedissonClient redissonClient3;
@GetMapping(value = "/multilock")
public String getMultiLock()
{
String taskThreadID = Thread.currentThread().getId()+"";
RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);
RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);
redLock.lock();
try
{
log.info("come in biz multilock:{}",taskThreadID);
try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
log.info("task is over multilock:{}",taskThreadID);
}catch (Exception e){
e.printStackTrace();
log.error("multilock exception:{}",e.getCause()+"\t"+e.getMessage());
}finally {
redLock.unlock();
log.info("释放分布式锁成功key:{}",CACHE_KEY_REDLOCK);
}
return "multilock task is over: "+taskThreadID;
}
}
锁续期成功
宕机后仍然成功