接口防重Redis分布式锁

wen java案例 1

本文目录导读:

接口防重Redis分布式锁

  1. 核心原理
  2. 完整实现方案
  3. 高级实现(Redisson)
  4. 关键设计要点
  5. 性能优化建议
  6. 监控和运维

我来详细讲解如何使用Redis分布式锁实现接口防重,这是企业级项目中非常常见的解决方案。

核心原理

为什么需要接口防重?

  • 用户重复提交(快速点击、网络重试)
  • 接口幂等性要求
  • 防止数据重复处理

Redis分布式锁优势

  • 原子性操作
  • 自动过期(避免死锁)
  • 高性能
  • 支持分布式场景

完整实现方案

1 基础依赖(Spring Boot + Redis)

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

2 Redis配置

# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    timeout: 5000
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

3 核心锁工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLockUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * 获取分布式锁(最简实现)
     */
    public boolean tryLock(String key, String value, long expireTime) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
    /**
     * 释放锁(使用Lua脚本保证原子性)
     */
    public void unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) " +
                       "else return 0 end";
        redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Collections.singletonList(key),
            value
        );
    }
}

4 注解方式(推荐)

自定义注解

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreventDuplicateSubmit {
    // 锁的key前缀
    String prefix() default "";
    // 过期时间(秒)
    long expireTime() default 10;
    // 提示信息
    String message() default "请求处理中,请勿重复提交";
}

AOP切面实现

import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.UUID;
@Aspect
@Component
@Order(1)  // 高优先级
public class PreventDuplicateAspect {
    @Autowired
    private RedisLockUtil redisLockUtil;
    @Around("@annotation(com.example.annotation.PreventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        PreventDuplicateSubmit annotation = method.getAnnotation(PreventDuplicateSubmit.class);
        // 生成唯一锁Key
        String lockKey = generateLockKey(joinPoint, annotation);
        String lockValue = UUID.randomUUID().toString();
        try {
            // 尝试获取锁
            boolean locked = redisLockUtil.tryLock(lockKey, lockValue, annotation.expireTime());
            if (!locked) {
                // 重复提交,返回提示信息
                throw new RuntimeException(annotation.message());
            }
            // 执行业务逻辑
            return joinPoint.proceed();
        } finally {
            // 业务执行完毕后释放锁(注意:不是简单删除,要校验value)
            redisLockUtil.unlock(lockKey, lockValue);
        }
    }
    /**
     * 生成锁Key(结合请求参数和用户信息)
     */
    private String generateLockKey(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit annotation) {
        StringBuilder sb = new StringBuilder("prevent:duplicate:");
        // 1. 添加自定义前缀
        if (!annotation.prefix().isEmpty()) {
            sb.append(annotation.prefix()).append(":");
        }
        // 2. 添加用户ID(从请求中获取)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String userId = request.getHeader("userId");
            if (userId != null) {
                sb.append("user:").append(userId).append(":");
            }
        }
        // 3. 添加请求参数(建议只取关键参数)
        Object[] args = joinPoint.getArgs();
        if (args.length > 0) {
            sb.append("params:").append(JSON.toJSONString(args));
        }
        return sb.toString();
    }
}

5 业务使用示例

@RestController
@RequestMapping("/order")
public class OrderController {
    @PostMapping("/create")
    @PreventDuplicateSubmit(prefix = "order_create", expireTime = 5, message = "订单正在处理中")
    public Result createOrder(@RequestBody OrderDTO orderDTO) {
        // 业务逻辑
        return Result.success("订单创建成功");
    }
}

高级实现(Redisson)

如果希望使用更成熟的方案,推荐Redisson:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.22.0</version>
</dependency>
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedissonDistributedLock {
    @Autowired
    private RedissonClient redissonClient;
    /**
     * 使用Redisson实现分布式锁
     */
    public <T> T executeWithLock(String lockKey, long waitTime, long leaseTime, 
                                  TimeUnit unit, LockSupplier<T> supplier) {
        RLock lock = redissonClient.getLock(lockKey);
        boolean locked = false;
        try {
            // 尝试获取锁(等待时间,释放时间,时间单位)
            locked = lock.tryLock(waitTime, leaseTime, unit);
            if (!locked) {
                throw new RuntimeException("系统繁忙,请稍后再试");
            }
            return supplier.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取锁被中断", e);
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    @FunctionalInterface
    public interface LockSupplier<T> {
        T get();
    }
}

使用示例:

@PostMapping("/payment")
public Result payment(@RequestBody PaymentDTO paymentDTO) {
    String lockKey = "payment:" + paymentDTO.getOrderId();
    return redissonDistributedLock.executeWithLock(
        lockKey,
        3,      // 等待时间
        10,     // 释放时间
        TimeUnit.SECONDS,
        () -> {
            // 业务逻辑
            return Result.success("支付成功");
        }
    );
}

关键设计要点

1 锁Key设计原则

完整结构:prefix:userId:methodName:params[:uniqueId]
示例:
- order:create:user123:{"orderId":"ORD001"}
- payment:user456:payment:{"paymentId":"PAY001","amount":100}
- submit:user789:submit:{"formId":"FORM001"}

2 常见问题处理

// 1. Redis锁续期问题(复杂业务)
public boolean tryLockWithRenew(String key, String value, long expireTime) {
    Boolean result = redisTemplate.opsForValue()
        .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(result)) {
        // 启动一个后台线程定期续期
        renewExpire(key, value, expireTime);
    }
    return Boolean.TRUE.equals(result);
}
// 2. 锁粒度控制(尽可能细)
// 错误:全局锁
String badKey = "submit:all";
// 正确:细粒度锁
String goodKey = "submit:user:" + userId + ":order:" + orderId;

3 异常情况处理

@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
    if (e.getMessage().contains("请勿重复提交")) {
        return Result.error(400, e.getMessage());
    }
    // 其他异常处理
    return Result.error(500, "系统异常");
}

性能优化建议

  1. 锁过期时间要合理

    • 设置太短:业务未完成锁就释放
    • 设置太长:影响并发性能
    • 建议:平均业务处理时间的3倍
  2. 使用连接池

    • 避免频繁创建/销毁连接
    • 设置合理的连接池参数
  3. 异步释放锁

    • 对于异步处理的业务
    • 使用MQ等机制在业务完成后释放

监控和运维

// 添加监控指标
@Component
public class LockMonitor {
    @Autowired
    private RedisTemplate redisTemplate;
    // 定期清理过期锁(兜底方案)
    @Scheduled(cron = "0 0/1 * * * ?")  // 每分钟执行一次
    public void cleanupExpiredLocks() {
        Set<String> keys = redisTemplate.keys("prevent:duplicate:*");
        if (keys != null && !keys.isEmpty()) {
            for (String key : keys) {
                Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
                if (ttl != null && ttl <= 0) {
                    redisTemplate.delete(key);  // 清理已过期但未释放的锁
                }
            }
        }
    }
}

Redis分布式锁实现接口防重是成熟且高效的方案,关键点在于:

  • 原子性:加锁和释放锁要保证原子操作
  • 安全性:只能释放自己加的锁
  • 可用性:考虑Redis宕机等异常情况
  • 性能:合理设置过期时间,控制锁粒度

建议配合注解和AOP使用,这样可以非侵入式地实现防重逻辑,便于维护和扩展。

抱歉,评论功能暂时关闭!