一、帮我生一个一段 Java springBoot 项目代码示例,使用 Redis 实现分布式锁
在 Spring Boot 项目中,我们可以利用 RedisTemplate 或 StringRedisTemplate 结合 Lua 脚本来实现一个线程安全、可重入的分布式锁。
下面是一个完整的 Java 示例,使用 Spring Data Redis 和 RedisTemplate 实现分布式锁。
✅ 环境准备
确保 pom.xml 中包含 Spring Data Redis 依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
✅ 配置 RedisTemplate(支持字符串)
默认的 RedisTemplate 使用 JDK 序列化,我们通常希望使用字符串,所以先配置一下:
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
✅ 分布式锁实现类(RedisDistributedLock)
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RedisDistributedLock {
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final String lockValue; // 唯一标识(UUID + 线程ID)
private final long expireTime = 10; // 锁过期时间(秒)
// Lua 脚本:原子性释放锁
private static final String RELEASE_LOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
private static final RedisScript<Long> releaseScript = new DefaultRedisScript<>(RELEASE_LOCK_SCRIPT, Long.class);
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = "lock:" + lockKey;
this.lockValue = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();
}
/**
* 获取锁(阻塞式,带超时)
*/
public boolean acquire(long timeoutSeconds) {
long endTime = System.currentTimeMillis() + timeoutSeconds * 1000;
while (System.currentTimeMillis() < endTime) {
Boolean success = redisTemplate
.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
return true;
}
try {
Thread.sleep(100); // 短暂休眠后重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
/**
* 释放锁(原子操作)
*/
public void release() {
redisTemplate.execute(releaseScript, Collections.singletonList(lockKey), lockValue);
}
}
✅ 控制器示例(测试用)
@RestController
public class TestController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/do-something")
public String doSomething() {
RedisDistributedLock lock = new RedisDistributedLock(redisTemplate, "order:123");
if (lock.acquire(5)) {
try {
System.out.println("线程 " + Thread.currentThread().getId() + " 获取锁成功,开始执行任务…");
// 模拟业务逻辑(如扣减库存、生成订单等)
Thread.sleep(3000);
System.out.println("任务执行完成。");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.release();
System.out.println("锁已释放。");
}
return "操作成功";
} else {
return "获取锁失败,资源正被占用";
}
}
}
✅ 启动类
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
✅ 关键点说明
| 唯一标识 | 使用 UUID + ThreadId 防止 A 客户端删除 B 客户端的锁 |
| 原子获取 | setIfAbsent(…, expireTime, TimeUnit.SECONDS) 对应 Redis 的 SET key value NX EX |
| 原子释放 | 使用 Lua 脚本确保“判断 + 删除”是原子的 |
| 自动过期 | 防止客户端宕机导致死锁 |
| 可重试 | 获取失败后循环重试,直到超时 |
✅ 进阶建议(生产环境)
- 使用 Redisson(成熟的分布式锁库),支持可重入锁、公平锁、红锁(RedLock)等。
- 加入 看门狗机制(自动续期),防止业务执行时间超过锁过期时间。
- 使用 Redis Cluster 或 Sentinel 提高可用性。
这个示例可以直接运行在 Spring Boot 项目中,适合大多数分布式场景(如秒杀、库存扣减、订单幂等处理等)。
二、在帮我生成一段不使用工具类,直接把操作 Redis 写到业务代码中的案例
这就为你提供一个将 Redis 分布式锁逻辑直接嵌入业务代码的示例。这种写法常见于简单的脚本或快速原型开发中,虽然不利于代码复用,但逻辑一目了然。
以下是基于 Spring Boot + RedisTemplate 的完整业务接口示例,模拟“秒杀扣减库存”的场景:
🛒 业务控制器示例(含内联锁逻辑)
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class SeckillController {
@Resource
private RedisTemplate<String, String> redisTemplate;
// 模拟商品库存 Key
private static final String STOCK_KEY = "seckill:stock:1001";
// 锁的过期时间(秒)
private static final long LOCK_EXPIRE_SECONDS = 10;
@GetMapping("/seckill")
public String seckill() {
// 1. 生成唯一请求ID(代表当前客户端实例)
String requestId = UUID.randomUUID().toString();
// — 第一步:尝试获取分布式锁(内联代码开始)—
String lockKey = "lock:seckill:1001";
// 使用 RedisTemplate 执行 SET 命令获取锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
LOCK_EXPIRE_SECONDS,
TimeUnit.SECONDS
);
// 如果没有获取到锁,直接返回失败
if (Boolean.FALSE.equals(locked)) {
return "秒杀失败:系统繁忙,请重试";
}
try {
// — 第二步:执行业务逻辑(临界区)—
// 1. 查询当前库存
String stockStr = redisTemplate.opsForValue().get(STOCK_KEY);
int stock = stockStr == null ? 0 : Integer.parseInt(stockStr);
if (stock > 0) {
// 2. 扣减库存(模拟业务操作)
redisTemplate.opsForValue().set(STOCK_KEY, String.valueOf(stock – 1));
System.out.println("秒杀成功!剩余库存:" + (stock – 1) + ",线程:" + Thread.currentThread().getId());
return "秒杀成功!";
} else {
return "秒杀失败:库存不足";
}
} catch (Exception e) {
// 异常处理
e.printStackTrace();
return "秒杀异常";
} finally {
// — 第三步:释放锁(内联代码结束)—
// 使用 Lua 脚本保证释放锁的原子性(防止删除了别人的锁)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 执行 Lua 脚本删除锁
redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);
}
}
}
💡 代码核心逻辑解析
这段代码没有封装任何工具类,直接在 seckill() 方法中处理了锁的全流程:
1. 获取锁(非阻塞)
- 逻辑:使用 redisTemplate.opsForValue().setIfAbsent() 方法。
- 对应 Redis 命令:SET lock:seckill:1001 <requestId> NX EX 10
- 含义:仅当锁 Key 不存在时才设置成功(NX),并设置 10 秒自动过期(EX),防止死锁。
2. 业务处理
- 逻辑:在 try 块中直接操作 Redis 读取和扣减库存。
- 注意:这里假设库存也是存放在 Redis 中的,保证了数据读写的高性能。
3. 释放锁(原子性操作)
- 逻辑:使用 redisTemplate.execute() 执行 Lua 脚本。
- 关键点:Lua 脚本会先比对 requestId。只有当前线程持有的锁(Value 匹配)才会被删除,否则不做任何操作。这防止了业务执行时间过长导致锁自动过期后,该线程误删下一个线程持有的新锁。
⚠️ 注意事项(直接写业务代码的缺点)
三、在帮我生成一段不使用工具类,直接把操作 Redis 写到业务代码中,抢不到锁就延迟 300 ms 后重新执行的案例
这就为你提供一个在业务代码中直接实现 “抢不到锁则延迟重试” 逻辑的案例。
这个示例模拟了“用户领取优惠券”的场景。核心逻辑是:如果获取锁失败,线程会休眠 300 毫秒,然后递归调用自身方法重新尝试,直到成功获取锁并执行业务为止。
💡 业务逻辑说明
- 场景:领取优惠券(防止用户重复领取)。
- 重试机制:使用 Thread.sleep(300) 实现固定延迟重试。
- 递归调用:为了简化代码结构,使用了递归的方式来重新执行方法(生产环境如果并发极高,需注意递归深度,也可以改用 while 循环)。
🧩 完整代码示例
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class CouponController {
@Resource
private RedisTemplate<String, String> redisTemplate;
// 优惠券库存 Key
private static final String COUPON_STOCK_KEY = "coupon:stock:2026";
// 用户领取记录 Key (Set结构)
private static final String USER_RECEIVED_SET = "coupon:received:users";
// 锁的过期时间(秒)
private static final long LOCK_EXPIRE_SECONDS = 10;
// 重试延迟时间(毫秒)
private static final long RETRY_DELAY_MS = 300;
@GetMapping("/receive-coupon")
public String receiveCoupon(String userId) {
// 1. 生成唯一请求ID
String requestId = UUID.randomUUID().toString();
String lockKey = "lock:coupon:2026";
try {
// — 第一步:尝试获取分布式锁 —
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
LOCK_EXPIRE_SECONDS,
TimeUnit.SECONDS
);
// — 第二步:判断是否获取到锁 —
if (Boolean.TRUE.equals(isLocked)) {
// — 情况A:成功获取锁,执行业务逻辑 —
// 1. 检查用户是否已领取(幂等性检查)
Boolean isUserReceived = redisTemplate.opsForSet().isMember(USER_RECEIVED_SET, userId);
if (Boolean.TRUE.equals(isUserReceived)) {
return "您已经领取过啦!";
}
// 2. 查询库存
String stockStr = redisTemplate.opsForValue().get(COUPON_STOCK_KEY);
int stock = stockStr == null ? 0 : Integer.parseInt(stockStr);
if (stock > 0) {
// 3. 扣减库存 & 记录用户领取
redisTemplate.opsForValue().set(COUPON_STOCK_KEY, String.valueOf(stock – 1));
redisTemplate.opsForSet().add(USER_RECEIVED_SET, userId);
System.out.println("用户 " + userId + " 领取成功!剩余库存:" + (stock – 1));
return "领取成功!";
} else {
return "领取失败:优惠券已领完";
}
} else {
// — 情况B:获取锁失败,延迟后重试 —
System.out.println("用户 " + userId + " 获取锁失败,等待重试…");
Thread.sleep(RETRY_DELAY_MS); // 阻塞当前线程 300ms
// 递归调用,重新尝试获取锁和执行业务
// 注意:生产环境建议限制递归深度或改用 while 循环
return receiveCoupon(userId);
}
} catch (InterruptedException e) {
// 处理线程中断异常
Thread.currentThread().interrupt();
return "系统繁忙,请稍后再试";
} finally {
// 注意:只有在当前线程持有锁的情况下才释放
// 这里为了代码简洁,实际应确保只释放自己持有的锁(需结合 Lua 脚本)
// 如果业务执行时间过长导致锁过期,此处删除操作可能会误删别人的锁
// 生产环境建议在此处也加入 Lua 原子删除逻辑
if (Boolean.TRUE.equals(redisTemplate.hasKey(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
}
⚠️ 代码关键点与风险提示
递归重试的风险
- 代码中使用了 receiveCoupon(userId) 递归调用。
- 风险:如果并发量极大,或者锁一直无法获取,可能会导致栈溢出(Stack Overflow)。
- 建议:在生产环境中,建议使用 while 循环配合计数器来限制最大重试次数,例如最多重试 3 次。
锁释放的安全性
- 上述代码在 finally 中直接删除 Key,存在安全隐患。
- 风险:假设线程 A 执行业务耗时过长(超过 10 秒),锁自动过期了。此时线程 B 获取到了锁。如果线程 A 在结束时直接删除 Key,就会把线程 B 的锁删掉。
- 修复建议:如果要保证绝对安全,finally 块中必须使用 Lua 脚本,判断 Value 是当前线程的 requestId 才删除(参考我上一个回答中的 Lua 脚本写法)。
惊群效应
- 所有抢不到锁的请求都会在 300ms 后同时发起新一轮请求,这可能会给 Redis 带来瞬间的高并发压力。
- 优化建议:可以使用随机延迟(例如 Thread.sleep(new Random().nextInt(300)))来平滑请求。

