redis分布式锁

it2024-03-22  55

文章目录

前言一、分布式锁是什么?二、redis实现分布式锁的原理互斥性 防止死锁性能好需要注意的点 三、写一个基于注解分布式锁1.获取锁和释放锁的脚本代码编写使用方式 总结


前言

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做分布式锁可以解决这个问题。

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

最新回复(0)