分布式锁
1、分布式锁在项目中有哪些应用场景
- 系统是一个分布式系统,集群,java的锁已经锁不住了。
- 操作共享资源,比如库里唯一的用户数据。
- 同步访问,即多个进程同时操作共享资源
2、Redis作为分布式锁用什么命令
SETNX
格式:setnx key value 将key的值社会value,当前仅当key不存在。
若key已经存在,则SETNX不做任何动作
SETNX是【set if not exists】如果不存在,则set
一般要设置过期时间,一般是大于此完成的业务的时间
set key value nx ex 10s
3、 Redis做分布式锁死锁的情况如何解决?
- 加锁,没有释放锁。需要加释放锁的操作,delete key
- 加锁后,程序还没有执行释放锁,程序挂了,没有delete key 。需要使用key的过期机制。
4、Redis如何做分布式锁
假设有两个服务A,B都希望获得锁,执行过程大致如下:
- 服务A为了获得锁,向Redis发起如下命令:set ProductId:lock 0ASDASDAS NX EX 30000 ,其中,productId是自己定义,与业务有关的id,“0ASDASDAS ”是一串随机值,必须保证全局唯一,“NX”指的是当且仅当次key不存在的时候,返回执行成功,否则执行失败。"EX 30000"指的是30秒后,key会自动删除。执行命令返回成功,表明服务已经获取到了锁。
- 服务B为了获取锁,向Redis获取同样的命令:set ProductId:lock 00111 NX EX 30000,由于Redis中已经存在同名的key,且并未过期,因此执行失败,服务B未能获取锁。服务B进入循环请求状态,比如每隔一秒(自行设置)向redis发送请求,直到执行成功并获取锁。
- 服务A的业务代码执行时长超过了30秒,导致key超时,因此redis自动删除了key。此时服务B再次发送命令执行完毕,假设本次请求的value为00002222,此时需要在服务A中对key进行续期。
- 服务A执行完毕后,为了释放锁,服务A会主动向Redis发起删除key请求。注意:在删除key之前,一定要判断服务A持有的value与Redis内存储的value是否一致。比如在这个场景之下,Redis的锁已经不是A持有的那一把,而是服务B创建的,如果贸然使用服务A持有的key来删除锁,会导致服务B的锁释放掉。此外,由于删除时涉及一系列判断逻辑。
5、基于Zookeeper的分布式锁的实现原理是什么?
顺序节点特性:
使用Zookeeper的顺序节点特性,假如我们在/lock/目录下创建3个节点,ZK集群会按照发起创建的顺序来创建节点,节点分别为 /lock/00000001 , /lock/00000002 , /lock/00000003,最后一位数是依次递增的,节点名由zk完成。
临时节点的特性:
ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除。EPHEMERAL_SEQUENTIAL为临时顺序节点。
根据ZK中节点是否存在,可以作为分布式锁的锁状态,依次来实现一个分布式锁,下面是分布式锁的基本逻辑:
- 客户端1调用create()方法创建名为"/业务ID/lock"的临时顺序节点
- 客户端1调用getChildren(“业务ID”)方法来获取所有已经创建的子节点。
- 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否排第一,那么就认为这个客户端1获得了锁,在它前面没有别的客户端拿到了锁
- 如果创建的节点不是节点中需要最小的,那么则监视比自己创建节点序列号小的最大的节点,进入等待。知道下次监视的子节点变更的时候,再进行子节点获取,判断是否获取锁。
6、Redis分布式锁与ZK分布式锁的区别
Redis:
- Redis只是保证最终一致性,副本间的数据复制是异步进行的(Set是写,Get是读,Redis的集群一般是读写分离架构,存在主从延迟的情况),主从切换后可以有部分数据没有复制过去可能会丢失锁的情况,故强调数据一致性的时候不推荐使用Redis,推荐使用Zk
- Redis的集群各方法响应时间均为最低。随着并发量和业务数量的提升其响应时间会有明显上升(公网集群影响因素偏大),但是极限的qps可以达到最大且基本无异常
Zookeeper:
- 使用Zookeeper集群,锁原理是使用Zookeeper的临时顺序节点,临时顺序节点的生命周期在Clinet与集群的Session结束时结束。因此如果某个Clinet节点存在网络问题,与Zookeeper集群断开连接,Session超时同样会导致锁被错误释放(导致被其他线程错误的持有),因此Zookeeper也无法保证完全一致。
- ZK有较好的稳定性;响应时间抖动很少,没有出现异常情况。但是随着并发和业务数量提升其响应时间qps会明显下降。
总结:
- Zk每次操作锁都要创建若干个节点,完成后释放节点,会浪费很多时间
- 而Redis只是简单的数据操作,没有这个问题
7、Redis分布式锁模拟代码
(1)导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
配置
spring.redis.port=6379
spring.redis.host=localhost
编写util获取锁与释放锁
package com.ycz.unils;
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.stereotype.Component;
import java.time.Duration;
import java.util.Collections;
/**
* @Description:
* @Author: Alex
* @Date 2022-07-01-21:00
* @Version: V1.0
**/
@Component
public class RedisLockUtils {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final Long LOCK_REDIS_TIMEOUT =100L;
private static final Long LOCK_REDIS_WAIT =500L;
public Boolean getLock(String key ,String value){
return this.redisTemplate.opsForValue().setIfAbsent(key , value , Duration.ofSeconds(LOCK_REDIS_TIMEOUT));
}
public Long releaseLock(String key , String value){
String luaString = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> longDefaultRedisScript = new DefaultRedisScript<>(luaString, Long.class);
Long execute = redisTemplate.execute(longDefaultRedisScript, Collections.singletonList(key), value);
return execute;
}
}
controller
package com.ycz.controller;
import com.ycz.unils.RedisLockUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @Description:
* @Author: Alex
* @Date 2022-07-01-21:12
* @Version: V1.0
**/
@RestController
@Slf4j
public class RedisController {
@Autowired
private RedisLockUtils lockUtils;
//对这个订单的id相关的进行修改,所以要加分布式锁
@RequestMapping("/order/update/{orderId}")
public Object orderUpdate(@PathVariable String orderId) throws InterruptedException {//一般是传实体,这个不在意这个细节,到时候可以改成entity.getId
String key = orderId+":lock";
String value = UUID.randomUUID().toString();
Boolean lock = lockUtils.getLock(key, value);
if (lock){
log.info("获取到了锁");
//休息10s,假装为业务逻辑代码,长一点显示效果
Thread.sleep(10000);
lockUtils.releaseLock(key , value);
return "执行成功";
}else{
return "请稍后再试";
}
}
}
启动
http://localhost:8080/order/update/1
(1)假设其orderId是相同的
第一个获取到了锁
第二个在等待第一个锁执行完成,再发出请求
(2)假设他们的执行的orderId是不相同的
都会进行执行