可以参考Redis官网对SETNX命令的介绍:
https://redis.io/commands/setnx
命令格式
SETNX key value
将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。
返回值
返回整数,具体为
- 1,当 key 的值被设置
- 0,当 key 的值没被设置
例子
redis> SETNX mykey “hello”
(integer) 1
redis> SETNX mykey “hello”
(integer) 0
redis> GET mykey
“hello”
redis>
使用SETNX实现分布式锁
多个进程执行以下Redis命令:
SETNX lock.foo
如果 SETNX 返回1,说明该进程获得锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
如果 SETNX 返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试 SETNX 操作,以获得锁。
解决死锁
考虑一种情况,如果进程获得锁后,断开了与 Redis 的连接(可能是进程挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其他进程都会处于一直等待的状态,即出现“死锁”。
上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。
然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:
从上面的情况可以得知,在检测到锁超时后,进程不能直接简单地执行 DEL 删除键的操作以获得锁。
为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法。
我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。接下来的情况:
另外,值得注意的是,在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。
项目使用SETNX实现分布式锁原因:
由于线上所有的微服务都是采用两个服务节点来进行部署的,正常情况下不会出现两个服务节点同时执行同一个方法,即同一段业务逻辑,遇到这样一种情况,在部署的微服务中存在定时任务,也就是在将来的某个时间点会触发这个任务自动执行,那么这时候两个服务节点在这个时间点都会开启一个线程去执行相同的代码逻辑,显然这是违反业务逻辑的,即重复执行了一样的逻辑,因此在这种情况下就需要加入分布式锁来解决这样的定时任务场景问题。
其中一个定时任务代码:
package com.huajin.usercenter.task;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.huajin.common.util.DateUtil;
import com.huajin.usercenter.enums.CertStatus;
import com.huajin.usercenter.po.CertInfoPo;
import com.huajin.usercenter.service.seal.CertService;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class CertTask {
@Autowired
private CertService certService;
@Scheduled(cron = "0 0 0 * * ?")
public void updateAuthCode() {
Map map = new HashMap<>();
map.put("status", CertStatus.NotDownload.getValue());
map.put("updateTimeEnd", DateUtil.add(new Date(), Calendar.DAY_OF_YEAR, -10));
List list = certService.select(map);
for(CertInfoPo po : list) {
try {
certService.updateAuthCode(po.getSn());
} catch (Exception e) {
log.error("", e);
}
}
}
}
介绍了使用分布式锁的场景,那么怎么使用Redis提供的SETNX命令实现分布式锁呢?
由于项目中不止存在一个定时任务,且使用分布式锁不属于主业务逻辑部分,因此采用了AOP面向切面编程,项目中存在的定时任务均在task包下,还有一个特点就是所有的定时任务方法都有Spring提供的定时任务注解@Scheduled
,考虑到这里我们便知道了切点就是两者的结合,为了保险起见,采用&&符号连接这两个条件,接下来就是考虑使用何种通知方式,考虑到线程执行的快慢,定时任务方法时刻都在两个服务节点开启的定时任务线程争夺中,因此在这里使用@Around注解,即环绕通知实现。
在这里特别强调一下@Around环绕通知去其它几个通知的区别,@Around环绕通知接受的是ProceedingJoinPoint接口的实现类的一个实例,该ProceedingJoinPoint接口继承自JoinPoint接口,ProceedingJoinPoint接口比JoinPoint接口多暴露了两个方法,如下所示:
接下来便是使用RedisTemplate提供的API来操作Redis命令SETNX实现分布式锁,
package com.huajin.usercenter.task;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Aspect
@Component
@Slf4j
public class TaskAspect {
@Autowired
private RedisTemplate
以上两个服务节点开启的定时任务线程中的某个线程在key不存在的情况下则相当于与获取到了锁,待目标方法执行完毕后或者抛出异常都可以删除这个key,即也就是释放锁,达到了分布式锁的目的。
参考:
https://blog.csdn.net/lihao21/article/details/49104695