Skip to content

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";
}