Redis相关文章
分布式锁
通常我们单机部署服务的时候只需要在代码中加一个synchronized关键字或加一个Lock对象等单机锁就能保护好资源安全,但随着数据量越来越大,用户量越来越大,后端服务的部署通常都会加一层负载均衡加分布式集群部署。
单机锁仅仅只能保证同一进程中各个线程之间的操作安全,分布式部署下会出现多个进程多个线程的情况,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,只能通过分布式锁的方式来实现。
分布式锁需要具备三个能力:
- 互斥性
- 避免死锁
- 高可用(涉及分布式部署redis方案,本篇暂不介绍)
实现方案
分布式锁的核心功能是保证代码在分布式部署环境下保证数据的互斥性和操作安全性,通俗点讲就是A服务里有个根据数据库配置启动后台任务的功能,假如部署a,b两个实例,那么存在在a实例中的任务就没必要同时存在在b实例中。如何实现这个需求呢,显然就需要一个分布式锁来解决。
Redis是如何实现多实例之间的访问互斥的,背后依赖的命令就是setNX命令,setNX很好理解,set if not exist的缩写,意为如果 key 不存在,才会设置它的值,否则什么也不做。
- 加锁语句 setNX key value,如果 key 不存在,才会设置值
- 解锁语句 del key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
- 锁过期 expire key timeout,设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
setNX + expire
if (setnx(key,1)==1){ expire(key,30) try {} finally { del(key)}}
这个方案把加锁与设置过期时间的操作分成了两个,众所周知redis的事务是不可靠的,无法完全保证原子性,假如加锁成功之后,执行expire语句时候redis服务崩溃,那么key这个锁就永远不会被释放掉。
SET KEY VALUE EX PX NX XX
我们在springboot项目中使用redis通常使用封装好的RedisTemplate类,而setIfAbsent方法则是对redis中setNX的封装,先看下源码如何描述该方法的。
/** * Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @param timeout the key expiration timeout. * @param unit must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. * @since 2.1 * @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a> */ @Nullable Boolean setIfAbsent(K key, V value,long timeout, TimeUnit unit);
大体意思为如果 key 不存在,设置它的值直到指定时间后将其删除掉,否则什么也不做。将上锁和设置超时操作合并成一个原子操作。
乍一看似乎可以用了,其实忽略了一个非常重要的设定,有道是解铃还须系铃人,谁上的锁谁去解锁,所以setNX值的时候尽量set一个具有唯一标识的字符串,比如线程id又或是请求id。
用于业务逻辑处理完成finally语句中解锁的时候校验是否为同一个请求,但这时又有一个新问题。解锁的动作又不是一个原子操作了。怎么办呢。
lua语言
lua语言是可以嵌入到一些中间件里的脚本语言,比如redis、nginx等,因为redis实例公用一个lua解释器,一个lua脚本再执行的时候其他lua脚本无法执行,继而保证了原子性。
我们把需要的操作用lua语言整合到一起就不会有原子性问题了。
publicstaticbooleanreleaseDistributedLock(Jedisjedis, StringlockKey, StringrequestId) { Stringscript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Objectresult = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { returntrue; } returnfalse; }
看门狗
如果业务操作耗时长,还没有执行完操作就锁就到期了怎么办,如果不使用第三方框架的话可以参考redission框架,实现一个后台线程进行监控,如果业务还没结束就合理延长锁的存活时间,防止锁过期提前释放掉被别的实例抢到出现安全问题。
核心就是只要线程一加锁成功,就会启动一个后台线程,每隔ns检查一下,如果业务线程还持有锁,就对锁的过期时间延长。犹豫工作很像老家看门的狗子,俗称看门狗线程。