拓展缓存技术
缓存的基本介绍
随着访问量的上升,几乎大部分使用MySQL架构的网站在数据库上都出现了性能问题,web程序不再仅仅关注在功能上,同时也开始追求性能,Memcached(缓存)自然成为一个非常时尚的技术产品。
缓存的实质是替数据库挡了一层。主要是减轻对数据库的高频率读的压力。频繁被访问的数据可以被放置于缓存当中,以供频繁访问。
缓存的原理
(1)将数据写入/读取速度更快的存储(设备);
(2)将数据缓存到离应用最近的位置;
(3)将数据缓存到离用户最近的位置。
缓存分类
在分布式系统中,缓存的应用非常广泛,从部署角度有以下几个方面的缓存应用。
(1)CDN缓存;
(2)反向代理缓存;
(3)分布式缓存;
(4)本地应用缓存;
Ehcache本地缓存
Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cache管理方案,同时具有快速,简单,低消耗,依赖性小,扩展性强,支持对象或序列化缓存,支持缓存或元素的失效,提供 LRU、LFU 和 FIFO 缓存策略,支持内存缓存和磁盘缓存,分布式缓存机制等等特点。
@Cacheable
@Cacheable 用来声明方法是可缓存的,将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法,直接从缓存中取值。
@Cacheable 可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
@Cacheable 支持如下几个参数:
• value:缓存的名称。
• key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写;如果不指定,则缺省按照方法的所有参数进行组合。
• condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持 SpEL。
总结一下:当执行到一个被 @Cacheable 注解的方法时,Spring 首先检查 condition 条件是否满足,如果不满足,执行方法,返回;如果满足,在缓存空间中查找使用 key 存储的对象,如果找到,将找到的结果返回,如果没有找到执行方法,将方法的返回值以 key-value 对象的方式存入缓存中,然后方法返回。
@CachePut
项目运行中会对数据库的信息进行更新,如果仍然使用 @Cacheable 就会导致数据库的信息和缓存的信息不一致。在以往的项目中,我们一般更新完数据库后,再手动删除掉 Redis 中对应的缓存,以保证数据的一致性。Spring 提供了另外的一种解决方案,可以让我们以优雅的方式去更新缓存。
与 @Cacheable 不同的是使用 @CachePut 标注的方法在执行前,不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CachePut 的参数和使用方法基本和 @Cacheable 一致。
@CacheEvict
@CacheEvict 是用来标注在需要清除缓存元素的方法或类上的,当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
@JsonIgnore
@JsonIgnore是用来标注在需要忽略的属性上。因为我们前面在RedisConfig配置类中配置了使用Jackson的序列化对象,将对象转换为JSON保存在Redis中。那么在将对象转换为JSON时,有些属性需要忽略,特别是对象之间有关联关系时,需要使用@JsonIgnore忽略关联对象,避免转换时出现死循环。
Springboot整合Ehcache使用Redis作为缓存(二级缓存)
导入依赖
<!-- Spring Boot 缓存支持启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Ehcache 坐标-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
application.properties
#使用redis作为缓存
spring.cache.type=redis
#设置缓存过期时间为1小时
spring.cache.redis.time-to-live=3600000
#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE_
#指定是否默认开启前缀
spring.cache.redis.use-key-prefix=true
#是否开启null值,防止缓存雪崩
spring.cache.redis.cache-null-values=true
启动类
@SpringBootApplication
@EnableCaching//启用缓存
public class SpringbootReidsFenApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootReidsFenApplication.class, args);
}
}
查询操作进行一个新增缓存的操作(读模式)
/**
* 查出所有的一级分类
* @return
*/
//每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
//#root.method.name 使用方法名作为缓存的key, sync = true 加锁操作默认为false开启则为true变成枷锁操作(本地锁,解决缓存击穿)
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)//代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方霍
@Override
public List<CategoryEntity> getLevel1Category() {
List<CategoryEntity> parentCid = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return parentCid;
}
更新操作进行一个缓存的删除(写模式)
/**
* @CacheEvict 失效模式 ,他会将缓存的数据进行一个删除操作,等下次查询在重新加载到缓存中,注意:这里要加''
* @Caching 同时进行多种缓存操作
* 存储同—类型的数据,都可以指定成同一个分区,分区名默认就是缓存的前缀
* @param category
*/
@Override
@Transactional
@CacheEvict(value = "category",allEntries = true) //allEntries = true 满足条件删除的是category分区下的所有缓存的所有数据
// @CacheEvict(value = "category",key = "'getLevel1Category'") //这里只对一个缓存进行删除操作
// @Caching(evict = {//这里对两个缓存的数据进行一个清空操作
// @CacheEvict(value = "category",key = "'getLevel1Category'"),
// @CacheEvict(value = "category",key = "'getCatelogJson'")
// })
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}
自定义缓存配置
/**
自定义缓存配置
**/
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
/**
* 配置文件的配置没有用上
* @return
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config = config.entryTtl();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//将配置文件中所有的配置都生效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
Springboot整合Ehcache
导入依赖
<!-- Spring Boot 缓存支持启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Ehcache 坐标-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
application.properties
#指定缓存xml文件路径
spring.cache.ehcache.config=classpath:ehcache.xml
#配置数据库链接驱动
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/newsdb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#配置日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<!--持久化指定硬盘的位置-->
<diskStore path="java.io.tmpdir"/>
<!--defaultCache:echcache 的默认缓存策略-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
<!-- 自定义缓存策略 news对应serviceImpl的value = "news"-->
<cache name="news"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</cache>
</ehcache>
name : 缓存的名称,可以通过指定名称获取指定的某个Cache对象
maxElementsInMemory :内存中允许存储的最大的元素个数,0代表无限个
clearOnFlush:内存数量最大时是否清除。
eternal :设置缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。根据存储数据的不同,例如一些静态不变的数据如省市区等可以设置为永不过时
timeToIdleSeconds : 设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds :缓存数据的 生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。(和上面的两者取最小值)
overflowToDisk:内存不足时,是否启用磁盘缓存。
maxEntriesLocalDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
maxElementsOnDisk:硬盘最大缓存个数。
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskPersistent:是否在VM重启时存储硬盘的缓存数据。默认值是false。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。这里比较遗憾,Ehcache并没有提供一个用户定制策略的接口,仅仅支持三种指定策略,感觉做的不够理想。
news.java
package com.springbootehcache.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 新闻表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "news")
public class News {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 新闻类型
*/
@TableField(value = "type_id")
private String typeId;
/**
* 新闻标题
*/
@TableField(value = "title")
private String title;
/**
* 发布者
*/
@TableField(value = "publisher")
private String publisher;
/**
* 发布者头像
*/
@TableField(value = "publisher_face")
private String publisherFace;
/**
* 新闻内容
*/
@TableField(value = "content")
private String content;
/**
* 发布时间
*/
@TableField(value = "create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField(value = "update_time")
private Date updateTime;
/**
* 删除状态
*/
@TableField(value = "deleted")
private Integer deleted;
public static final String COL_ID = "id";
public static final String COL_TYPE_ID = "type_id";
public static final String COL_TITLE = "title";
public static final String COL_PUBLISHER = "publisher";
public static final String COL_PUBLISHER_FACE = "publisher_face";
public static final String COL_CONTENT = "content";
public static final String COL_CREATE_TIME = "create_time";
public static final String COL_UPDATE_TIME = "update_time";
public static final String COL_DELETED = "deleted";
}
NewsMapper.java
package com.springbootehcache.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.springbootehcache.pojo.News;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NewsMapper extends BaseMapper<News> {
}
NewsService.java
package com.springbootehcache.service;
import com.springbootehcache.pojo.News;
import com.baomidou.mybatisplus.extension.service.IService;
public interface NewsService extends IService<News>{
News byId(Integer id);
boolean deleteById(Integer id);
News add(News news);
}
NewsServiceImpl.java
package com.springbootehcache.service.impl;
import com.springbootehcache.service.NewsService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.springbootehcache.mapper.NewsMapper;
import com.springbootehcache.pojo.News;
@Service
public class NewsServiceImpl extends ServiceImpl<NewsMapper, News> implements NewsService {
//value = "news"指定xml自定义的名称
//key = "id"对应参数传的id
//condition = "#id>60"判断返回的是值true做缓存,false不做缓存 id>60做缓存
//@Cacheable 作用:把方法的返回值添加到Ehcache 中做缓存.
@Override
@Cacheable(value = "news",key = "#id",condition = "#id>60")
public News byId(Integer id) {
System.out.println("进入service 查询列表执行数据库查询....");
return this.getById(id);
}
// allEntries:默认时false不会清除 是否清除缓存中的元素
//beforeInvocation:是否在方法之前清除缓存:默认值false在方法调用值后清除
//@CacheEvic是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作.
@Override
@CacheEvict(value = "news",key = "#id",allEntries = true,beforeInvocation = false)
public boolean deleteById(Integer id) {
return this.removeById(id);
}
@Override
@CachePut(value = "news",key = "#result.id") //result.id获取方法的返回值 id
//@Cacheable 的key 要和 @CachePut 的key 一致,类似于更新缓存 将重新查询到的数据 缓存到redis 替换原有的同key的缓存。
public News add(News news) {
boolean result = this.save(news);
News byId = this.getById(news.getId());
return byId;
}
}
NewsController.java
package com.springbootehcache.controller;
import com.springbootehcache.pojo.News;
import com.springbootehcache.service.NewsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class NewsController {
@Autowired
private NewsService newsService;
/**
* 查询
* @return
*/
@RequestMapping("/list")
public Object list() {
System.out.println("准备执行查询news列表");
return newsService.byId(2);
}
/**
* 删除
* @param id
* @return
*/
@RequestMapping("/delete/{id}")
public boolean deleteNews(@PathVariable("id") Integer id){
return newsService.deleteById(id);
}
/**
* 新增
* @return
*/
@RequestMapping("/add")
public Object newsAdd(){
News news = new News()
.setTypeId("1001")
.setTitle("aaa");
return newsService.add(news);
}
}
启动类
@SpringBootApplication
@EnableCaching//启用缓存
public class EhcacheApplication {
public static void main(String[] args) {
SpringApplication.run(EhcacheApplication.class, args);
}
}
Springboot整合本地缓存
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
RedisConfig
package com.springbootreidsfen.config;
import org.springframework.cache.CacheManager;
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.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;
@Configuration
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
//创建一个缓存的配置对象
RedisCacheConfiguration configuration=RedisCacheConfiguration.defaultCacheConfig()
//指定序列化器
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
//指定缓存的有效器为30分钟
.entryTtl(Duration.ofSeconds(30))
//禁止缓存空值
.disableCachingNullValues();
//通过缓存对象,创建缓存管理器
CacheManager cacheManager= RedisCacheManager.builder(factory)
.withCacheConfiguration("news",configuration)
.build();
return cacheManager;
}
}
application.properties
#配置redis
#设置连接本机地址
spring.redis.host=127.0.0.1
#设置redis端口号
spring.redis.port=6379
#设置密码
spring.redis.password=
#连接池配置
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=10000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=5
#配置数据库链接驱动
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/newsdb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#配置日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
News
package com.springbootreidsfen.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 新闻表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@TableName(value = "news")
public class News {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 新闻类型
*/
@TableField(value = "type_id")
private String typeId;
/**
* 新闻标题
*/
@TableField(value = "title")
private String title;
/**
* 发布者
*/
@TableField(value = "publisher")
private String publisher;
/**
* 发布者头像
*/
@TableField(value = "publisher_face")
private String publisherFace;
/**
* 新闻内容
*/
@TableField(value = "content")
private String content;
/**
* 发布时间
*/
@TableField(value = "create_time")
private Date createTime;
/**
* 修改时间
*/
@TableField(value = "update_time")
private Date updateTime;
/**
* 删除状态
*/
@TableField(value = "deleted")
private Integer deleted;
public static final String COL_ID = "id";
public static final String COL_TYPE_ID = "type_id";
public static final String COL_TITLE = "title";
public static final String COL_PUBLISHER = "publisher";
public static final String COL_PUBLISHER_FACE = "publisher_face";
public static final String COL_CONTENT = "content";
public static final String COL_CREATE_TIME = "create_time";
public static final String COL_UPDATE_TIME = "update_time";
public static final String COL_DELETED = "deleted";
}
NewsMapper
@Mapper
public interface NewsMapper extends BaseMapper<News> {
}
NewsService
package com.springbootreidsfen.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.springbootreidsfen.pojo.News;
public interface NewsService extends IService<News>{
News byId(Integer id);
boolean deleteById(Integer id);
News add(News news);
}
NewsServiceImpl
package com.springbootreidsfen.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.springbootreidsfen.mapper.NewsMapper;
import com.springbootreidsfen.pojo.News;
import com.springbootreidsfen.service.NewsService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class NewsServiceImpl extends ServiceImpl<NewsMapper, News> implements NewsService {
//value = "news"指定xml自定义的名称
//key = "id"对应参数传的id
//condition = "#id>1"判断返回的是值true做缓存,false不做缓存 id>60做缓存
//@Cacheable 作用:把方法的返回值添加到Ehcache 中做缓存.
@Override
@Cacheable(value = "news",key = "#id",condition = "#id>1")
public News byId(Integer id) {
System.out.println("进入service 查询列表执行数据库查询....");
return this.getById(id);
}
// allEntries:默认时false不会清除 是否清除缓存中的元素
//beforeInvocation:是否在方法之前清除缓存:默认值false在方法调用值后清除
//@CacheEvic是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作.
@Override
@CacheEvict(value = "news",key = "#id",allEntries = true,beforeInvocation = false)
public boolean deleteById(Integer id) {
return this.removeById(id);
}
@Override
@CachePut(value = "news",key = "#result.id") //result.id获取方法的返回值 id
//@Cacheable 的key 要和 @CachePut 的key 一致,类似于更新缓存 将重新查询到的数据 缓存到redis 替换原有的同key的缓存。
public News add(News news) {
boolean result = this.save(news);
News byId = this.getById(news.getId());
return byId;
}
}
NewsController
package com.springbootreidsfen.controller;
import com.springbootreidsfen.pojo.News;
import com.springbootreidsfen.service.NewsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NewsController {
@Autowired
private NewsService newsService;
@RequestMapping("/list")
public Object list() {
System.out.println("准备执行查询news列表");
return newsService.byId(2);
}
/**
* 删除
* @param id
* @return
*/
@RequestMapping("/news/{id}")
public boolean deleteNews(Integer id){
return newsService.deleteById(id);
}
@RequestMapping("/add")
public Object newsAdd(){
News news = new News()
.setTypeId("1001")
.setTitle("aaa");
return newsService.add(news);
}
}
启动类
@SpringBootApplication
@EnableCaching//启用缓存
public class SpringbootReidsFenApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootReidsFenApplication.class, args);
}
}