一、分布式锁主流方案
1、基于数据库
2、基于缓存Redis(性能最高)
3、基于Zookeeper(可靠性最高)
二、redis方案
1、使用setnx 上锁,通过del释放锁
2、锁一直没有释放,设置过期时间,自动释放
3、上锁之后突然出现异常,无法设置过期时间(解决:上锁同时添加过期时间)
set [key] [value] nx ex [second]
或 setex [key] [second] [value]
4、代码案例
@GetMapping("testLock")
public void testLock() {
//获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS);
//获取锁成功,查询num值
if (lock) {
try{
Object value = redisTemplate.opsForValue().get("num");
if (ObjectUtils.isEmpty(value)) {
return;
}
//将有值的num转化为int
int num = Integer.parseInt(value + "");
//将num+1
redisTemplate.opsForValue().set("num", String.valueOf(++num));
}catch (Exception e){
e.printStackTrace();
}finally {
//释放锁
redisTemplate.delete("lock");
}
}else{
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ab测试
ab -n 1000 -c 100 http://192.168.xxx.1:9000/redisTest/testLock
这里需要注意的是要在配置文件中添加redis集群配置
#集群节点
spring.redis.cluster.nodes=192.168.xxx.165:6379,192.168.xxx.165:6380,192.168.xxx.165:6381,192.168.xxx.165:6389,192.168.xxx.165:6390,192.168.xxx.165:6391
并且在使用ab测试的时候出现了redis反序列化问题,在redisTemplete配置中添加如下代码
template.setValueSerializer(new StringRedisSerializer());
三、UUID防止锁误删
(解决a线程释放b线程的锁)
原因:假如有两个线程a和b,a先执行,在a执行过程中,因为某种原因a卡顿了,然后到了锁的自动释放时间锁自动释放了,此时b线程可以上锁,然后执行业务逻辑,此时a线程恢复正常,往下执行,然后释放锁,此时释放的是b线程上的锁。
解决:用uuid表示不同的线程
释放的时候,首先判断当前uuid和要释放的uuid是否一样
代码:
@GetMapping("testLock")
public void testLock() {
//生成uuid
String uuid = UUID.randomUUID().toString();
//获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);
//获取锁成功,查询num值
if (lock) {
try{
Object value = redisTemplate.opsForValue().get("num");
if (ObjectUtils.isEmpty(value)) {
return;
}
//将有值的num转化为int
int num = Integer.parseInt(value + "");
//将num+1
redisTemplate.opsForValue().set("num", String.valueOf(++num));
}catch (Exception e){
e.printStackTrace();
}finally {
String lockUuid = (String)redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUuid)){
//释放锁
redisTemplate.delete("lock");
}
}
}else{
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
四、LUA保证删除原子性
问题:释放锁的时候,比较uuid一样,删除锁的时候,正要删除,还没有删除,锁到了过期时间,自动释放,此时其他线程可以进行上锁操作,然后当前线程执行删除时,会删除其他线程上的锁
根本原因:删除操作不具备原子性
解决:使用lua脚本
代码:
@GetMapping("testLockLua")
public void testLockLua(){
//生成
String uuid = UUID.randomUUID().toString();
System.out.println(uuid);
String locKey = "lock";
//定义lua脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] \n\r"
+ "then return redis.call('del',KEYS[1]) \n\r"
+ "else return 0 \n\r"
+ "end ";
//获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid,3,TimeUnit.SECONDS);
//获取锁成功,查询num值
if (lock) {
try{
Object value = redisTemplate.opsForValue().get("num");
if (ObjectUtils.isEmpty(value)) {
return;
}
//将有值的num转化为int
int num = Integer.parseInt(value + "");
//将num+1
redisTemplate.opsForValue().set("num", String.valueOf(++num));
}catch (Exception e){
e.printStackTrace();
}finally {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Arrays.asList(locKey),uuid);
}
}else{
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
五,分布式锁满足的四个条件
1、互斥性:在任意时刻,只有一个客户端能持有锁
2、不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动释放锁,也能保证后续其他客户端能加锁。
3、加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给解了
4、加锁和解锁必须具有原子性