SpringBoot整合Redis與Cache與實現(xiàn)

Redis 簡介

GitHub 地址:https://github.com/antirez/redis 。

GitHub 介紹:Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, HyperLogLogs, Bitmaps.

對于緩存

  • 內(nèi)存的速度遠(yuǎn)遠(yuǎn)大于硬盤的速度
  • 緩存主要是在獲取資源方便性能優(yōu)化的關(guān)鍵方面
  • Redis 是緩存數(shù)據(jù)庫
  • 緩存未命中解決與防止緩存擊穿

緩存更新策略

  1. Cache aside :

    • 思路:先更新數(shù)據(jù)庫,在更新緩存。

    • 問題:一個讀操作,但是沒有命中緩存,然后就到數(shù)據(jù)庫中取數(shù)據(jù),此時來了一個寫操作,寫完數(shù)據(jù)庫后,讓緩存失效,然后,之前的那個讀操作再把老的數(shù)據(jù)放到緩存,所以,會造成臟數(shù)據(jù)。

    • 出現(xiàn)此問題的前提:讀緩存時緩存失效,而且并發(fā)著有一個寫操作。

    • 而實際上數(shù)據(jù)庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進(jìn)入數(shù)據(jù)庫操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的概率基本并不大。

  2. Read through

  • 思路:在查詢操作中更新緩存
  1. Write through
    • 思路:有數(shù)據(jù)更新的時候,如果沒有命中緩存,直接更新數(shù)據(jù)庫,然后返回。如果命中了緩存,則更新緩存,然后再由Cache自己更新數(shù)據(jù)庫(這是一個同步操作)
  2. Write behind caching
    • 思路:只更新緩存,不更新數(shù)據(jù)庫,而我們的緩存會異步地批量更新數(shù)據(jù)庫。
    • 實現(xiàn)有點復(fù)雜,具體參考《緩存更新的套路》

Redis 實踐(復(fù)雜緩存)

配置application.yml

spring:
  cache:
    type: REDIS
    redis:
      cache-null-values: false
      time-to-live: 600000ms
      use-key-prefix: true
      #緩存名稱列表
    cache-names: userCache,allUsersCache
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    # 單通道
    lettuce:
      shutdown-timeout: 200ms
      pool:
        max-active: 7
        max-idle: 7
        min-idle: 2
        max-wait: -1ms
    timeout: 1000

對應(yīng)的配置類:org.springframework.boot.autoconfigure.data.redis.RedisProperties

添加配置類

這里自定義RedisTemplate的配置類,主要是想使用Jackson替換默認(rèn)的序列化機(jī)制:

@Configuration
public class RedisConfig {
    /**
     * redisTemplate 默認(rèn)使用JDK的序列化機(jī)制, 存儲二進(jìn)制字節(jié)碼, 所以自定義序列化類
     * @param redisConnectionFactory redis連接工廠類
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替換默認(rèn)序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 設(shè)置value的序列化規(guī)則和 key的序列化規(guī)則
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

使用Cache aside策略的實例

這里只展示使用服務(wù)

@Service(value = "appUserService")
public class AppUserServiceImpl implements AppUserService {

    @Resource(name = "appUserRepository")
    private AppUserRepository appUserRepository;
    
    @Resource
    private RedisTemplate<String, User> redisTemplate;
    
    /**
    * 不做任何操作
    * @param appUser 用戶
    **/
    @Override
    public AppUser saveOne(AppUser appUser) {
        return appUserRepository.save(appUser);
    }
    /**
     * 獲取用戶信息
     * 如果緩存存在,從緩存中獲取城市信息
     * 如果緩存不存在,從 DB 中獲取城市信息,然后插入緩存
     *
     * @param loginName 用戶登錄名
     * @return 用戶
     */
    @Override
    public AppUser findByLoginName(String loginName) {
        ogger.info("獲取用戶start...");
        // 從緩存中獲取用戶信息
        String key = "AppUser:" + loginName;
        ValueOperations<String, User> operations = redisTemplate.opsForValue();

        // 緩存存在
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            AppUser user = operations.get(key);
            logger.info("從緩存中獲取了用戶 AppUser = " + loginName);
            return user;
        }
        // 緩存不存在,從 DB 中獲取
        List<AppUser> appUserList = appUserRepository.findByLoginNameEquals(loginName); 
        // 插入緩存
        if(appUserList.size() > 0){
            operations.set(key, appUserList.get(0), 10, TimeUnit.SECONDS);
        }
        return appUserList.size() > 0 ? appUserList.get(0) : null;
    }
    /**
     * 更新用戶
     * 如果緩存存在,刪除
     * 如果緩存不存在,不操作
     *
     * @param user 用戶
     */
    public void updateUser(AppUser user) {
        logger.info("更新用戶start...");
        appUserRepository.save(user);
        // 緩存存在,刪除緩存
        String key = "AppUser:" + user.getLoginName();
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            redisTemplate.delete(key);
            logger.info("更新用戶時候,從緩存中刪除用戶 >> " + user.getLoginName());
        }
    }
    /**
     * 刪除用戶
     * 如果緩存中存在,刪除
     */
    public void deleteById(Long id) {
        logger.info("刪除用戶start...");
        AppUser user = appUserRepository.get(id);
        appUserRepository.deleteById(id);

        // 緩存存在,刪除緩存
        String key = "AppUser:" + user.getLoginName();
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            redisTemplate.delete(key);
            logger.info("刪除用戶時候,從緩存中刪除用戶 >> " + user.getLoginName());
        }
    }
}

Redis + Cache 實踐(簡單緩存)

Spring緩存支持

Spring定義了org.springframework.cache.CacheManagerorg.springframework.cache.Cache 接口來統(tǒng)一不同緩存技術(shù)。 其中CacheManager是Spring提供的各種緩存技術(shù)抽象接口,內(nèi)部使用Cache接口進(jìn)行緩存的增刪改查操作,我們一般不會直接和Cache打交道。

針對不同的緩存技術(shù),Spring有不同的CacheManager實現(xiàn)類,定義如下表:

CacheManager 描述
SimpleCacheManager 使用簡單的Collection存儲緩存數(shù)據(jù),用來做測試用
ConcurrentMapCacheManager 使用ConcurrentMap存儲緩存數(shù)據(jù)
EhCacheCacheManager 使用EhCache作為緩存技術(shù)
GuavaCacheManager 使用Google Guava的GuavaCache作為緩存技術(shù)
JCacheCacheManager 使用JCache(JSR-107)標(biāo)準(zhǔn)的實現(xiàn)作為緩存技術(shù),比如Apache Commons JCS
RedisCacheManager 使用Redis作為緩存技術(shù)

在我們使用任意一個實現(xiàn)的CacheManager的時候,需要注冊實現(xiàn)Bean:

/**
 * EhCache的配置
 */
@Bean
public EhCacheCacheManager cacheManager(CacheManager cacheManager) {
    return new EhCacheCacheManager(cacheManager);
}

聲明式緩存注解

Spring提供4個注解來聲明緩存規(guī)則,如下表所示:

注解 說明
@Cacheable 方法執(zhí)行前先看緩存中是否有數(shù)據(jù),如果有直接返回。如果沒有就調(diào)用方法,并將方法返回值放入緩存
@CachePut 無論怎樣都會執(zhí)行方法,并將方法返回值放入緩存
@CacheEvict 將數(shù)據(jù)從緩存中刪除
@Caching 可通過此注解組合多個注解策略在一個方法上面

@Cacheable 、@CachePut 、@CacheEvict都有value屬性,指定要使用的緩存名稱,而key屬性指定緩存中存儲的鍵。

@EnableCaching 開啟緩存。

@Cacheable

這個注解含義是方法結(jié)果會被放入緩存,并且一旦緩存后,下一次調(diào)用此方法,會通過key去查找緩存是否存在,如果存在就直接取緩存值,不再執(zhí)行方法。

這個注解有幾個參數(shù)值,定義如下

參數(shù) 解釋
cacheNames 緩存名稱
value 緩存名稱的別名
condition Spring SpEL 表達(dá)式,用來確定是否緩存
key SpEL 表達(dá)式,用來動態(tài)計算key
keyGenerator Bean 名字,用來自定義key生成算法,跟key不能同時用
unless SpEL 表達(dá)式,用來否決緩存,作用跟condition相反
sync 多線程同時訪問時候進(jìn)行同步

在計算key、condition或者unless的值得時候,可以使用到以下的特有的SpEL表達(dá)式

表達(dá)式 解釋
#result 表示方法的返回結(jié)果
#root.method 當(dāng)前方法
#root.target 目標(biāo)對象
#root.caches 被影響到的緩存列表
#root.methodName 方法名稱簡稱
#root.targetClass 目標(biāo)類
#root.args[x] 方法的第x個參數(shù)

@CachePut

該注解在執(zhí)行完方法后會觸發(fā)一次緩存put操作,參數(shù)跟@Cacheable一致

@CacheEvict

該注解在執(zhí)行完方法后會觸發(fā)一次緩存evict操作,參數(shù)除了@Cacheable里的外,還有個特殊的allEntries, 表示將清空緩存中所有的值。

緩存注解使用

在service中定義增刪改的幾個常見方法,通過注解實現(xiàn)緩存:

@Service
@Transactional
public class UserService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Resource
    private AppuserRepository appuserRepository;

    /**
     * cacheNames 設(shè)置緩存的值
     * key:指定緩存的key,這是指參數(shù)id值。key可以使用spEl表達(dá)式
     *
     * @param id
     * @return
     */
    @Cacheable(value = "userCache", key = "#id", unless="#result == null")
    public AppUser getById(int id) {
        logger.info("獲取用戶start...");
        return appuserRepository.selectById(id);
    }

    @Cacheable(value = "allUsersCache", unless = "#result.size() == 0")
    public List<User> getAllUsers() {
        logger.info("獲取所有用戶列表");
        return appuserRepository.findByLoginNameEquals(null);
    }

    /**
     * 創(chuàng)建用戶,同時使用新的返回值的替換緩存中的值
     * 創(chuàng)建用戶后會將allUsersCache緩存全部清空
     */
    @Caching(
            put = {@CachePut(value = "userCache", key = "#user.id")},
            evict = {@CacheEvict(value = "allUsersCache", allEntries = true)}
    )
    public AppUser createUser(AppUser user) {
        logger.info("創(chuàng)建用戶start..., user.id=" + user.getId());
        appuserRepository.save(user);
        return user;
    }

    /**
     * 更新用戶,同時使用新的返回值的替換緩存中的值
     * 更新用戶后會將allUsersCache緩存全部清空
     */
    @Caching(
            put = {@CachePut(value = "userCache", key = "#user.id")},
            evict = {@CacheEvict(value = "allUsersCache", allEntries = true)}
    )
    public AppUser updateUser(Appuser user) {
        logger.info("更新用戶start...");
        appuserRepository.save(user);
        return user;
    }

    /**
     * 對符合key條件的記錄從緩存中移除
     * 刪除用戶后會將allUsersCache緩存全部清空
     */
    @Caching(
            evict = {
                    @CacheEvict(value = "userCache", key = "#id"),
                    @CacheEvict(value = "allUsersCache", allEntries = true)
            }
    )
    public void deleteById(int id) {
        logger.info("刪除用戶start...");
        appuserRepository.deleteById(id);
    }

}

緩存配置類

@Configuration
@EnableCaching
public class RedisCacheConfig {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private Environment env;

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisConf = new RedisStandaloneConfiguration();
        redisConf.setHostName(env.getProperty("spring.redis.host"));
        redisConf.setPort(Integer.parseInt(env.getProperty("spring.redis.port")));
        redisConf.setPassword(RedisPassword.of(env.getProperty("spring.redis.password")));
        return new LettuceConnectionFactory(redisConf);
    }

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .disableCachingNullValues();
        return cacheConfig;
    }

    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager rcm = RedisCacheManager.builder(redisConnectionFactory())
                .cacheDefaults(cacheConfiguration())
                .transactionAware()
                .build();
        return rcm;
    }
}

keyGenerator 自定義key

一般來講我們使用key屬性就可以滿足大部分要求,但是如果你還想更好的自定義key,可以實現(xiàn)keyGenerator。

這個屬性為定義key生成的類,和key屬性不能同時存在。

RedisCacheConfig配置類中添加我自定義的KeyGenerator:

/**
 * 自定義緩存key的生成類實現(xiàn)
 */
@Bean(name = "myKeyGenerator")
public KeyGenerator myKeyGenerator() {
    return new KeyGenerator() {
        @Override
        public Object generate(Object o, Method method, Object... params) {
            logger.info("自定義緩存,使用第一參數(shù)作為緩存key,params = " + Arrays.toString(params));
            // 僅僅用于測試,實際不可能這么寫
            return params[0];
        }
    };
}

切換緩存技術(shù)

得益于SpringBoot的自動配置機(jī)制,切換緩存技術(shù)除了替換相關(guān)maven依賴包和配置Bean外,使用方式和實例中一樣, 不需要修改業(yè)務(wù)代碼。如果你要切換到其他緩存技術(shù)非常簡單。

EhCache

當(dāng)我們需要使用EhCache作為緩存技術(shù)的時候,只需要在pom.xml中添加EhCache的依賴:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcahe</artifactId>
</dependency>

EhCache的配置文件ehcache.xml只需要放到類路徑下面,SpringBoot會自動掃描,例如:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false" monitoring="autodetect"
         dynamicConfig="true">

    <diskStore path="java.io.tmpdir/ehcache"/>

    <defaultCache
            maxElementsInMemory="50000"
            eternal="false"
            timeToIdleSeconds="3600"
            timeToLiveSeconds="3600"
            overflowToDisk="true"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />

    <cache name="authorizationCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="3600"
           overflowToDisk="false"
           statistics="true">
    </cache>
</ehcache>

SpringBoot會為我們自動配置EhCacheCacheManager這個Bean,不過你也可以自己定義。

Guava

當(dāng)我們需要Guava作為緩存技術(shù)的時候,只需要在pom.xml中增加Guava的依賴即可:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

SpringBoot會為我們自動配置GuavaCacheManager這個Bean。

Redis

最后還提一點,本篇采用Redis作為緩存技術(shù),添加了依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

SpringBoot會為我們自動配置RedisCacheManager這個Bean,同時還會配置RedisTemplate這個Bean。 后面這個Bean就是下一篇要講解的操作Redis數(shù)據(jù)庫用,這個就比單純注解緩存強(qiáng)大和靈活的多了。

參考文章

Spring Boot Redis Cache

SpringBoot系列 - 緩存

本文地址:

SpringBoot整合Redis與Cache與實現(xiàn)

推薦

SpringBoot整合Redis及Redis簡介和操作

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容