上一篇我们实战了Redis缓存数据库查询、页面访问量计数器两大高频场景,借助内存高速读写和原子命令,实现了性能提速与精准计数。但在高并发场景下,比如缓存击穿重建、库存扣减、重复提交、定时任务重复执行,单纯的缓存和计数无法解决并发竞争问题,极易出现数据错乱、超卖等故障。
这时候就需要分布式锁来兜底,而Redis凭借SETNX原子命令,能零成本实现轻量级分布式互斥锁,无需引入复杂中间件,是入门分布式锁的最佳选择。本篇从零拆解SETNX分布式锁,从原理、实战到避坑,带你快速落地安全可靠的互斥逻辑。
核心定位:分布式锁用于跨JVM、跨服务器的并发互斥,保证同一时间只有一个线程执行关键业务;SETNX(SET if Not eXists)是Redis实现最简分布式锁的核心,原子性操作无并发漏洞。
一、为什么需要分布式锁?
单机应用中,我们用synchronized、Lock就能解决线程安全问题,但分布式架构下,服务部署在多台服务器,单机锁失效,必须用分布式锁协调并发。
典型并发痛点场景
- 缓存击穿:热点缓存过期,大量请求同时击穿缓存打到数据库
- 库存超卖:商品秒杀时,多个线程同时扣减库存,导致库存为负
- 重复提交:用户频繁点击提交按钮,重复发起请求创建多条数据
- 定时任务重复执行:集群部署下,同一定时任务多节点同时运行
分布式锁的核心目标:加锁互斥、防并发、防死锁、保证数据一致性。
二、SETNX实现分布式锁核心原理
1. SETNX命令基础
SETNX是Redis原子性命令,全称“不存在则设置”,执行逻辑具备排他性,天然适配锁机制:
- 命令格式:
SETNX key value - key不存在:创建key并赋值,返回1,代表加锁成功
- key已存在:不做任何操作,返回0,代表加锁失败
致命缺陷:单纯SETNX加锁后,如果服务宕机、程序异常,锁永远不会释放,会造成死锁。因此必须结合EX过期时间,使用SET key value EX 超时时间 NX合并命令(Redis 2.6.12+支持),保证锁超时自动释放。
2. 标准锁命令(推荐)
摒弃单独SETNX,改用原子合并命令,彻底解决死锁问题:
- 加锁:
SET lock:key uniqueValue EX 10 NX - 解锁:
DEL lock:key
参数说明:
- lock:key:锁的Key,遵循业务命名规范
- uniqueValue:唯一标识(如UUID),用于防止误删他人锁
- EX 10:锁超时时间10秒,超时自动释放
- NX:等价于SETNX,仅不存在时设置
3. 分布式锁执行流程
- 线程尝试加锁,执行SET…EX…NX命令
- 加锁成功:执行业务逻辑
- 业务执行完毕:主动释放锁(DEL)
- 加锁失败:等待重试或直接返回失败
- 服务宕机:锁超时自动释放,避免死锁
三、实战:SpringBoot实现SETNX分布式锁
基于前文RedisTemplate环境,实现可直接复用的分布式锁工具类,适配缓存击穿、库存扣减等场景。
第一步:定义锁Key规范
沿用Redis命名规范,做到见名知意、便于维护:
- 缓存重建锁:
lock:cache:article:1001 - 库存扣减锁:
lock:stock:goods:6688 - 提交防重锁:
lock:submit:user:1001
第二步:分布式锁工具类代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Redis SETNX 分布式锁工具类
*/
@Component
public class RedisLockUtil {
@Resource
private RedisTemplate<String, String> redisTemplate;
// 锁前缀
private static final String LOCK_PREFIX = "lock:";
// 默认锁超时时间(秒)
private static final long DEFAULT_LOCK_EXPIRE = 10;
/**
* 加锁(SETNX+EX原子命令)
* @param lockKey 锁业务Key
* @return 锁唯一标识(用于解锁),加锁失败返回null
*/
public String lock(String lockKey) {
// 生成唯一值,防止误删锁
String uniqueValue = UUID.randomUUID().toString();
String key = LOCK_PREFIX + lockKey;
// 原子加锁:不存在则设置+超时时间
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, uniqueValue, DEFAULT_LOCK_EXPIRE, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success) ? uniqueValue : null;
}
/**
* 解锁(校验唯一值,防止误删)
* @param lockKey 锁业务Key
* @param uniqueValue 加锁时的唯一标识
*/
public void unlock(String lockKey, String uniqueValue) {
String key = LOCK_PREFIX + lockKey;
// 校验唯一值,一致才删除锁
String value = redisTemplate.opsForValue().get(key);
if (uniqueValue.equals(value)) {
redisTemplate.delete(key);
}
}
}
第三步:业务场景实战(缓存击穿防护)
结合前文文章查询缓存,用分布式锁解决热点缓存过期击穿问题:
@Service
public class ArticleService {
@Resource
private ArticleMapper articleMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedisLockUtil redisLockUtil;
private static final String ARTICLE_CACHE_KEY = "article:detail:";
private static final long CACHE_EXPIRE = 7200;
/**
* 带分布式锁的文章查询(防缓存击穿)
*/
public Article getArticleByIdWithLock(Long articleId) {
String cacheKey = ARTICLE_CACHE_KEY + articleId;
// 1. 先查缓存
Article cacheArticle = (Article) redisTemplate.opsForValue().get(cacheKey);
if (cacheArticle != null) {
return cacheArticle;
}
// 2. 缓存未命中,加锁防止缓存击穿
String lockKey = "cache:article:" + articleId;
String uniqueValue = redisLockUtil.lock(lockKey);
if (uniqueValue == null) {
// 加锁失败,休眠重试(或直接返回提示)
try {
Thread.sleep(100);
return getArticleByIdWithLock(articleId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
try {
// 3. 二次校验缓存(防止其他线程已重建缓存)
cacheArticle = (Article) redisTemplate.opsForValue().get(cacheKey);
if (cacheArticle != null) {
return cacheArticle;
}
// 4. 查数据库并重建缓存
Article dbArticle = articleMapper.selectById(articleId);
if (dbArticle != null) {
redisTemplate.opsForValue().set(cacheKey, dbArticle, CACHE_EXPIRE, TimeUnit.SECONDS);
}
return dbArticle;
} finally {
// 5. 无论业务成败,必须释放锁
redisLockUtil.unlock(lockKey, uniqueValue);
}
}
}
四、SETNX分布式锁注意事项(避坑指南)
1. 必须设置锁超时时间
严禁使用无过期时间的SETNX,服务宕机、程序异常会导致锁无法释放,引发死锁。超时时间根据业务耗时设置,建议5~30秒。
2. 解锁必须校验唯一值
如果不校验唯一标识,线程A的锁超时自动释放后,线程B加锁成功,线程A执行完毕会误删线程B的锁,导致锁失效。唯一值+校验解锁是必备操作。
3. 避免锁超时导致业务未执行完
如果业务执行时间超过锁超时时间,锁会提前释放,并发问题依旧存在。入门场景可适当延长超时时间;进阶场景可采用锁续期(守护线程定时延长锁时间)。
4. 加锁与解锁必须成对出现
解锁逻辑放在finally代码块中,保证无论业务是否异常,锁都会被释放,杜绝死锁隐患。
5. 不适合复杂分布式场景
SETNX锁是非重入、非阻塞、无等待队列的简易锁,适合低并发、简单互斥场景;高并发、集群可靠场景,建议使用Redisson、ZooKeeper等专业分布式锁组件。
五、SETNX分布式锁优缺点总结
| 优点 | 缺点 |
|---|---|
| 实现简单,零额外组件依赖 | 非重入锁,不支持嵌套加锁 |
| 性能极高,Redis内存操作速度快 | 无等待队列,高并发下重试成本高 |
| 原子操作,无并发竞争漏洞 | Redis集群模式下,存在锁丢失风险 |
| 轻量级,适合入门与简单业务 | 需手动处理超时、续期问题 |
核心总结:SETNX是Redis入门分布式锁的最优方案,通过“原子加锁+超时释放+校验解锁”三步,就能解决绝大多数简单并发互斥问题,完美衔接缓存、计数等场景,是后端开发者必备的基础技能。