自研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地址
https://github.com/redisson/redisson
https://redisson.pro/docs/configuration/#cluster-mode
pom文件
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.19.1</version></dependency>
RedissonConfig配置类
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
{@Beanpublic RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置value的序列化方式jsonredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}@Beanpublic Redisson redisson(){Config config = new Config();config.useSingleServer().setAddress("redis://172.18.8.229:6379").setDatabase(0).setPassword("root");return (Redisson) Redisson.create(config);}
}
业务方法的改造
@Autowiredprivate 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();
} }
@Autowiredprivate 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实例
server.port=9090
spring.application.name=redlockspring.swagger2.enabled=truespring.redis.database=0
spring.redis.password=
spring.redis.timeout=3000
spring.redis.mode=singlespring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10spring.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三个实例对应的配置类
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 {@AutowiredRedisProperties redisProperties;@BeanRedissonClient 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);}@BeanRedissonClient 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);}@BeanRedissonClient 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的演示方法
@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;}
}
锁续期成功
宕机后仍然成功