springboot集成ehcache或Redis作为缓存

springboot 缓存

基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。Spring boot 默认使用的是 SimpleCacheConfiguration,即使用 ConcurrentMapCacheManager 来实现缓存。

相关配置

application.properties

server.port=9090
spring.datasource.url=jdbc:mysql://localhost:3306/test_cache?autoReconnect=true&useUnicode=true&allowMultiQueries=true&useSSL=false&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.platform=mysql
spring.messages.encoding=UTF-8
#jpa
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.open-in-view=true
spring.jpa.hibernate.use-new-id-generator-mappings=true
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
# logs
spring.output.ansi.enabled=detect
logging.file=logs/log-v1.log
logging.level.com.welooky.cache.demo.*=INFO
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=INFO
#ehcache的配置
spring.cache.ehcache.config=classpath:config/ehcache.xml
dependencies {
    compile('org.springframework.boot:spring-boot-starter-cache')
    //compile('org.springframework.boot:spring-boot-starter-data-redis')
    compile('net.sf.ehcache:ehcache')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('mysql:mysql-connector-java')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache
        updateCheck="true"
        monitoring="autodetect"
        dynamicConfig="true">

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

    <defaultCache
            maxElementsInMemory="500"
            eternal="false"
            timeToIdleSeconds="300"
            timeToLiveSeconds="1200"
            overflowToDisk="true"
            diskSpoolBufferSizeMB="100"/>

    <cache name="account"
           maxElementsInMemory="1000"
           memoryStoreEvictionPolicy="LRU"
           timeToIdleSeconds="30000"
           timeToLiveSeconds="10000"
           diskSpoolBufferSizeMB="400"
           overflowToDisk="true"/>
    <!--
    maxElementsInMemory : 缓存最大个数、
    eternal : 对象是否永久有效,一但设置了,timeout将不起作用
    timeToIdleSeconds : 设置对象在失效前的允许闲置时间(单位:秒)。
                        仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大
    timeToLiveSeconds : 设置对象在失效前允许存活时间(单位:秒)。
                        最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.也就是对象存活时间无穷大。
    overflowToDisk : 当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中
    diskSpoolBufferSizeMB : 这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
    maxElementsOnDisk : 硬盘最大缓存个数。
    diskPersistent : 是否缓存虚拟机重启期数据
    diskExpiryThreadIntervalSeconds : 磁盘失效线程运行时间间隔,默认是120秒。
    memoryStoreEvictionPolicy : 当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。
                                默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)
    clearOnFlush: 内存数量最大时是否清除。
    -->
</ehcache>
  1. 使用@EnableCaching 启用 Cache 注解支持;
  2. 实现 CachingConfigurer,然后注入需要的 cacheManager 和 keyGenerator;从 spring4 开始默认的 keyGenerator 是 SimpleKeyGenerator;
  3. Spring cache 利用了 Spring AOP 的动态代理技术

注解说明

@Cacheable

主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。

  • value :
    缓存的名称,在 spring 配置文件中定义,必须指定至少一个
    @Cacheable(value="account") 或者@Cacheable(value={"cache1","cache2"}
  • key :
    缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则按照方法的所有参数进行组合
    @Cacheable(value="account",key="#userName")
  • condition:
    缓存的条件,可以为空,使用 SpEL。返回 true 或者 false,只有为 true 才进行缓存
    @Cacheable(value="account",condition="#userName.length()>2")
  • unless
    缓存的条件,非必需,使用 SpEL。该条件是在函数被调用之后才做判断的,它可以对 result 进行判断。

@CachePut

配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable 不同的是,它可以既保证方法被调用,
又可以实现结果被缓存。所以主要用于数据新增和修改操作上,同时能够起到更新缓存的作用。它的参数与@Cacheable 类似,具体功能可参考上面对@Cacheable 参数的说明。

@CacheEvict

配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。

  • value
    缓存的名称,在 spring 配置文件中定义,必须指定至少一个
    @CachEvict(value="mycache") 或者 @CachEvict(value={"cache1","cache2"}
  • key
    缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
    @CachEvict(value="account",key="#userName")
  • condition
    缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存
    @CachEvict(value="account",condition="#userName.length()>2")
  • allEntries
    是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
    @CachEvict(value="account",allEntries=true)
  • beforeInvocation
    非必需,默认为 false,会在调用方法之后移除数据,如果方法执行中抛出异常,则不会清空缓存。为 true 时,会在调用方法之前移除数据。
    @CachEvict(value="account",beforeInvocation=true)

@CacheConfig

是类级别的注解,表示该类中方法定义的所有缓存操作默认使用的缓存 name。如果方法上有@Cacheable 注解,会覆盖它。

  1. spring cache 是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,缓存不起作用。
  2. 和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制
@Service
public class AccountService {
    @Resource
    private AccountRepository accountRepository;
    private Logger logger = LoggerFactory.getLogger(AccountService.class);
    private static final String CACHE_ACCOUNT = "account";

    public void saveUser(String username, String password) {
        Account account = new Account(username, password);
        accountRepository.save(account);
    }

    @Cacheable(value = CACHE_ACCOUNT, key = "#root.caches[0].name + ':' +#username")
    public Account findByUsername(String username) {
        logger.info("select info from db");
        return accountRepository.findByUsername(username);
    }

    @CachePut(value = CACHE_ACCOUNT, key = "#root.caches[0].name + ':' +#username")
    public Account updateUserInfo(String username, String password) {
        Account account = accountRepository.findByUsername(username);
        account.setPassword(password);
        logger.info("update info inside db");
        return accountRepository.save(account);
    }

    @CacheEvict(value = CACHE_ACCOUNT, key = "#scope+':'+#username")
    public void clearCache(String username, String scope) {
        logger.info("clear cache");
    }

    @Cacheable(value = CACHE_ACCOUNT, keyGenerator = "aKeyGenerator")
    public List<Account> findAll() {
        return accountRepository.findAll();
    }

    @Cacheable(value = CACHE_ACCOUNT, keyGenerator = "bKeyGenerator")
    public Account getAccountById(Long id) {
        return accountRepository.getOne(id);
    }

    @CacheEvict(value = CACHE_ACCOUNT, keyGenerator = "bKeyGenerator")
    public void deleteAccountById(Long id) {
        accountRepository.deleteById(id);
    }
}

知识索引:

  1. Spring Cache 抽象详解
  2. 注释驱动的 Spring cache 缓存介绍

Redis

Redis是一个开源,先进的key-value存储,并用于构建高性能,可扩展的Web应用程序的完美解决方案。
Redis从它的许多竞争继承来的三个主要特点:
Redis数据库完全在内存中,使用磁盘仅用于持久性。
相比许多键值数据存储,Redis拥有一套较为丰富的数据类型。
Redis可以将数据复制到任意数量的从服务器。

Redis 优势
异常快速:Redis的速度非常快,每秒能执行约11万集合,每秒约81000+条记录。
支持丰富的数据类型:Redis支持最大多数开发人员已经知道像列表,集合,有序集合,散列数据类型。这使得它非常容易解决各种各样的问题,因为我们知道哪些问题是可以处理通过它的数据类型更好。
操作都是原子性:所有Redis操作是原子的,这保证了如果两个客户端同时访问的Redis服务器将获得更新后的值。
多功能实用工具:Redis是一个多实用的工具,可以在多个用例如缓存,消息,队列使用(Redis原生支持发布/订阅),任何短暂的数据,应用程序,如Web应用程序会话,网页命中计数等

Redis 安装

  • mac

    1. 执行 brew install redis
    2. 启动 redis,可以使用后台服务启动 brew services start redis。或者直接启动:redis-server /usr/local/etc/redis.conf
    3. $ redis-cli
  • ubuntu

    1. $ sudo apt-get update
    2. $ sudo apt-get install redis-server
    3. $ redis-server
    4. $ redis-cli
  • linux

    1. $ wget http://download.redis.io/releases/redis-4.0.6.tar.gz
    2. $ tar xzf redis-4.0.6.tar.gz
    3. $ cd redis-4.0.6
    4. $ make
    5. $ cd src
    6. $ ./redis-server
    7. $ ./redis-cli

知识索引:centos install redis

Redis 配置

  • $ redis-cli:该命令会连接本地的 redis 服务
  • 连接远程:$ redis-cli -h host -p port -a password
  • 查看所有配置:config get *
  • 查看某个配置:config get requirepass
  • 修改某个配置:config set config_setting_name new_config_value
    临时设置:config set
    永久设置:config rewrite,将目前服务器的参数配置写入redis.conf

知识索引:

  1. redis 教程
  2. redis 中国

Redis 命令

  1. 删除当前数据库中的所有 Key:flushdb
  2. 删除所有数据库中的 key:flushall
  3. 删除指定 key:delkey

项目配置

去掉 Elcache 的相关配置,使项目的缓存切换到 Redis

application.properties

# redis
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000ms

修改 gradle

dependencies {
    compile('org.springframework.boot:spring-boot-starter-cache')
    compile('org.springframework.boot:spring-boot-starter-data-redis')
  //compile('net.sf.ehcache:ehcache')
}

RedisConfig

package com.welooky.cache.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.welooky.cache.demo.entity.Account;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;

@Configuration
public class RedisConfig {

    @Bean("aKeyGenerator")
    public KeyGenerator aKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(".").append(method.getName());
            StringBuilder paramsSb = new StringBuilder();
            for (Object param : params) {
                if (param != null) {
                    paramsSb.append("_").append(param.toString());
                }
            }
            if (paramsSb.length() > 0) {
                sb.append("_").append(paramsSb);
            }
            return sb.toString();
        };
    }

    @Bean("bKeyGenerator")
    public KeyGenerator bKeyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                String[] value = new String[1];
                Cacheable cacheable = method.getAnnotation(Cacheable.class);
                if (cacheable != null) {
                    value = cacheable.value();
                }
                CachePut cachePut = method.getAnnotation(CachePut.class);
                if (cachePut != null) {
                    value = cachePut.value();
                }
                CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class);
                if (cacheEvict != null) {
                    value = cacheEvict.value();
                }
                sb.append(value[0]);
                for (Object obj : params) {
                    sb.append(":").append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    @Bean
    public JedisConnectionFactory connectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public CacheManager cacheManager(JedisConnectionFactory connectionFactory) {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }

    @Bean
    public RedisTemplate<String, Account> accountTemplate(JedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Account> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Account.class));
        return template;
    }
}

使用 keyGenerator

  @Cacheable(value = "account", keyGenerator = "aKeyGenerator")
  public List<Account> findAll() {
      return accountRepository.findAll();
  }

test

    @Resource
    private RedisTemplate<String, Account> redisTemplate;

    @Test
    public void testRedisTemplate() {
        redisTemplate.opsForValue().set("andy", new Account("mingXi", "mingxi"));
        Assert.assertEquals("mingXi", redisTemplate.opsForValue().get("andy").getUsername());
    }

生成的缓存 key 为:account::com.welooky.cache.demo.service.AccountService.findAll

使用命令keys *查看所有的缓存 key.

@Configuration
public class RedisConfig {
    @Resource
    private LettuceConnectionFactory lettuceConnectionFactory;


    @Bean
    public CacheManager cacheManager() {
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        return RedisCacheManager.builder(lettuceConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)
                .build();
    }

}

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:2.9.8'


@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        List<MediaType> supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
        supportedMediaTypes.add(MediaType.TEXT_PLAIN);
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(new HibernateAwareObjectMapper());
        converter.setPrettyPrint(true);
        converter.setSupportedMediaTypes(supportedMediaTypes);
        converters.add(converter);
        super.configureMessageConverters(converters);
    }

}

public class HibernateAwareObjectMapper extends ObjectMapper {
    public HibernateAwareObjectMapper() {
        registerModule(new Hibernate5Module());
    }
}

知识索引:

Spring Boot 使用 Redis 缓存

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容