分布式锁的面试题
问题1:Redis除了拿来做缓存,你还见过基于Redis的什么用法?
答:传统五大数据类型的落地应用;做分布式锁
问题2:Redis做分布式锁的时候有需要注意的问题?
问题3:如果是Redis是单点部署的,会带来什么问题?
那你准备怎么解决单点问题呢?
问题4:集群模式下,比如主从模式,有没有什么问题呢?
问题5:那你简单的介绍一下Redlock吧?你简历上写redisson,你谈谈
问题6:Redis分布式锁如何续期?看门狗知道吗?
Base案例(boot+redis)
使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
1、建Module
两个微服务:boot_redis01、boot_redis02
2、改POM
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.hhf</groupId>
<artifactId>boot_redis01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</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>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3、建YML
server.port=1111
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=123456
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认0
spring.redis.lettuce.pool.min-idle=0
4、启动类
@SpringBootApplication
public class Application01 {
public static void main(String[] args) {
SpringApplication.run(Application01.class, args);
}
}
5、config
@Configuration
public class RedisConfig {
/**
* 保证不是序列化后的乱码配置
*/
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
6、controller
@RestController
public class RedisController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}
7、测试
访问:
http://localhost:1111/buy_goods
http://localhost:2222/buy_goods
正常访问!
吐槽
1.单机版没加锁
没有加锁,并发下数字不对,出现
超卖现象
思考:加synchronized?加ReentrantLock?还是都可以?
答 :视业务需求而定。synchronized加锁的话会一直进行阻塞直到获得锁,而ReentrantLock可以使用tryLock()设置获取锁的时间,如果获取不到锁可以中断做其它事情。
解决办法
修改为2.0版本:
@GetMapping("/buy_goods")
public String buy_Goods() {
synchronized (this) {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}
2.nginx分布式微服务架构
分布式部署后,单机锁还是出现超卖现象,需要分布式锁
Nginx配置负载均衡
我这里使用的docker配置Nginx实现负载均衡
详细参考:Dokcer部署Nginx 与 Nginx实现负载均衡
下面进行测试
手动方式:
可以发现可以轮询访问微服务。
高并发模拟:模拟2000个线程同时访问微服务
结果:
出现重复消费问题!
解决办法
上redis分布式锁
setnx
:Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理.
修改为3.0
public static final String REDIS_LOCK_KEY = "lock";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
}
3.程序异常
出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
加锁解锁,lock/unlock必须同时出现并保证调用
解决办法
修改为4.0
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
}
}
4.微服务宕机
部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
解决办法
修改为5.0
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
stringRedisTemplate.expire(REDIS_LOCK_KEY, 10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
}
}
5.过期时间设置不能保证原子性
设置key+过期时间分开了,必须要合并成一行具备原子性
解决办法
修改为6.0:换重载方法,同时设置过期时间
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
}
}
6.误释放锁
张冠李戴,删除了别人的锁
解决办法
修改为7.0版本:对要删除的锁进行判断
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
} finally {
if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
}
}
}
7.释放锁不是原子性
finally块的判断+del删除操作不是原子性的
解决办法1:使用Redis事务
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
} finally {
while (true) {
stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁
if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))) {
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();//开始事务
stringRedisTemplate.delete(REDIS_LOCK_KEY);
List<Object> list = stringRedisTemplate.exec();
if (list == null) { //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除
continue;
}
}
//如果删除成功,释放监控器,并且break跳出当前循环
stringRedisTemplate.unwatch();
break;
}
}
}
解决办法2:使用Lua脚本
Redis可以通过eval命令
保证代码执行的原子性
public class RedisUtils {
private static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool = new JedisPool(jedisPoolConfig, "ip", 6379, 100000);
}
public static Jedis getJedis() throws Exception {
if (null != jedisPool) {
return jedisPool.getResource();
}
throw new Exception("Jedispool is not ok");
}
}
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
} else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
}
} finally {
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1]" + "then " + "return redis.call('del', KEYS[1])" + "else " + " return 0 " + "end";
try {
Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
if ("1".equals(result.toString())) {
System.out.println("------del REDIS_LOCK_KEY success");
} else {
System.out.println("------del REDIS_LOCK_KEY error");
}
} finally {
if (null != jedis) {
jedis.close();
}
}
}
}
8.redis续期问题
确保redisLock过期时间大于业务执行时间的问题
集群+CAP对比zookeeper
- Redis:AP:redis异步复制造成的锁丢失, 比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。此时如果集群模式下,就得上Redisson来解决
- Zookeeper:CP
解决办法
redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现
配置类:
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
controller:
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
redissonLock.unlock();
}
}
9.完善
可能出现错误:
是在并发多的时候就可能会遇到这种错误,可能会被重新抢占
不见得当前这个锁的状态还是在锁定,并且本线程持有
解决办法
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
} finally {
//还在持有锁的状态,并且是当前线程持有的锁再解锁
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
总结
synchronized 单机版oK,上分布式
===> nginx分布式微服务 单机锁不行
===>取消单机锁 上redis分布式锁setnx
===> 只加了锁,没有释放锁, 出异常的话,可能无法释放锁,必须要在代码层面finally释放锁
===> 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
===> 为redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行的原子性操作
===>必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
===> lua或者事务
===> redis集群环境下,我们自己写的也不OK直接上RedLock之Redisson落地实现