基于 Redis SETNX 实现分布式锁手记

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 基于 Redis SETNX 实现分布式锁手记

环境与配置

* 

Redis 任意版本即可

* 

SpringBoot 任意版本即可,但是需要依赖 spring-boot-starter-data-redis


org.springframework.boot
spring-boot-starter-data-redis

看一看 Redis 社区对 SETNX 的解释

Redis SETNX

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for “SET if Not eXists”.

Return value: Integer reply, specifically:

* 

1 if the key was set

* 

0 if the key was not set

由于当某个 key 不存在的时候,SETNX 才会设置该 key。且由于 Redis 采用单进程单线程模型,所以,不需要担心并发的问题。那么,就可以利用 SETNX 的特性维护一个 key,存在的时候,即锁被某个线程持有;不存在的时候,没有线程持有锁。
关于实现的解释

由于只涉及到 Redis 的操作,所以,代码实现比较简单。只对外提供两个接口:获取锁、释放锁。

* 

IDistributedLock: 操作接口定义

* 

RedisLock: IDistributedLock 的实现类

* 

DistributedLockUtil: 分布式锁工具类

* 

SpringContextUtil: 获取当前 classpath 中的 Bean

SETNX 命令对应到 StringRedisTemplate 的 api 是 setIfAbsent,如下所示

/**

  • Set {@code key} to hold the string {@code value} if {@code key} is absent.

*

*/
Boolean setIfAbsent(K key, V value);
源码和注释信息

/**

  • 分布式锁接口

  • 只需要两个接口: 获取锁与释放锁

*/
public interface IDistributedLock {
/**

  • 获取锁

  • */

boolean acquire();
/**

  • 释放锁

  • */

void release();
}

import SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;

/**

  • 基于 Redis 实现的分布式锁

*/
@Slf4j
public class RedisLock implements IDistributedLock {

/* redis client /
private static StringRedisTemplate redisTemplate;

private String lockKey; // 锁的键值
private int expireMsecs = 15 * 1000; // 锁超时, 防止线程得到锁之后, 不去释放锁
private int timeoutMsecs = 15 * 1000; // 锁等待, 防止线程饥饿
private boolean locked = false; // 是否已经获取锁

RedisLock(String lockKey) {
this.lockKey = lockKey;
}

RedisLock(String lockKey, int timeoutMsecs) {
this.lockKey = lockKey;
this.timeoutMsecs = timeoutMsecs;
}

RedisLock(String lockKey, int expireMsecs, int timeoutMsecs) {
this.lockKey = lockKey;
this.expireMsecs = expireMsecs;
this.timeoutMsecs = timeoutMsecs;
}

public String getLockKey() {
return this.lockKey;
}

@Override
public synchronized boolean acquire() {

int timeout = timeoutMsecs;

if (redisTemplate == null) {
redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
}

try {

while (timeout >= 0) {

long expires = System.currentTimeMillis() + expireMsecs + 1;
String expiresStr = String.valueOf(expires); // 锁到期时间

if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiresStr)) {
locked = true;
log.info("[1] 成功获取分布式锁!");
return true;
}
String currentValueStr = redisTemplate.opsForValue().get(lockKey); // redis里的时间

// 判断是否为空, 不为空的情况下, 如果被其他线程设置了值, 则第二个条件判断是过不去的
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

String oldValueStr = redisTemplate.opsForValue().getAndSet(lockKey, expiresStr);

// 获取上一个锁到期时间, 并设置现在的锁到期时间
// 只有一个线程才能获取上一个线程的设置时间
// 如果这个时候, 多个线程恰好都到了这里, 但是只有一个线程的设置值和当前值相同, 它才有权利获取锁
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
locked = true;
log.info("[2] 成功获取分布式锁!");
return true;
}
}

timeout -= 100;
Thread.sleep(100);
}
} catch (Exception e) {
log.error("获取锁出现异常, 必须释放: {}", e.getMessage());
}

return false;
}

@Override
public synchronized void release() {

if (redisTemplate == null) {
redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
}

try {
if (locked) {

String currentValueStr = redisTemplate.opsForValue().get(lockKey); // redis里的时间

// 校验是否超过有效期, 如果不在有效期内, 那说明当前锁已经失效, 不能进行删除锁操作
if (currentValueStr != null && Long.parseLong(currentValueStr) > System.currentTimeMillis()) {
redisTemplate.delete(lockKey);
locked = false;
log.info("[3] 成功释放分布式锁!");
}
}
} catch (Exception e) {
log.error("释放锁出现异常, 必须释放: {}", e.getMessage());
}
}
}

/**

  • 分布式锁工具类

*/
public class DistributedLockUtil {

/**

  • 获取分布式锁
  • 默认获取锁15s超时, 锁过期时间15s

*/
public static IDistributedLock getDistributedLock(String lockKey) {
lockKey = assembleKey(lockKey);
return new RedisLock(lockKey);
}

/**

  • 获取分布式锁

*/
public static IDistributedLock getDistributedLock(String lockKey, int timeoutMsecs) {
lockKey = assembleKey(lockKey);
return new RedisLock(lockKey, timeoutMsecs);
}

/**

  • 获取分布式锁

*/
public static IDistributedLock getDistributedLock(String lockKey, int timeoutMsecs, int expireMsecs) {
lockKey = assembleKey(lockKey);
return new RedisLock(lockKey, expireMsecs, timeoutMsecs);
}

/**

  • 对 key 进行拼接

*/
private static String assembleKey(String lockKey) {
return String.format("imooc_analyze_%s", lockKey);
}
}

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**

  • 获取当前 classpath 中的 Bean

*/
@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

@SuppressWarnings("unchecked")
public static T getBean(Class c) throws BeansException {
return (T) applicationContext.getBean(c);
}
}

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
11天前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
47 16
|
1月前
|
NoSQL Redis 数据库
计数器 分布式锁 redis实现
【10月更文挑战第5天】
48 1
|
1月前
|
NoSQL 算法 关系型数据库
Redis分布式锁
【10月更文挑战第1天】分布式锁用于在多进程环境中保护共享资源,防止并发冲突。通常借助外部系统如Redis或Zookeeper实现。通过`SETNX`命令加锁,并设置过期时间防止死锁。为避免误删他人锁,加锁时附带唯一标识,解锁前验证。面对锁提前过期的问题,可使用守护线程自动续期。在Redis集群中,需考虑主从同步延迟导致的锁丢失问题,Redlock算法可提高锁的可靠性。
76 4
|
1月前
|
缓存 NoSQL 算法
面试题:Redis如何实现分布式锁!
面试题:Redis如何实现分布式锁!
|
3月前
|
NoSQL Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
112 2
基于Redis的高可用分布式锁——RedLock
|
3月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
1月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
61 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
存储 缓存 NoSQL
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
63 4
|
1月前
|
缓存 NoSQL Ubuntu
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
55 3
下一篇
无影云桌面