欢迎光临
我们一直在努力

帮我生一个一段 Java springBoot 项目代码示例,使用 Redis 实现分布式锁;在帮我生成一段不使用工具类,直接把操作 Redis 写到业务代码中,抢不到锁就延迟 300 ms 后重新抢

一、帮我生一个一段 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 匹配)才会被删除,否则不做任何操作。这防止了业务执行时间过长导致锁自动过期后,该线程误删下一个线程持有的新锁。

⚠️ 注意事项(直接写业务代码的缺点)

  • 代码冗余:如果项目中有多个地方需要加锁,这段代码会被复制粘贴多次,维护困难。
  • 缺乏容错:没有重试机制(自旋),如果瞬间获取锁失败就直接返回了。
  • 锁续期缺失:如果业务逻辑执行时间超过了 LOCK_EXPIRE_SECONDS(例如 10 秒),锁会自动释放,但代码还在执行,此时可能会出现并发安全问题(建议生产环境使用 Redisson 解决此问题)。
  • 三、在帮我生成一段不使用工具类,直接把操作 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)))来平滑请求。
  • 赞(0)
    未经允许不得转载:171主机测评 » 帮我生一个一段 Java springBoot 项目代码示例,使用 Redis 实现分布式锁;在帮我生成一段不使用工具类,直接把操作 Redis 写到业务代码中,抢不到锁就延迟 300 ms 后重新抢
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址