redis分布式锁


前言

redis大部分公司都在使用,再面试redis的时候一定也会被问到的一个问题就是分布式锁。或者你的领导让你写一个分布式锁,那这篇文章肯定可以帮到你


提示:以下是本篇文章正文内容,下面案例可供参考

一、分布式锁是什么?

在多线程同是访问同一个资源的时候,由于线程的访问顺序不能确定,会造成资源同时被多个线程修改的时候数据不一致的情况,锁就是为了避免这种情况的。只有获取到锁的线程,才能访问资源。所以同一时刻只有一个线程可以访问资源。
在分布式环境中一个服务会有多个实例(多个java虚拟机),java自带的锁synchronized只能锁住一个虚拟机,在单体应用的系统中是没有问题的,在分布式系统中要用分布式锁才可以。
在这里插入图片描述
上述场景中请求量大的时候就非常容易出现库存为负的场景,就是因为多个线程去减库存数据不一致造成的。
在这里插入图片描述
如果我们能够控制请求每次只能执行一个,这一个执行完之后才能执行下一个。这样就可以避免出现多个线程抢占同一个资源的情况了。这就是分布式锁的作用。

二、redis实现分布式锁的原理

一个锁应该具备的几个特性

互斥性

如果不能互斥的话就锁不住资源了,这个是锁最基本的要求

防止死锁

在使用锁的过程中一个线程拿到锁之后,一直都释放不了,其他的线程获取不到锁,程序执行一直阻塞,这个在设计上应该避免出现。

性能好

本身加锁的行为就会降低程序的QPS,所以在获取锁和释放锁的程序最好耗时少。

基于以上几点我们用redis来设计一个分布式锁:基本的思路就是我们每次执行代码之前先在redis里面做一个标记,执行完之后把这个标记清除,如果redis里面有这个标记的话说明有线程在执行,就不能执行(获取不到锁)。没有标记的话就可以执行(可以拿到锁)。
在这里插入图片描述

1.每次请求接口之前先拿着自己专有的key(可以的请求的参数+当前时间一定要保证唯一,每次的请求key都是不一样的)去redis查询是否存在这个key
2.如果存在说明有线程正在执行,如果不存在就可以继续执行。

需要注意的点

redis虽然时单线程的,java是多线程的,用我们的java去操作redis的时候一定要注意获取锁和释放锁是一个原子操作,要保证原子操作需要借助域lua脚本。

三、写一个基于注解分布式锁

1.获取锁和释放锁的脚本

获取锁脚本:

local requestKey=KEYS[1]
local lockedKeys=KEYS[2]
local requestValue=ARGV[1]
local expireTime=ARGV[2]
local nowTime=ARGV[3]
if redis.call('get',requestKey)
then
    return 0
end
local lockedHash = redis.call('hkeys',lockedKeys)
for i=1, #lockedHash do
    if string.find(requestKey,lockedHash[i]) or string.find(lockedHash[i],requestKey)
    then
        local lockTime = redis.call('hget',lockedKeys,lockedHash[i])
        if (nowTime-lockTime) >= expireTime * 1000
        then
            redis.call('hdel',lockedKeys,lockedHash[i])
        else
            return 0
        end
    end
end
redis.call('set',requestKey,requestValue)
redis.call('expire',requestKey,expireTime)
redis.call('hset',lockedKeys,requestKey,nowTime)
return 1

释放锁的脚本:

local requestKey=KEYS[1]
local lockedKeys=KEYS[2]
local requestValue=ARGV[1]
if redis.call('get', requestKey) == requestValue
then
    redis.call('hdel', lockedKeys,requestKey)
    return redis.call('del',requestKey)
else
    return 0
end

把脚本放在classPath的路径下面,方便程序加载里面的内容

代码编写

编写工具类 获取锁和释放锁的代码

public class RedisLockUtils {
    static final Long SUCCESS = 1L;
    static final String LOCKED_HASH = "cs:lockedKeyHash";
    static final String GET_LOCK_LUA_RESOURCE = "/lua/getLock.lua";
    static final String RELEASE_LOCK_LUA_RESOURCE = "/lua/releaseLock.lua";
    static final Logger LOG = LoggerFactory.getLogger(RedisLockUtils.class);

    public RedisLockUtils() {
    }

    public static boolean getLock(RedisTemplate redisTemplate, String lockKey, String requestValue, Integer expireTime) {
        LOG.info("start run lua script,{{}} start request lock", lockKey);
        lockKey = "{" + lockKey + "}";
        long start = System.currentTimeMillis();
        DefaultRedisScript<Long> luaScript = new DefaultRedisScript();
        luaScript.setLocation(new ClassPathResource("/lua/getLock.lua"));
        luaScript.setResultType(Long.class);
        Object result = redisTemplate.execute(luaScript, Arrays.asList(lockKey, lockKey + "cs:lockedKeyHash"), new Object[]{requestValue, String.valueOf(expireTime), String.valueOf(System.currentTimeMillis())});
        boolean getLockStatus = SUCCESS.equals(result);
        LOG.info("{{}} cost time {} ms,request lock result:{}", new Object[]{lockKey, System.currentTimeMillis() - start, getLockStatus});
        return getLockStatus;
    }

    public static boolean releaseLock(RedisTemplate redisTemplate, String lockKey, String requestValue) {
        lockKey = "{" + lockKey + "}";
        DefaultRedisScript<Long> luaScript = new DefaultRedisScript();
        luaScript.setLocation(new ClassPathResource("/lua/releaseLock.lua"));
        luaScript.setResultType(Long.class);
        Object result = redisTemplate.execute(luaScript, Arrays.asList(lockKey, lockKey + "cs:lockedKeyHash"), new Object[]{requestValue});
        boolean releaseLockStatus = SUCCESS.equals(result);
        LOG.info("{{}}release lock result:{}", lockKey, releaseLockStatus);
        return releaseLockStatus;
    }
}

编写锁的注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DistributeLock {
    //锁的key
    String key() default "";
    //唯一的标志 确保当前线程持有的锁不会被其他线程释放
    String hasKey() default "";
    //锁的过期时间模式5s
    int expire() default 5;
}

编写锁的切面

@Aspect
@Component
public class DistributeLockAspect {
    private static final Logger log = LoggerFactory.getLogger(DistributeLockAspect.class);
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    StringRedisTemplate redisTemplate;
    private SpelExpressionParser spelParser = new SpelExpressionParser();
    private static final String PERFIX = "#";

    public DistributeLockAspect() {
    }

    @Pointcut("@annotation(com.*.annotation.DistributeLock)")
    public void distributedLockable() {
    }

    @Around("distributedLockable()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        DistributeLock lockable = (DistributeLock)method.getAnnotation(DistributeLock.class);
        String value = "";
        List paramNameList;
        if (StringUtils.startsWith(lockable.key(), "#")) {
            paramNameList = Arrays.asList(signature.getParameterNames());
            List<Object> paramList = Arrays.asList(joinPoint.getArgs());
            EvaluationContext ctx = new StandardEvaluationContext();

            for(int i = 0; i < paramNameList.size(); ++i) {
                ctx.setVariable((String)paramNameList.get(i), paramList.get(i));
            }

            value = this.spelParser.parseExpression(lockable.key()).getValue(ctx).toString();
        } else {
            value = lockable.key();
        }

        paramNameList = null;

        try {
            Boolean lock = RedisLockUtils.getLock(this.redisTemplate, value, lockable.hasKey(),lockable.expire() );
            if (!lock) {
                return null;
            } else {
                Object object = joinPoint.proceed();
                return object;
            }
        } catch (Exception var10) {
            this.logger.error("分布式锁异常:" + var10.getMessage());
            throw var10;
        }finally {
            //执行完之后都要释放锁
            RedisLockUtils.releaseLock(this.redisTemplate, value, lockable.hasKey());
        }
    }
}

使用方式

@RestController
public class TestController {

    @DistributeLock(key = "lockTest",hasKey = "lockTest",expire = 4)
    @RequestMapping("/test")
    public String lockTest()throws Exception{
        return "SUCCESS";
    }
}

总结

redis的分布式锁有一个缺点就是过期时间,不适合时间比较长的任务,zk做分布式锁可以解决这个问题。

							欢迎大家关注我的微信公众号,您的关注就是我不懈的动力

在这里插入图片描述

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页