外观
锁与限流
锁保护同一资源的临界区,限流控制一段时间内的调用速率。它们通过独立管理器使用,不属于 StoreTemplate 的数据操作,也不会出现在模板的键扫描结果中。
作用域锁
注入 StoreLockManager,将需要共同保护的操作放入回调。以下类放在应用扫描范围内:
java
package example.store;
import com.own.component.store.api.lock.StoreLockManager;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Collection;
@Service
public class GuardedActionService {
private final StoreLockManager locks;
public GuardedActionService(StoreLockManager locks) {
this.locks = locks;
}
public void execute(String resourceKey, Runnable action) {
locks.withLock(resourceKey, Duration.ofSeconds(2), action);
}
public boolean tryExecuteAll(Collection<String> resourceKeys, Runnable action) {
return locks.tryWithLocks(resourceKeys, Duration.ofSeconds(2), action);
}
}| 方法 | 等待与返回 |
|---|---|
withLock(key, action) / withLocks(keys, action) | 一直等待,取得所有锁后执行 |
withLock(key, waitTime, action) / withLocks(keys, waitTime, action) | 在正数等待时长内获取,超时抛出 StoreLockException |
tryWithLock(key, waitTime, action) / tryWithLocks(keys, waitTime, action) | 取得并执行成功返回 true,超时返回 false 且不执行 |
with... 支持 Runnable 和 Supplier<T>,后者直接返回回调结果,包括 null;tryWith... 接受 Runnable。回调异常继续传播,锁仍按释放策略处理。限时等待被中断时恢复线程中断标记并抛出 StoreLockException。
waitTime 控制获取锁的等待时间,不限制回调运行时间,也不是锁租约。回调中的异步任务在返回后继续执行时,不再属于原临界区;需要保护的操作应在持锁线程内完成。
批量锁与共享范围
批量锁要求非空集合,先校验所有键并去重,再按稳定顺序加锁。Redis 按物理键排序,本地按锁分片再按逻辑键排序。只有全部成功后才执行一次回调;限时方法的预算由整组共享,任何一把锁超时都会释放本次此前取得的锁。
无事务时按获取顺序逆序释放;同步事务中可保留到事务完成。批量排序减少同批资源顺序不同导致的循环等待,但不能替代应用对嵌套调用、跨批次持锁顺序的约定。
| 维度 | 本地 | Redis |
|---|---|---|
| 共享范围 | 同一个锁管理器实例 | 相同 Redis 目标、数据库、命名空间与逻辑键 |
| 锁数量 | 固定分片,默认 1024 | 按逻辑键取得 Redisson 锁 |
| 键碰撞 | 不同键映射到同一分片会串行 | 不同物理键独立竞争 |
| 命名空间 | 管理器本身不接收命名空间 | 自动配置使用 own.store.namespace |
多实例互斥必须使用共享锁;仅把数据后端换成 Redis 而保留自定义本地锁并不充分。锁只协调遵守同一协议的调用方,不替代数据库唯一约束。多次写入的原子提交仍需要数据库事务。
方法级锁注解
Starter 自动提供切面,在受 Spring AOP 代理的方法上使用 @StoreLockAop:
java
package example.store;
import com.own.component.store.api.lock.StoreLockAop;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@Service
public class AnnotatedActionService {
@StoreLockAop(keys = {"resource:#{#p0}", "#p1"},
waitTime = 2, timeUnit = TimeUnit.SECONDS)
public String execute(String resourceId, Collection<String> relatedKeys) {
return "locked:" + resourceId;
}
}从另一个 Bean 调用该代理方法,切面会先解析全部键,取得整组锁后只调用一次方法。示例的 relatedKeys 是额外的完整逻辑键集合,#p0、#p1 按参数位置引用,不依赖编译时保留参数名。
| 声明 | 解释 |
|---|---|
"lookup:global" | 固定逻辑键 |
"resource:#{#p0}" | SpEL 模板,拼接动态值 |
"#p1" | 直接求值,数组或 Iterable 展开为多个键 |
"@beanName.method(#p0)"、"T(...).method(...)" | Bean 或类型表达式,返回值继续按键解析 |
标量结果转为字符串,最终键去重;null、空白键、空声明及整体没有解析出任何键都会失败。命名参数如 #resourceId 需要可发现的参数名,通用示例优先使用位置参数。表达式应由应用代码固定,不接受外部用户输入作为表达式文本。
waitTime 默认 -1 表示一直等待;正数为整组总预算,单位默认毫秒。零或小于 -1 在调用时抛出参数异常。与其他 Spring AOP 注解一样,手动 new 对象、自调用或无法被代理拦截的方法不会获得该锁保护。
事务中的释放时机
当 Spring 事务相关类存在、使用自动配置的释放策略,且获取锁时已经处于同步事务中,第一次物理锁会登记到事务同步回调,直到提交或回滚完成后才释放。同一事务通过同一个管理器再次获取相同键会逻辑重入,不增加物理加锁次数。
应用应先通过事务代理进入事务,再调用持锁操作。例如已有事务管理器并启用事务代理时,可在外层 @Transactional 方法中调用 StoreLockManager.withLock;或由事务服务调用另一个带锁注解的 Bean。不要依赖同一方法同时标注 @Transactional 与 @StoreLockAop 的默认切面顺序来保证事务先于加锁。
没有活动事务时,回调结束立即释放。事务活动但无法登记同步回调时,抛出 StoreLockException,不会静默提前释放后继续执行。手动创建管理器默认立即释放,除非显式传入其他 StoreLockReleaseStrategy。
该机制不跨线程,不覆盖响应式事务或独立 REQUIRES_NEW 事务的逻辑重入。回调结束后仍持锁的时间取决于整个事务长度;不要在其中进行无关的长时间等待。Store 写入本身不会加入数据库事务,缓存的提交边界见失效与事务。
按键限流
注入 StoreRateLimiterManager,每次使用前以相同策略配置,再获取许可:
java
package example.store;
import com.own.component.store.api.limit.RateLimitPolicy;
import com.own.component.store.api.limit.StoreRateLimiterManager;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
public class RequestRateService {
private final StoreRateLimiterManager limiters;
private final RateLimitPolicy policy =
new RateLimitPolicy(10, Duration.ofSeconds(1));
public RequestRateService(StoreRateLimiterManager limiters) {
this.limiters = limiters;
}
public boolean allow(String callerId) {
String key = "lookup:" + callerId;
limiters.configure(key, policy);
return limiters.tryAcquire(key, 1);
}
}configure 创建或更新策略。同一键用完全相同策略重复配置不会补满已消费许可;换成不同策略会重置旧消费状态。多个实例应采用一致策略,避免来回覆盖。
tryAcquire(key, permits) 立即尝试并返回是否获得许可;acquire(key, permits) 会阻塞到获得许可,没有超时参数,适合后台限速任务。策略许可数和单次申请数均需为正,申请量不应超过配置许可量;兼容本地与 Redis 的调用将单次申请控制在 Integer.MAX_VALUE 以内,并使用至少一毫秒的 interval。
限流后端差异
| 维度 | 本地 | Redis |
|---|---|---|
| 算法入口 | Guava RateLimiter,按每秒速率平滑发放 | Redisson RateType.OVERALL,共享许可状态 |
| 共享范围 | 当前管理器实例 | 相同 Redis 目标、数据库、命名空间与键 |
| interval | 正 Duration,换算为每秒速率 | 至少一毫秒,按毫秒处理 |
| 单次申请数 | 1..Integer.MAX_VALUE | 正 long |
| 生命周期 | 容量淘汰或一小时未访问后可能丢失配置 | 保留在 Redis,Store 未暴露限流器删除 API |
相同策略不意味着完全相同的瞬时突发行为,本地实现也不是严格的固定窗口计数。本地限流器被淘汰后,未重新配置就使用会失败;因此示例在每次调用前幂等配置,但容量淘汰仍意味着消费状态可能重建。使用 Redis 动态生成大量限流键时,应由应用规划键数量与清理方式。
组合锁与限流时要明确许可消费时机:先取许可再竞争锁,锁失败也已消费许可;先持锁再取许可,则限流判断属于临界区的一部分。组件不会自动退还许可,也没有内置的 HTTP 限流注解或响应封装。