前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Cache优化

Spring Cache优化

作者头像
用户4283147
发布2022-10-27 15:41:58
7020
发布2022-10-27 15:41:58
举报
文章被收录于专栏:对线JAVA面试对线JAVA面试

缓存是web项目不可或缺的一部分,通过缓存能够降低服务器数据库压力,提高服务器的稳定性及响应速度。

spring cache

spring cache是spring框架自带的一套缓存框架,其具有多种实现,比较常用的是基于Redis的实现,其核心注解有 @CacheConfig@Cacheable@CachePut@CacheEvict,不熟悉用法的可以参考官方文档,有很详细的说明,https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#spring-integration 。建议大家有时间还是多看看spring官方文档,比从网上找文章看高效多了。 这里主要介绍一下@CacheConfig这个注解,此注解有四个属性,cacheNames 用于指定缓存名字,可以按照在缓存中按模块保存,keyGenerator 缓存键生成器,如果指定了缓存键则忽略,cacheManager 由spring管理的缓存管理器的名字,如果没有指定则采用默认的缓存管理器,cacheResolver。 spring cache具有极高的易用性,在保存缓存时能够根据Spring EL表达式自由定制缓存键,但是spring cache在使用过程中有两点缺陷:

  • 在使用@CacheEvict时,如果指定了allEntries=true,在从Redis中删除缓存时使用的是 keys指令,keys指令时间复杂度是O(N),如果缓存数量较大会产生明显的阻,因此在生产环境中Redis会禁用这个指令,导致报错。 看下DefaultRedisCacheWriter的clean方法:
代码语言:javascript
复制
@Override
    public void clean(String name, byte[] pattern) {

        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(pattern, "Pattern must not be null!");

        execute(name, connection -> {

            boolean wasLocked = false;

            try {

                if (isLockingCacheWriter()) {
                    doLock(name, connection);
                    wasLocked = true;
                }
                                //keys 指令
                byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
                        .toArray(new byte[0][]);

                if (keys.length > 0) {
                    statistics.incDeletesBy(name, keys.length);
                    connection.del(keys);
                }
            } finally {

                if (wasLocked && isLockingCacheWriter()) {
                    doUnlock(name, connection);
                }
            }

            return "OK";
        });
    }

  • 在通过cacheManager属性指定缓存管理器时,如果不指定则采用全局的声明的缓存管理器,无法调整缓存的过期时间,而如果指定了缓存管理器则必须要手动创建一个缓存管理器且需要交给spring托管,无法动态指定缓存管理器。 这里对上述两个缺陷进行了修改,一是通过scan指令替代keys指令,虽然scan指令的时间复杂度也是O(N),但是其通过指定游标和count能够分批执行,不会导致长时间的阻塞;二是在项目启动后,通过扫描注解动态生成cacheManager,能够满足不同缓存模块指定不同的缓存时间的需求,且无需手动创建RedisCacheManager。

重写DefaultRedisCacheWriter

DefaultRedisCacheWriter是spring cache提供的默认的Redis缓存写出器,其内部封装了缓存增删改查等逻辑,但是由于其不是public修饰的,因此重写了一个Redis缓存写出器,大部分代码均与DefaultRedisCacheWriter相同,只有clean方法做了修改。

代码语言:javascript
复制
package com.cube.share.cache.writer;

import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.data.redis.cache.CacheStatistics;
import org.springframework.data.redis.cache.CacheStatisticsCollector;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * @author poker.li
 * @date 2021/7/17 13:20
 * <p>
 * 自定义的RedisCacheWriter的实现,重写DefaultRedisCacheWriter的clear()方法,使用scan指令替换keys指令
 * <p>
 */
@SuppressWarnings({"WeakerAccess", "unused"})
public class IRedisCacheWriter implements RedisCacheWriter {

    private final RedisConnectionFactory connectionFactory;
    private final Duration sleepTime;
    private final CacheStatisticsCollector statistics;

    /**
     * @param connectionFactory must not be {@literal null}.
     */
    public IRedisCacheWriter(RedisConnectionFactory connectionFactory) {
        this(connectionFactory, Duration.ZERO);
    }

    /**
     * @param connectionFactory must not be {@literal null}.
     * @param sleepTime         sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
     *                          to disable locking.
     */
    public IRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
        this(connectionFactory, sleepTime, CacheStatisticsCollector.none());
    }

    /**
     * @param connectionFactory        must not be {@literal null}.
     * @param sleepTime                sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
     *                                 to disable locking.
     * @param cacheStatisticsCollector must not be {@literal null}.
     */
    public IRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime,
                             CacheStatisticsCollector cacheStatisticsCollector) {

        Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
        Assert.notNull(sleepTime, "SleepTime must not be null!");
        Assert.notNull(cacheStatisticsCollector, "CacheStatisticsCollector must not be null!");

        this.connectionFactory = connectionFactory;
        this.sleepTime = sleepTime;
        this.statistics = cacheStatisticsCollector;
    }

    @Override
    public CacheStatistics getCacheStatistics(String cacheName) {
        return statistics.getCacheStatistics(cacheName);
    }

    @Override
    public void clearStatistics(String name) {
        statistics.reset(name);
    }

    @Override
    public RedisCacheWriter withStatisticsCollector(CacheStatisticsCollector cacheStatisticsCollector) {
        return new IRedisCacheWriter(connectionFactory, sleepTime, cacheStatisticsCollector);
    }

    @Override
    public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {

        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(key, "Key must not be null!");
        Assert.notNull(value, "Value must not be null!");

        execute(name, connection -> {

            if (shouldExpireWithin(ttl)) {
                connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
            } else {
                connection.set(key, value);
            }

            return "OK";
        });
    }

    @Override
    public byte[] get(String name, byte[] key) {

        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(key, "Key must not be null!");

        return execute(name, connection -> connection.get(key));
    }

    @Override
    public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) {

        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(key, "Key must not be null!");
        Assert.notNull(value, "Value must not be null!");

        return execute(name, connection -> {

            if (isLockingCacheWriter()) {
                doLock(name, connection);
            }

            try {
                //noinspection ConstantConditions
                if (connection.setNX(key, value)) {

                    if (shouldExpireWithin(ttl)) {
                        connection.pExpire(key, ttl.toMillis());
                    }
                    return null;
                }

                return connection.get(key);
            } finally {

                if (isLockingCacheWriter()) {
                    doUnlock(name, connection);
                }
            }
        });
    }

    @Override
    public void remove(String name, byte[] key) {

        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(key, "Key must not be null!");

        execute(name, connection -> connection.del(key));
    }

    @Override
    public void clean(String name, byte[] pattern) {

        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(pattern, "Pattern must not be null!");

        execute(name, connection -> {

            boolean wasLocked = false;

            try {

                if (isLockingCacheWriter()) {
                    doLock(name, connection);
                    wasLocked = true;
                }

                //使用scan命令代替keys命令
                Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(new String(pattern)).count(1000).build());
                Set<byte[]> byteSet = new HashSet<>();
                while (cursor.hasNext()) {
                    byteSet.add(cursor.next());
                }

                byte[][] keys = byteSet.toArray(new byte[0][]);

                if (keys.length > 0) {
                    connection.del(keys);
                }
            } finally {

                if (wasLocked && isLockingCacheWriter()) {
                    doUnlock(name, connection);
                }
            }

            return "OK";
        });
    }

    /**
     * Explicitly set a write lock on a cache.
     *
     * @param name the name of the cache to lock.
     */
    void lock(String name) {
        execute(name, connection -> doLock(name, connection));
    }

    /**
     * Explicitly remove a write lock from a cache.
     *
     * @param name the name of the cache to unlock.
     */
    void unlock(String name) {
        executeLockFree(connection -> doUnlock(name, connection));
    }

    private Boolean doLock(String name, RedisConnection connection) {
        return connection.setNX(createCacheLockKey(name), new byte[0]);
    }

    @SuppressWarnings("UnusedReturnValue")
    private Long doUnlock(String name, RedisConnection connection) {
        return connection.del(createCacheLockKey(name));
    }

    private boolean doCheckLock(String name, RedisConnection connection) {
        //noinspection ConstantConditions
        return connection.exists(createCacheLockKey(name));
    }

    /**
     * @return {@literal true} if {@link RedisCacheWriter} uses locks.
     */
    private boolean isLockingCacheWriter() {
        return !sleepTime.isZero() && !sleepTime.isNegative();
    }

    private <T> T execute(String name, Function<RedisConnection, T> callback) {

        try (RedisConnection connection = connectionFactory.getConnection()) {

            checkAndPotentiallyWaitUntilUnlocked(name, connection);
            return callback.apply(connection);
        }
    }

    private void executeLockFree(Consumer<RedisConnection> callback) {

        try (RedisConnection connection = connectionFactory.getConnection()) {
            callback.accept(connection);
        }
    }

    private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {

        if (!isLockingCacheWriter()) {
            return;
        }

        try {

            while (doCheckLock(name, connection)) {
                Thread.sleep(sleepTime.toMillis());
            }
        } catch (InterruptedException ex) {

            // Re-interrupt current thread, to allow other participants to react.
            Thread.currentThread().interrupt();

            throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
                    ex);
        }
    }

    private static boolean shouldExpireWithin(@Nullable Duration ttl) {
        return ttl != null && !ttl.isZero() && !ttl.isNegative();
    }

    private static byte[] createCacheLockKey(String name) {
        return (name + "~lock").getBytes(StandardCharsets.UTF_8);
    }
}

自定义缓存注解替代spring cache的注解

  • @ICacheConfig
代码语言:javascript
复制
package com.cube.share.cache.anonotation;

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @author poker.li
 * @date 2021/7/17 16:08
 * <p>
 * 基于{@link org.springframework.cache.annotation.CacheConfig}提供的缓存配置注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CacheConfig
@Inherited
public @interface ICacheConfig {

    /**
     * 缓存前缀名,通过该属性指定不同模块缓存的存放位置,
     * 在Redis中分块展示,对于指定的缓存键key="11235813",存在Redis的实际键为 "sysUser::11235813"
     */
    @AliasFor(annotation = CacheConfig.class, attribute = "cacheNames")
    String[] cacheNames() default {};

    /**
     * 缓存键生成器
     */
    @AliasFor(annotation = CacheConfig.class, attribute = "keyGenerator")
    String keyGenerator() default "";

    /**
     * 缓存管理器,如果没有指定则采用默认的缓存管理器,如果需要自定义缓存的过期时间。
     * 则必须指定该属性,并且要使该属性唯一,这样能创建一个新的RedisCacheManager(bean的名字就是cacheManager)
     */
    @AliasFor(annotation = CacheConfig.class, attribute = "cacheManager")
    String cacheManager() default "";

    @AliasFor(annotation = CacheConfig.class, attribute = "cacheResolver")
    String cacheResolver() default "";

    /**
     * 是否允许缓存存入null
     */
    boolean allowCachingNullValues() default false;

    /**
     * 缓存的有效期限,如果值小于等于0则表示永久保存
     */
    int expire() default 8;

    /**
     * 缓存过期的时间单位
     */
    TimeUnit timeUnit() default TimeUnit.HOURS;

    /**
     * 设置是否兼容事务,
     * 默认是true,只在事务成功提交后才会进行缓存的put/evict操作
     */
    boolean transactionAware() default true;
}

  • @ICache
代码语言:javascript
复制
package com.cube.share.cache.anonotation;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * @author poker.li
 * @date 2021/7/17 17:08
 * <p>
 * 基于{@link Cacheable}实现的缓存存放注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@SuppressWarnings("SpringCacheNamesInspection")
@Cacheable
public @interface ICache {

    /**
     * 缓存的名字(缓存键的前缀),例如,指定为"sysUser",
     * 对于指定的缓存键key="11235813",存在Redis的实际键为 "sysUser::11235813"
     */
    @AliasFor(annotation = Cacheable.class, attribute = "value")
    String[] value() default {};

    /**
     * 缓存的名字(缓存键的前缀)
     */
    @AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
    String[] cacheNames() default {};

    /**
     * 缓存键
     */
    @AliasFor(annotation = Cacheable.class, attribute = "key")
    String key() default "";

    /**
     * 缓存键生成器
     */
    @AliasFor(annotation = Cacheable.class, attribute = "keyGenerator")
    String keyGenerator() default "";

    /**
     * 缓存管理器,如果没有指定则采用默认的缓存管理器
     */
    @AliasFor(annotation = Cacheable.class, attribute = "cacheManager")
    String cacheManager() default "";

    @AliasFor(annotation = Cacheable.class, attribute = "cacheResolver")
    String cacheResolver() default "";

    /**
     * 判断是否放入缓存的条件,使用Spring EL表达式
     */
    @AliasFor(annotation = Cacheable.class, attribute = "condition")
    String condition() default "";

    /**
     * 在方法执行结束后,根据方法的执行结果执行是否需要放入缓存,例如
     * unless = "#result != null",表示仅当方法执行结果不为null时才放入缓存
     */
    @AliasFor(annotation = Cacheable.class, attribute = "unless")
    String unless() default "";

    /**
     * 是否需要同步调用,如果设置为true,具有相同key的多次调用串行执行
     */
    @AliasFor(annotation = Cacheable.class, attribute = "sync")
    boolean sync() default false;

}

  • @ICachePut
代码语言:javascript
复制
package com.cube.share.cache.anonotation;

import org.springframework.cache.annotation.CachePut;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * @author poker.li
 * @date 2021/7/17 17:33
 * <p>
 * 基于{@link CachePut}提供的缓存更新注解
 */
@SuppressWarnings("SpringCacheNamesInspection")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CachePut
public @interface ICachePut {

    /**
     * 缓存的名字(缓存键的前缀),例如,指定为"sysUser",
     * 对于指定的缓存键key="11235813",存在Redis的实际键为 "sysUser::11235813"
     */
    @AliasFor(annotation = CachePut.class, attribute = "value")
    String[] value() default {};

    /**
     * 缓存的名字(缓存键的前缀)
     */
    @AliasFor(annotation = CachePut.class, attribute = "cacheNames")
    String[] cacheNames() default {};

    /**
     * 缓存键
     */
    @AliasFor(annotation = CachePut.class, attribute = "key")
    String key() default "";

    /**
     * 缓存管理器
     */
    @AliasFor(annotation = CachePut.class, attribute = "cacheManager")
    String cacheManager() default "";

    @AliasFor(annotation = CachePut.class, attribute = "cacheResolver")
    String cacheResolver() default "";

    /**
     * 判断是否放入缓存的条件,使用Spring EL表达式
     */
    @AliasFor(annotation = CachePut.class, attribute = "condition")
    String condition() default "";

    /**
     * 在方法执行结束后,根据方法的执行结果执行是否需要放入缓存,例如
     * unless = "#result != null",表示仅当方法执行结果不为null时才放入缓存
     */
    @AliasFor(annotation = CachePut.class, attribute = "unless")
    String unless() default "";
}

  • @ICacheEvict
代码语言:javascript
复制
package com.cube.share.cache.anonotation;

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * @author cube.li
 * @date 2021/7/17 21:23
 * @description {@link org.springframework.cache.annotation.CacheEvict}提供的缓存清除注解
 */
@SuppressWarnings({"SingleElementAnnotation", "SpringCacheNamesInspection"})
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CacheEvict
public @interface ICacheEvict {

    /**
     * 缓存的名字(缓存键的前缀),例如,指定为"sysUser",
     * 对于指定的缓存键key="11235813",存在Redis的实际键为 "sysUser::11235813"
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "value")
    String[] value() default {};

    /**
     * 缓存的名字(缓存键的前缀)
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "cacheNames")
    String[] cacheNames() default {};

    /**
     * 缓存键
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "key")
    String key() default "";

    /**
     * 缓存管理器
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "cacheManager")
    String cacheManager() default "";

    @AliasFor(annotation = CacheEvict.class, attribute = "cacheResolver")
    String cacheResolver() default "";

    /**
     * 判断是否放入缓存的条件,使用Spring EL表达式
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "condition")
    String condition() default "";

    /**
     * 是否删除缓存中所有的记录(当前指定的cacheNames下),
     * 如果设置为false,仅删除设定的key
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "allEntries")
    boolean allEntries() default false;

    /**
     * 是否在方法调用前删除缓存,默认是false,仅当方法成功执行后才删除缓存,
     * 如果设定为true,则在调用前即删除缓存,无论方法最终是否调用成功
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "beforeInvocation")
    boolean beforeInvocation() default false;

}

上面的四个注解实际上只有@ICacheConfig对原生注解@CacheConfig做了再封装,增加了三个属性,另外三个注解只是对spring cache对应的原生注解起了个别名,以后可能会有拓展的需要。

指定默认的RedisCacheManager配置

代码语言:javascript
复制
package com.cube.share.cache.config;

import com.cube.share.cache.writer.IRedisCacheWriter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @author poker.li
 * @date 2021/7/17 14:07
 * <p>
 * Redis配置
 */
@Configuration
@ConditionalOnProperty(prefix = "ICache", name = "enabled", havingValue = "true")
@EnableCaching
public class RedisCacheConfig {

    @Bean
    @Primary
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(8))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .cacheWriter(redisCacheWriter(redisConnectionFactory));
        return builder.transactionAware()
                .cacheDefaults(cacheConfiguration).build();
    }

    @Bean
    public RedisCacheWriter redisCacheWriter(RedisConnectionFactory redisConnectionFactory) {
        return new IRedisCacheWriter(redisConnectionFactory);
    }
}

如果没有指定RedisCacheManager,则采用上述配置的RedisCacheManger作为默认的缓存管理器,其指定了缓存的过期时间是8个小时。

动态生成RedisCacheManager并交给Spring托管

代码语言:javascript
复制
package com.cube.share.cache.processor;

import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.constant.RedisCacheConstant;
import com.cube.share.cache.writer.IRedisCacheWriter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author poker.li
 * @date 2021/7/20 11:50
 */
@Component
@SuppressWarnings("unused")
@ConditionalOnProperty(prefix = "ICache", name = "enabled", havingValue = "true")
public class CacheManagerProcessor implements BeanFactoryAware, ApplicationContextAware {

    private DefaultListableBeanFactory beanFactory;

    private ApplicationContext applicationContext;

    @Resource(type = IRedisCacheWriter.class)
    private IRedisCacheWriter redisCacheWriter;

    private Set<String> cacheManagerNameSet = new HashSet<>();

    @PostConstruct
    public void registerCacheManager() {
        cacheManagerNameSet.add(RedisCacheConstant.DEFAULT_CACHE_MANAGER_BEAN_NAME);
        //获取所有使用ICacheConfig注解的Bean
        Map<String, Object> annotatedBeanMap = this.applicationContext.getBeansWithAnnotation(ICacheConfig.class);
        //获取所有Bean上的ICacheConfig注解
        Set<Map.Entry<String, Object>> entrySet = annotatedBeanMap.entrySet();
        for (Map.Entry<String, Object> entry : entrySet) {
            Object instance = entry.getValue();
            ICacheConfig iCacheConfig = instance.getClass().getAnnotation(ICacheConfig.class);
            registerRedisCacheManagerBean(iCacheConfig);
        }
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (DefaultListableBeanFactory) beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    private void registerRedisCacheManagerBean(ICacheConfig annotation) {
        final String cacheManagerName = annotation.cacheManager();
        if (StringUtils.isBlank(cacheManagerName)) {
            return;
        }

        if (!cacheManagerNameSet.contains(cacheManagerName)) {
            RootBeanDefinition definition = new RootBeanDefinition(RedisCacheManager.class);
            ConstructorArgumentValues argumentValues = new ConstructorArgumentValues();
            argumentValues.addIndexedArgumentValue(0, redisCacheWriter);
            argumentValues.addIndexedArgumentValue(1, getRedisCacheConfiguration(annotation));
            definition.setConstructorArgumentValues(argumentValues);
            beanFactory.registerBeanDefinition(cacheManagerName, definition);

            if (annotation.transactionAware()) {
                //事务
                RedisCacheManager currentManager = applicationContext.getBean(cacheManagerName, RedisCacheManager.class);
                currentManager.setTransactionAware(true);
            }
        }
    }

    @NonNull
    private RedisCacheConfiguration getRedisCacheConfiguration(ICacheConfig annotation) {
        final boolean allowCachingNullValues = annotation.allowCachingNullValues();
        final int expire = annotation.expire();
        final TimeUnit timeUnit = annotation.timeUnit();
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        if (!allowCachingNullValues) {
            config = config.disableCachingNullValues();
        }
        if (expire > 0) {
            Duration duration = getDuration(expire, timeUnit);
            config = config.entryTtl(duration);
        }
        return config;
    }

    @NonNull
    private RedisCacheManager getRedisCacheManager(ICacheConfig annotation) {
        return RedisCacheManager.RedisCacheManagerBuilder
                .fromCacheWriter(redisCacheWriter)
                .transactionAware()
                .cacheDefaults(getRedisCacheConfiguration(annotation))
                .build();
    }

    @NonNull
    private Duration getDuration(int expire, TimeUnit timeUnit) {
        switch (timeUnit) {
            case DAYS:
                return Duration.ofDays(expire);
            case HOURS:
                return Duration.ofHours(expire);
            case MINUTES:
                return Duration.ofMinutes(expire);
            case SECONDS:
                return Duration.ofSeconds(expire);
            case MILLISECONDS:
                return Duration.ofMillis(expire);
            case NANOSECONDS:
                return Duration.ofNanos(expire);
            default:
                throw new IllegalArgumentException("Illegal Redis Cache Expire TimeUnit!");
        }
    }
}

这里在容器启动后扫描@ICacheConfig注解修饰的Bean,并根据指定的cacheManager属性生成对应的RedisCacheManager管理器。

测试

代码语言:javascript
复制
package com.cube.share.cache.service;

import com.cube.share.cache.anonotation.ICache;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.anonotation.ICacheEvict;
import com.cube.share.cache.anonotation.ICachePut;
import com.cube.share.cache.model.SysDepartment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @author poker.li
 * @date 2021/7/20 14:35
 */
@Service
@Slf4j
@ICacheConfig(cacheNames = "sysDepartment", cacheManager = "sysDepartmentCacheManager", expire = -1)
public class SysDepartmentService {

    @ICache(key = "#a0")
    public SysDepartment getById(Integer id) {
        return new SysDepartment(id, "部门名字" + id, "部门别名" + id);
    }

    @ICachePut(key = "#p0?.id", condition = "#p0 != null")
    public SysDepartment update(SysDepartment sysDepartment) {
        return sysDepartment;
    }

    @ICacheEvict(key = "#p0")
    public void deleteById(Integer id) {
        log.debug("删除: {}", id);
    }
}
代码语言:javascript
复制
package com.cube.share.cache.service;

import com.cube.share.cache.anonotation.ICache;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.anonotation.ICachePut;
import com.cube.share.cache.model.SysLog;
import org.springframework.stereotype.Service;

/**
 * @author cube.li
 * @date 2021/7/20 23:27
 * @description
 */
@Service
@ICacheConfig(cacheNames = "sysLog", cacheManager = "sysLogCacheManager", expire = 1)
public class SysLogServiceImpl implements SysLogService {

    @Override
    @ICache(key = "#id")
    public SysLog getById(Integer id) {
        return new SysLog(id, "操作" + id);
    }

    @Override
    @ICachePut(key = "#p0.id", condition = "#p0?.id != null")
    public SysLog update(SysLog sysLog) {
        return sysLog;
    }
}
代码语言:javascript
复制
package com.cube.share.cache.service;

import com.cube.share.cache.anonotation.ICache;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.anonotation.ICacheEvict;
import com.cube.share.cache.anonotation.ICachePut;
import com.cube.share.cache.model.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @author poker.li
 * @date 2021/7/17 15:28
 */
@Service
@ICacheConfig(cacheNames = "sysUser")
@Slf4j
public class SysUserService {

    @ICache(key = "#p0")
    public SysUser getById(Integer id) {
        return new SysUser(id, id + "姓名", id + "地址");
    }

    @ICachePut(key = "#sysUser.id")
    public SysUser update(SysUser sysUser) {
        return sysUser;
    }

    @ICacheEvict(allEntries = true)
    public void deleteById(Integer id) {
        log.debug("删除 {}", id);
    }
}

在配置文件中开启缓存

代码语言:javascript
复制
spring:
  redis:
    host: 127.0.0.1
    ssl: false
    port: 6379
    database: 1
    connect-timeout: 1000
    lettuce:
      pool:
        max-active: 10
        max-wait: -1
        min-idle: 0
        max-idle: 20
server:
  port: 8899

ICache:
  enabled: true

写几个单元测试,看一下Redis里的数据:

从测试结果来看,如果指定了cacheManager,则动态生成对应的RedisCacheManager,如果没有指定,则采用默认的缓存管理器。 示例代码:https://gitee.com/li-cube/share/tree/master/cache/src

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 对线JAVA面试 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • spring cache
  • 重写DefaultRedisCacheWriter
  • 自定义缓存注解替代spring cache的注解
  • 指定默认的RedisCacheManager配置
  • 动态生成RedisCacheManager并交给Spring托管
  • 测试
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档