Spring Cache優(yōu)化

前言

緩存是web項目不可或缺的一部分,通過緩存能夠降低服務器數(shù)據(jù)庫壓力绪钥,提高服務器的穩(wěn)定性及響應速度哩掺。

spring cache

spring cache是spring框架自帶的一套緩存框架,其具有多種實現(xiàn)厘擂,比較常用的是基于Redis的實現(xiàn)昆淡,其核心注解有 @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具有極高的易用性,在保存緩存時能夠根據(jù)Spring EL表達式自由定制緩存鍵泪姨,但是spring cache在使用過程中有兩點缺陷:

  • 在使用@CacheEvict時游沿,如果指定了allEntries=true,在從Redis中刪除緩存時使用的是 keys指令驴娃,keys指令時間復雜度是O(N)奏候,如果緩存數(shù)量較大會產生明顯的阻,因此在生產環(huán)境中Redis會禁用這個指令唇敞,導致報錯蔗草。
    看下DefaultRedisCacheWriter的clean方法:
@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屬性指定緩存管理器時咒彤,如果不指定則采用全局的聲明的緩存管理器,無法調整緩存的過期時間咒精,而如果指定了緩存管理器則必須要手動創(chuàng)建一個緩存管理器且需要交給spring托管镶柱,無法動態(tài)指定緩存管理器。
    這里對上述兩個缺陷進行了修改模叙,一是通過scan指令替代keys指令歇拆,雖然scan指令的時間復雜度也是O(N),但是其通過指定游標和count能夠分批執(zhí)行范咨,不會導致長時間的阻塞故觅;二是在項目啟動后,通過掃描注解動態(tài)生成cacheManager渠啊,能夠滿足不同緩存模塊指定不同的緩存時間的需求输吏,且無需手動創(chuàng)建RedisCacheManager。
重寫DefaultRedisCacheWriter

DefaultRedisCacheWriter是spring cache提供的默認的Redis緩存寫出器替蛉,其內部封裝了緩存增刪改查等邏輯贯溅,但是由于其不是public修飾的,因此重寫了一個Redis緩存寫出器躲查,大部分代碼均與DefaultRedisCacheWriter相同它浅,只有clean方法做了修改。

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的實現(xiàn),重寫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
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 "";

    /**
     * 緩存管理器,如果沒有指定則采用默認的緩存管理器,如果需要自定義緩存的過期時間镣煮。
     * 則必須指定該屬性,并且要使該屬性唯一,這樣能創(chuàng)建一個新的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
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}實現(xiàn)的緩存存放注解
 */
@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 "";

    /**
     * 在方法執(zhí)行結束后,根據(jù)方法的執(zhí)行結果執(zhí)行是否需要放入緩存,例如
     * unless = "#result != null",表示僅當方法執(zhí)行結果不為null時才放入緩存
     */
    @AliasFor(annotation = Cacheable.class, attribute = "unless")
    String unless() default "";

    /**
     * 是否需要同步調用,如果設置為true,具有相同key的多次調用串行執(zhí)行
     */
    @AliasFor(annotation = Cacheable.class, attribute = "sync")
    boolean sync() default false;

}

  • @ICachePut
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 "";

    /**
     * 在方法執(zhí)行結束后,根據(jù)方法的執(zhí)行結果執(zhí)行是否需要放入緩存,例如
     * unless = "#result != null",表示僅當方法執(zhí)行結果不為null時才放入緩存
     */
    @AliasFor(annotation = CachePut.class, attribute = "unless")
    String unless() default "";
}

  • @ICacheEvict
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,僅當方法成功執(zhí)行后才刪除緩存,
     * 如果設定為true,則在調用前即刪除緩存,無論方法最終是否調用成功
     */
    @AliasFor(annotation = CacheEvict.class, attribute = "beforeInvocation")
    boolean beforeInvocation() default false;

}

上面的四個注解實際上只有@ICacheConfig對原生注解@CacheConfig做了再封裝姐霍,增加了三個屬性,另外三個注解只是對spring cache對應的原生注解起了個別名怎静,以后可能會有拓展的需要邮弹。

指定默認的RedisCacheManager配置
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個小時。

動態(tài)生成RedisCacheManager并交給Spring托管
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盟劫,并根據(jù)指定的cacheManager屬性生成對應的RedisCacheManager管理器夜牡。

測試
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);
    }
}
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;
    }
}
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);
    }
}

在配置文件中開啟緩存

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里的數(shù)據(jù):


分模塊存儲.png
全局過期時間.png
指定過期時間1.png

指定過期時間2.png

從測試結果來看侣签,如果指定了cacheManager塘装,則動態(tài)生成對應的RedisCacheManager,如果沒有指定影所,則采用默認的緩存管理器蹦肴。
示例代碼: https://gitee.com/li-cube/share/tree/master/cache/src

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市猴娩,隨后出現(xiàn)的幾起案子阴幌,更是在濱河造成了極大的恐慌勺阐,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矛双,死亡現(xiàn)場離奇詭異渊抽,居然都是意外死亡,警方通過查閱死者的電腦和手機议忽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門懒闷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人栈幸,你說我怎么就攤上這事愤估。” “怎么了速址?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵灵疮,是天一觀的道長。 經常有香客問我壳繁,道長震捣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任闹炉,我火速辦了婚禮蒿赢,結果婚禮上,老公的妹妹穿的比我還像新娘渣触。我一直安慰自己羡棵,他們只是感情好,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布嗅钻。 她就那樣靜靜地躺著皂冰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪养篓。 梳的紋絲不亂的頭發(fā)上秃流,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音柳弄,去河邊找鬼舶胀。 笑死,一個胖子當著我的面吹牛碧注,可吹牛的內容都是我干的嚣伐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼萍丐,長吁一口氣:“原來是場噩夢啊……” “哼轩端!你這毒婦竟也來了?” 一聲冷哼從身側響起逝变,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤基茵,失蹤者是張志新(化名)和其女友劉穎刻撒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體耿导,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡声怔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了舱呻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片醋火。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖箱吕,靈堂內的尸體忽然破棺而出芥驳,到底是詐尸還是另有隱情,我是刑警寧澤茬高,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布兆旬,位于F島的核電站,受9級特大地震影響怎栽,放射性物質發(fā)生泄漏丽猬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一熏瞄、第九天 我趴在偏房一處隱蔽的房頂上張望脚祟。 院中可真熱鬧,春花似錦强饮、人聲如沸由桌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽行您。三九已至,卻和暖如春剪廉,著一層夾襖步出監(jiān)牢的瞬間娃循,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工妈经, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留淮野,地道東北人捧书。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓吹泡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親经瓷。 傳聞我的和親對象是個殘疾皇子爆哑,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容