这篇博客将手把手带你实现一个生产级的资源配额管理系统。


基于 Spring Cloud Alibaba 的会员套餐资源配额管理系统实现

1. 前言:为什么要实现配额管理?

在 SaaS 系统或多租户系统中,我们经常需要根据用户购买的会员套餐(Package)来限制其可以创建的资源数量。例如:

  • 基础版:最多创建 100 个商品,5 个 H5 模板。
  • 专业版:最多创建 1000 个商品,50 个 H5 模板。

如果每个接口都写 if-else 校验,代码会变得极其臃肿且难以维护。本文将利用 Spring Cloud Alibaba (Nacos) + AOP + Redis 实现一个优雅、可配置的配额控制方案。


2. 技术架构图

  1. Nacos: 动态存储各等级会员的配额限制。
  2. Redis: 缓存当前已使用的数量(高性能计数),可选 Lua 脚本保证原子性。
  3. AOP: 拦截业务请求,解耦业务逻辑与配额校验。
  4. Strategy Pattern (策略模式): 适配不同微服务、不同表的统计逻辑。

3. 环境准备

  • Spring Cloud Alibaba (Nacos)
  • Spring Boot Starter AOP
  • Spring Data Redis

4. 手把手实现步骤

第一步:Nacos 配置动态配额

在 Nacos 配置中心创建 quota-config.yaml,方便运营随时调整限额而无需重启服务。

quota:
  packages:
    - code: "BASIC"
      limits:
        PRODUCT: 100
        TAG: 50
        H5_TEMPLATE: 5
    - code: "PRO"
      limits:
        PRODUCT: 1000
        TAG: 500
        H5_TEMPLATE: 50
第二步:定义核心组件

1. 资源类型枚举

public enum ResourceType {
    PRODUCT("商品"), 
    TAG("标签"), 
    H5_TEMPLATE("H5模板"), 
    TRACE_TEMPLATE("溯源模板");
    
    private final String desc;
    ResourceType(String desc) { this.desc = desc; }
}

2. 自定义业务注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QuotaLimit {
    ResourceType value(); // 指定检查哪种资源
}
第三步:读取 Nacos 配置

利用 @ConfigurationProperties 自动映射配置。

@Component
@ConfigurationProperties(prefix = "quota")
@Data
@RefreshScope // 支持 Nacos 动态刷新
public class MemberQuotaConfig {
    private List<PackageLimit> packages;

    @Data
    public static class PackageLimit {
        private String code;
        private Map<String, Integer> limits; // 资源名 -> 数量
    }

    public Integer getLimit(String packageCode, ResourceType type) {
        return packages.stream()
                .filter(p -> p.getCode().equals(packageCode))
                .map(p -> p.getLimits().get(type.name()))
                .findFirst()
                .orElse(0); // 找不到则默认为0,禁止创建
    }
}
第四步:实现统计策略(适配多模块)

由于商品和模板可能在不同微服务中,我们定义一个接口,由各业务模块自行实现计数逻辑。

public interface QuotaCounter {
    ResourceType getResourceType();
    int count(Long tenantId);
}

// 商品模块的实现示例
@Component
public class ProductCounter implements QuotaCounter {
    @Autowired private ProductMapper productMapper;

    @Override
    public ResourceType getResourceType() { return ResourceType.PRODUCT; }

    @Override
    public int count(Long tenantId) {
        return productMapper.selectCountByTenant(tenantId);
    }
}
第五步:核心 AOP 切面实现

这是校验发生的地方。

@Aspect
@Component
@Slf4j
public class QuotaAspect {

    @Autowired private MemberQuotaConfig quotaConfig;
    @Autowired private List<QuotaCounter> counters;

    @Around("@annotation(quotaLimit)")
    public Object doAround(ProceedingJoinPoint joinPoint, QuotaLimit quotaLimit) throws Throwable {
        // 1. 获取当前租户信息(从SecurityContext或Header获取)
        Long tenantId = SecurityUtils.getTenantId();
        String packageCode = SecurityUtils.getPackageCode(); 

        // 2. 获取配置限额
        Integer maxLimit = quotaConfig.getLimit(packageCode, quotaLimit.value());

        // 3. 寻找对应的计数器
        QuotaCounter counter = counters.stream()
                .filter(c -> c.getResourceType() == quotaLimit.value())
                .findFirst()
                .orElseThrow(() -> new RuntimeException("系统未配置该资源计数器"));

        // 4. 统计当前数量(建议此处引入Redis优化,见下文)
        int currentCount = counter.count(tenantId);

        // 5. 校验
        if (currentCount >= maxLimit) {
            log.warn("租户 {} 创建 {} 失败,超出限额 {}", tenantId, quotaLimit.value(), maxLimit);
            throw new BusinessException("您的套餐配额已用尽,请联系客服升级套餐");
        }

        return joinPoint.proceed();
    }
}

5. 进阶:性能与并发优化

在真实高并发环境下,直接 COUNT(*) 数据库性能较差,且无法绝对防止并发超限。

优化方案:Redis Lua 脚本
在切面中使用 Redis 维护计数器,利用 Lua 脚本保证“校验+自增”的原子性。

-- Lua 脚本:check_and_increment.lua
local current = redis.call('get', KEYS[1])
if current and tonumber(current) >= tonumber(ARGV[1]) then
    return -1 -- 返回-1表示超限
else
    return redis.call('incr', KEYS[1])
end

Java 调用代码:

public boolean tryAcquire(String redisKey, int limit) {
    DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
    Long result = redisTemplate.execute(script, Collections.singletonList(redisKey), String.valueOf(limit));
    return result != -1;
}

6. 实际业务使用

现在,在任何需要限制数量的 Service 方法上,只需要一行注解:

@Service
public class ProductServiceImpl implements ProductService {

    @Override
    @QuotaLimit(ResourceType.PRODUCT) // 自动校验商品配额
    @Transactional
    public void addProduct(ProductDTO dto) {
        // 业务逻辑
        productMapper.insert(product);
    }
}

@Service
public class TemplateServiceImpl implements TemplateService {

    @Override
    @QuotaLimit(ResourceType.H5_TEMPLATE) // 自动校验H5模板配额
    public void createH5(H5DTO dto) {
        // 业务逻辑
    }
}

7. 总结

通过这套方案,我们实现了:

  1. 高度解耦:业务代码完全不需要关心配额校验逻辑。
  2. 动态配置:通过 Nacos 修改配置后,全网实时生效。
  3. 易于扩展:增加一种受限资源只需要增加一个枚举项和一个 Counter 实现类。
  4. 性能可靠:引入 Redis 后,配额校验不再是系统瓶颈。

希望这篇教程能帮到你!如果有分布式事务相关的疑问,欢迎在评论区留言交流。


注:本文代码为核心逻辑演示,实际项目中请根据具体的权限框架(如 Spring Security 或 Shiro)调整获取用户信息的逻辑。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐