🛠️ 一、准备工作(只需 2 分钟)

你需要:

  • JDK 17
  • IDEA
  • Redis(本地或 Docker)
  • JMeter(用于压测验证)

💡 没有 Redis? 用 Docker 一行命令启动:

docker run -d -p 6379:6379 --name redis-lock redis:7.0

💻 二、分布式锁核心原理(Redis 实现)

为什么需要分布式锁?

  • 单机锁(synchronized)只在当前 JVM 有效
  • 多台服务器部署,需要 Redis 这样的共享存储来协调

核心命令:SET NX EX

SET lock:product:123 "thread-1" NX EX 30
  • NX:key 不存在才设置(保证原子性)
  • EX 30:30秒过期(防死锁)
  • "thread-1":锁的值(用于解锁时验证)

🔐 三、实现分布式锁(完整代码)

步骤 1:添加依赖(pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

步骤 2:配置 Redis(application.yml)

spring:
  redis:
    host: localhost
    port: 6379
    database: 2
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

# 分布式锁配置
lock:
  default-expire: 30  # 锁默认过期时间(秒)
  retry-count: 3      # 获取锁失败时重试次数
  retry-delay: 100    # 重试间隔(毫秒)

步骤 3:创建分布式锁工具类(util/DistributedLock.java)

package com.example.lockdemo.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
public class DistributedLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${lock.default-expire:30}")
    private int defaultExpireSeconds;

    @Value("${lock.retry-count:3}")
    private int retryCount;

    @Value("${lock.retry-delay:100}")
    private long retryDelay;

    private final ThreadLocal<String> lockValue = new ThreadLocal<>();

    /**
     * 尝试获取分布式锁
     */
    public boolean tryLock(String lockKey, int expireSeconds) {
        String value = UUID.randomUUID().toString();
        lockValue.set(value);

        for (int i = 0; i < retryCount; i++) {
            // 使用 SET NX EX 命令原子性地设置锁
            Boolean result = redisTemplate.opsForValue()
                    .setIfAbsent(lockKey, value, expireSeconds, TimeUnit.SECONDS);

            if (Boolean.TRUE.equals(result)) {
                return true; // 获取锁成功
            }

            // 获取锁失败,等待后重试
            try {
                Thread.sleep(retryDelay);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return false; // 获取锁失败
    }

    /**
     * 释放分布式锁
     */
    public boolean releaseLock(String lockKey) {
        String value = lockValue.get();
        if (value == null) {
            return false;
        }

        // 使用 Lua 脚本保证原子性:只释放当前线程持有的锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "return redis.call('del', KEYS[1]) " +
                "else return 0 end";

        Long result = redisTemplate.execute(
                (connection) -> connection.eval(script.getBytes(), 1, 
                        lockKey.getBytes(), value.getBytes()),
                null);

        lockValue.remove();
        return result != null && result == 1;
    }

    /**
     * 获取锁(带默认过期时间)
     */
    public boolean tryLock(String lockKey) {
        return tryLock(lockKey, defaultExpireSeconds);
    }
}

🔍 关键点:

  • SET NX EX:原子性加锁
  • UUID:防止误删其他线程的锁
  • Lua 脚本:保证解锁的原子性

步骤 4:创建秒杀服务(service/SeckillService.java)

package com.example.lockdemo.service;

import com.example.lockdemo.util.DistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SeckillService {

    @Autowired
    private DistributedLock distributedLock;

    // 模拟商品库存(实际项目中应从数据库获取)
    private volatile int productStock = 10; // 10个商品用于秒杀

    /**
     * 执行秒杀(带分布式锁)
     */
    public String seckill(Long userId, Long productId) {
        String lockKey = "lock:seckill:" + productId;

        // 尝试获取分布式锁
        if (distributedLock.tryLock(lockKey, 10)) { // 10秒过期
            try {
                // 双重检查库存
                if (productStock <= 0) {
                    return "秒杀失败:库存不足";
                }

                // 扣减库存(模拟数据库操作)
                productStock--;
                System.out.println("用户 " + userId + " 秒杀成功,剩余库存:" + productStock);

                // 模拟业务处理时间
                Thread.sleep(100);

                return "秒杀成功!";
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return "秒杀失败:系统异常";
            } finally {
                // 释放锁
                distributedLock.releaseLock(lockKey);
            }
        } else {
            return "秒杀失败:获取锁失败,请重试";
        }
    }
}

步骤 5:创建控制器(controller/SeckillController.java)

package com.example.lockdemo.controller;

import com.example.lockdemo.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    /**
     * 秒杀接口
     */
    @PostMapping("/buy")
    public String seckill(@RequestParam Long userId, @RequestParam Long productId) {
        return seckillService.seckill(userId, productId);
    }

    /**
     * 获取库存
     */
    @GetMapping("/stock/{productId}")
    public String getStock(@PathVariable Long productId) {
        // 这里简化,实际应从数据库获取
        return "剩余库存: " + 10; // 模拟返回固定值,实际项目中应动态获取
    }
}

🧪 四、测试验证(JMeter 压测)

1. 启动项目

mvn spring-boot:run

2. JMeter 配置(模拟 100 并发)

  • 线程数:100
  • 循环次数:1
  • 请求路径:POST http://localhost:8080/api/seckill/buy
  • 参数:userId=1&productId=123

3. 预期结果

  • 10个用户秒杀成功
  • 90个用户返回“库存不足”或“获取锁失败”
  • 无超卖现象

✅ 控制台日志显示:剩余库存:0 → 秒杀失败:库存不足        

💡 五、Bonus:分布式锁常见坑点

坑 1:锁过期时间设置不当

  • 过短:业务还没执行完,锁就过期了 → 可能出现超卖
  • 过长:某个线程挂了,锁一直不释放 → 系统阻塞
  • 建议:业务执行时间的 2-3 倍

坑 2:锁的粒度过粗

  • 错误:用全局锁锁整个系统
  • 正确:按业务粒度锁,如 lock:seckill:product:123

坑 3:忘记释放锁

  • 必须:在 finally 块中释放锁
  • 使用:try-with-resources 或 AOP 切面自动释放

坑 4:Redis 单点故障

  • 生产建议:使用 Redis Cluster 或哨兵模式
  • 考虑:引入 Redission 等成熟的分布式锁框架

🚀 六、进阶:AOP 自动加锁解锁

// 创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key() default "";
    int expireSeconds() default 30;
}

// AOP 切面
@Aspect
@Component
public class DistributedLockAspect {

    @Autowired
    private DistributedLock lockUtil;

    @Around("@annotation(lockAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint, com.example.lockdemo.annotation.DistributedLock lockAnnotation) throws Throwable {
        String lockKey = lockAnnotation.key();
        if (lockKey.isEmpty()) {
            // 可以根据方法名和参数生成锁 key
            lockKey = "lock:" + joinPoint.getSignature().getName();
        }

        if (lockUtil.tryLock(lockKey, lockAnnotation.expireSeconds())) {
            try {
                return joinPoint.proceed();
            } finally {
                lockUtil.releaseLock(lockKey);
            }
        } else {
            throw new RuntimeException("获取锁失败");
        }
    }
}

// 使用注解
@DistributedLock(key = "seckill:123", expireSeconds = 10)
public String seckill(Long userId, Long productId) {
    // 业务逻辑...
}

💬 七、写在最后

分布式锁,看似高深,实则是 Redis 命令 + 原子性 + 防死锁 的组合。

记住三句话:

  • SET NX EX 是核心,保证原子性
  • UUID + Lua 脚本,防止误删锁
  • 锁粒度要细,过期时间要合理

学会分布式锁,你就掌握了 高并发场景下的数据一致性保障!

        

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐