什么是分布式锁?
在单体式架构中我们常用到锁,悲观锁乐观锁,但是在分布式中,因为会有很多系统,多进程,这些锁只能在自己的进程中生效,其他服务就不可以了,即他们只能实现线程并发问题的解决,而多进程的并发问题就没办法解决了,这个时候就出现了分布式锁,常用的有以下几种方式:
1.基于数据库实现分布式锁
2.基于缓存(redis等)
3.基于zookeeper
本章主要讲解基于redis的实现!
常用命令:
setnx users 10 设置一个key,值为10,此时,别的操作都不可以修改或者操作
del 设置的锁key 解锁
expire users seconds 设置过期时间
set users 10 nx ex 10 不仅设置了key还上了锁还设置了过期时间
java操作redis使用uuid防止误删:
有这么一种情况就是比如你两个操作并发,第一次设置了锁,过期时间为12秒,但是操作中卡顿了,导致13秒还没执行完,那此时就会出现一个问题,那就是设置的锁过期了,第二次操作可能就抢到了锁,但是你第一次还没执行完,它还没释放锁,等它反应过来不卡的时候,就会释放锁,那此时它释放的就是第二次的锁,这肯定不对了,那我么就可以在上锁的时候给每个锁设置自己的uuid,如果解锁的时候发现不是自己的就不解了
LUA脚本解决非原子操作:
uuid基本上可以解决大部分问题了,但是还是会出现一个问题,比如你已经判断完是自己的锁了,可以解锁,但是还没解锁,此时锁的时间到了自动过期了,那并发的操作还是会抢到锁,此时就需要,使用lua脚本把判断和解锁的过程绑定为一个原子性操作,这两个操作不做完,其他操作都进不来
可以看到有一个excute方法,这个方法就是执行lua脚本的,第一个参数就是一个存放lua字符串和返回值的接口实例,注意这个必须设置两个参数,一个是lua字符串,一个是返回值,否则会报错,然后第二个参数就是keys,第二个是avgv参数
除了锁里边的操作和上边不一样以外,其他都一样
上边的代码已经实现了基本的分布式锁的功能,但是还有一个巨大的漏洞就是,假如我在这个锁里去递归调用当前锁的方法,就会出现,死锁的问题,因为我们的锁是不可重入的,想要实现可重入锁,就要使用hash+lua脚本去实现
package sca.pro.core.threadtask;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import sca.pro.common.exception.BusinessException;
import sca.pro.core.utils.string.Convert;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
@Component
@RequiredArgsConstructor
public class ReantrLock implements Lock {
@Autowired
StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuId;
private String expire="3000";
public ReantrLock(String lockName) {
this.lockName = lockName;
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//加锁的逻辑,首先判断锁是否存在,不存在,则直接获取锁 ,如果锁存在则判断是不是自己的锁,如果是自己的锁则重入:就是在uuid基础上加一,否则重试,递归循环
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time!=-1){
this.expire= Convert.toStr(unit.toSeconds(time));
}
String script=" if redis.call('exists','lock')== 0 or redis.call('hexists','lock',ARGV[1])==1" +
"then " +
" redis.call('hincrby',KEYS[1],ARGV[1],1)" +
" redis.call('expire',KEYS[1],ARGV[2])" +
" return 1" +
"else" +
" return 0" +
"end";
Boolean lock = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuId,expire);
while (!lock){
Thread.sleep(80);
}
return true;
}
//判断自己的锁是否存在(hexists),不存在则返回nil,如果自己的锁存在,则减一,判断减一后的值是否为0,为0则释放锁并返回1,不为0,则返回0
@Override
public void unlock() {
String script="if redis.call('hexists',KEYS[1],ARGV[1]) ==0" +
"then" +
" return nil" +
"elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0" +
"then" +
" return redis.call('del',KEYS[1])" +
"else" +
" return 0" +
"end";
Long execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuId);
if (execute==null){
throw new BusinessException(1111,"这个锁不属于你");
}
}
@Override
public Condition newCondition() {
return null;
}
}