protection
这里介绍一下 protection 的使用。
功能介绍
主要包含在 wmeimob-spring-boot-starter-protection
jar 中。
支持的功能:
- 分布式缓存
- 幂等
- 分布式锁
分布式缓存
当并发高时,我们会把数据库的数据缓存到 redis,当 redis 数据过期时,我们业务代码通常会进行数据库查询然后再重新设置到 redis 中去。这是通常做法,没问题。但是再并发高时,会有大量线程漏到数据库里去查询数据,严重的可能会拖垮数据库,甚至最终缓存到 redis 的数据也不一定是准确的。为此,我们需要一个互斥锁,来保证当 redis 过期时,只有一个线程在重建缓存数据,其他线程都得等着。基于此,提供了一个 DistributeCacheService
接口。
java
/**
* 执行函数
*
* @param consumer 函数
*/
void execute(Consumer<StringRedisTemplate> consumer);
/**
* 执行函数
*
* @param function 函数
* @param <R> R
* @return R
*/
<R> R executeRtn(Function<StringRedisTemplate, R> function);
/**
* 设置缓存
*
* @param key key
* @param value value
*/
void set(String key, Object value);
/**
* 设置缓存
*
* @param key key
* @param value value
* @param timeout 缓存时长
* @param unit 时间单位
*/
void set(String key, Object value, Long timeout, TimeUnit unit);
/**
* 删除指定的 key
*
* @param key key
* @return boolean
*/
Boolean delete(String key);
/**
* 批量删除给定的 key 列表
*
* @param keys keys 列表
* @return long
*/
Long deleteList(List<String> keys);
/**
* 根据 key 获取缓存数据。
*
* @param key key
* @param type 缓存的实际对象类型
* @param <R> R
* @return list R
*/
<R> List<R> getList(String key, Class<R> type);
/**
* 根据 key 获取缓存数据。缓存不存在时,利用互斥锁安全设置缓存。
*
* @param key key
* @param type 缓存的实际对象类型
* @param fallback 缓存无数据时,执行的委托
* @param timeout 缓存时长
* @param unit 时间单位
* @param <R> R
* @return list R
*/
<R> List<R> getList(String key, Class<R> type, Supplier<List<R>> fallback, Long timeout, TimeUnit unit);
/**
* 根据 key 获取缓存数据。缓存不存在时,利用互斥锁安全设置缓存。
*
* @param key key
* @param type 缓存的实际对象类型
* @param fallback 缓存无数据时,执行的委托
* @param timeout 缓存时长
* @param unit 时间单位
* @param <R> R
* @return R
*/
<R> R get(String key, Class<R> type, Supplier<R> fallback, Long timeout, TimeUnit unit);
/**
* 根据 key 获取缓存数据。
*
* @param key key
* @param type 缓存的实际对象类型
* @param <R> R
* @return R
*/
<R> R get(String key, Class<R> type);
通过注释,我们可以看到 get 接口里面提供 fallback 为具有互斥锁能力的。可以安全使用。而没有 fallback 的普通 get 就和常规的一样,并不具备缓存失效后的补偿操作。 而且考虑到通用性,提供了 execute,executeRtn
回调接口来使用更多的 redis 能力。
同时也提供了一个配置类来进行属性配置:
java
/**
* 分布式缓存属性配置类
*
* @author mjyang
* @date 2023/8/4 18:14
*/
@Data
@ConfigurationProperties(prefix = "wmeimob.distribute.cache")
public class DistributeCacheProperties {
/**
* 空数据缓存时长,单位秒,默认60秒
*/
private Long cacheNullTtl = 60L;
/**
* 缓存空数据。默认 ”“
*/
private String cacheNullValue = StrUtil.EMPTY;
/**
* 线程休眠毫秒数。默认50毫秒
*/
private long cacheTheadSleepMilliSeconds = 50;
}
幂等
通过 @Idempotent
注解可以很容易的使用它。定义如下:
java
/**
* 幂等注解
*
* @author mjyang
* @date 2023/4/27 13:51
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 幂等的超时时间,默认为 1 秒
*/
int timeout() default 1;
/**
* 时间单位,默认为 SECONDS 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 提示信息,正在执行中的提示
*/
String message() default "";
/**
* 使用的 key 参数, spel 表达式使用
*/
String[] keyArgs() default {};
}
默认情况下,当不指定 keyArgs 参数时,默认以方法参数为 key,而后 md5。关键代码:
java
/**
* 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key,同时使用 MD5 进行压缩。
* 如果设置了 keyArgs,则使用 spel 表达式进行解析
*
* @author mjyang
* @date 2023/4/27 13:55
*/
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
private static final ExpressionParser PARSER = new SpelExpressionParser();
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
if (ArrayUtil.isEmpty(idempotent.keyArgs())) {
String methodName = joinPoint.getSignature().toString();
String args = StrUtil.join(StrUtil.COMMA, joinPoint.getArgs());
return SecureUtil.md5(methodName + args);
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
return resolverKey(joinPoint.getThis(), signature.getMethod(), idempotent.keyArgs(), joinPoint.getArgs());
}
private String resolverKey(Object root, Method method, String[] keyArgs, Object[] args) {
EvaluationContext context = new MethodBasedEvaluationContext(root, method, args, PARAMETER_NAME_DISCOVERER);
List<String> keyList = new ArrayList<>(keyArgs.length);
for (String keyArg : keyArgs) {
Object value = PARSER.parseExpression(keyArg).getValue(context);
keyList.add((String) value);
}
return StrUtil.join(StrUtil.DOT, keyList);
}
}
同时提供了一个简单的幂等配置类:
java
/**
* 幂等配置类
*
* @author mjyang
* @date 2023/4/27 14:13
*/
@Data
@ConfigurationProperties(prefix = "wmeimob.idempotent")
public class IdempotentProperties {
private String prefix;
}
可用于配置生成 redis key 时的前缀。
分布式锁
基于开源 lock4j 实现。默认使用如下依赖:
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>
</dependency>
在标品代码中均有使用。通过它提供的 @Lock4j 注解,可以很轻松的来使用。同时它也提供了一个配置类:
java
@Getter
@Setter
@ConfigurationProperties(prefix = "lock4j")
public class Lock4jProperties {
/**
* 过期时间 单位:毫秒
*/
private Long expire = 30000L;
/**
* 获取锁超时时间 单位:毫秒
*/
private Long acquireTimeout = 3000L;
/**
* 获取锁失败时重试时间间隔 单位:毫秒
*/
private Long retryInterval = 100L;
/**
* 默认执行器,不设置默认取容器第一个(默认注入顺序,redisson>redisTemplate>zookeeper)
*/
private Class<? extends LockExecutor> primaryExecutor;
/**
* 锁key前缀
*/
private String lockKeyPrefix = "lock4j";
}