我们都知道访问数据库所消耗的IO是很大的,为了让系统性能更好我们通常都需要引入缓存,因为如果没有缓存所有操作都去操作数据库并且某些操作还很频繁那么就会影响整个系统的性能。而需要使用缓存的地方也有很多例如:shiro中的权限缓存、登录验证码的缓存,某些临时的数据缓存等等。所以下面就来介绍一下缓存的使用
缓存大致可以分为两种,一种是依赖于应用本身的本地缓存例如ehcache,一种是可以独立于应用的缓存例如redis。而这里也主要介绍ehcache和redis的使用及整合。
在系统中如果不涉及到分布式以及缓存的共享还是建议直接使用ehcache就可以了,因为ehcache使用很简单,而且ehcache是在应用内的所以不需要进行网络请求。当然如果是涉及到分布式以及缓存的共享那就建议使用redis,目前主流的共享缓存解决方案也是使用redis数据库。
对于MyAdmin项目中的缓存我的想法是:
因此系统里面需要同时支持两种缓存并且可以随意切换。幸运的是spring中提供的缓存抽象正好就符合我们的要求。
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;他就类似于jdbc统一了数据库的操作
CacheManager 缓存管理器,管理各种缓存(Cache)组件
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等;
每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
缓存注解:
spring缓存抽象还提供了一系列的注解,来简化缓存的操作,这些注解包括:
@CacheConfig
:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = "users")
:配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable
自己配置缓存集的名字来定义。
@Cacheable
:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:
value
、cacheNames
:两个等同的参数(cacheNames
为Spring 4新增,作为value
的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig
,因此在Spring 3中原本必须有的value
属性,也成为非必需项了key
:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0")
:使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档
condition
:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3")
,表示只有当第一个参数的长度小于3的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。unless
:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition
参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。并且condition和unless的表达式中使用的都是SpEL表达式,如果有多个条件要表示且或非可以使用 and、ro、notkeyGenerator
:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator
接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的cacheManager
:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用cacheResolver
:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver
接口来实现自己的缓存解析器,并用该参数指定。@CachePut
:配置于函数上,用于更新缓存,所以主要用于数据新增和修改操作上。它的参数与@Cacheable
类似,具体功能可参考上面对@Cacheable
参数的解析
@CacheEvict
:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable
一样的参数之外,它还有下面两个参数:
allEntries
:非必需,默认为false。当为true时,会移除所有数据beforeInvocation
:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。@Catching
: 多个缓存注释的组合注释(不同或相同类型),在有多种缓存需求的时候使用。他里面有3个参数:cacheable、put、evict分别对应着Cacheable、CachePut、CacheEvict。简单理解就是需要操作多个缓存的时候就通过他来组合。
例如
@Caching(
cacheable = {
@Cacheable(value = "empCache",key = "#lastName")
},
put = {
@CachePut(value = "empCache",key = "#lastName"),
@CachePut(value = "empCache",key = "#result.id"),
@CachePut(value = "empCache",key = "#result.email")
}
)
在springboot中使用缓存抽象只需要添加下面的依赖,然后再启动类中添加@EnableCaching
注解,最后再需要缓存的方法上就可以直接使用spring缓存抽象提供的缓存注解了,使用缓存注解后方法的数据就会被缓存
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
如果不对cache做任何配置spring boot默认使用的自动配置类是SimpleCacheConfiguration
,而他使用的cacheManager是ConcurrentMapCacheManager
,这个manager创建的cache组件是ConcurrentMapCache
,而ConcurrentMapCache的作用是将数据保存到ConcurrentMap
中。如果我们需要使用其他的缓存,那么我们就需要进行配置
在springboot中使用ehcache缓存非常简单,只需要两步配置
<dependency>
<groupId>net.sf.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>2.10.6version>
dependency>
在 resources 目录下,添加 ehcache 的配置文件 ehcache.xml
<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="user_auth"
maxElementsInMemory="50000"
eternal="false"
timeToLiveSeconds="3600"
timeToIdleSeconds="3600"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
ehcache>
注意
默认情况下,这个文件名是固定的,必须叫 ehcache.xml ,如果一定要换一个名字,那么需要在 application.properties 中明确指定配置文件名,配置方式如下:
spring.cache.ehcache.config=classpath:aaa.xml
在Spring Boot中通过@EnableCaching
注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
因为我们提供了ehcache,所以他会自动应用ehcache缓存,直接在方法上添加的缓存注解使用的缓存就是ehcache缓存了。
注意他只兼容Ehcache2.x,并不兼容最新的Ehcach3.x。如果使用Ehcache3.x那么他会提示:
No cache manager could be auto-configured, check your configuration (caching type is 'EHCACHE')
没有可以自动配置的CacheManage。
因为Ehcache2.x相对于Ehcache3.x。不仅依赖变了包也变了。Ehcache2.x为net.sf.ehcache
,而Ehcache3.x为org.ehcache
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type
来强制指定。
使用redis缓存我们首先要做的也是导包,添加redis依赖包:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-redisartifactId>
dependency>
然后再application.yml
中增加redis配置,以本地运行为例,比如:
spring:
#---------缓存配置----------
cache:
# 使用的缓存,可以选为redis或ehcache
type: redis
#redis 配置
redis:
host: localhost
password: redis-password
port: 6379
这样我们直接在方法上添加缓存注解使用的缓存就是redis缓存了。并且他还提供了org.springframework.data.redis.core.RedisTemplate
类,具体请查看org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
源码,那里面注册了两个bean,redisTemplate 和 stringRedisTemplate,那么意味着,你可以直接使用这两个template了。所以我们除了直接使用注解还可以使用redisTemplate 来操作redis数据库。
SpringBoot2.x默认采用Lettuce客户端来连接Redis服务端的,并且在默认情况下如果我们没有配置连接池他是不会使用连接池的,如果我们要使用连接池那么需要添加连接池配置:
spring:
#---------缓存配置----------
cache:
# 使用的缓存,可以选为redis或ehcache
type: redis
#redis 配置
redis:
host: localhost
password: redis-password
port: 6379
lettuce: # 使用默认的lettuce连接池
shutdown-timeout: 5s # 关闭超时时间
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-idle: 2 # 连接池中的最大空闲连接
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
连接池如果使用lettuce他是需要依赖于commons-pool2
的所以还需要将commons-pool2的依赖导入
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
当然如果你想使用jedis连接池,那么就需要配置jedis,并且导入jedis依赖然后将lettuce依赖排除
配置:
#redis 配置
redis:
host: localhost
password: redis-password
port: 6379
jedis: # 使用默认的lettuce连接池
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-idle: 2 # 连接池中的最大空闲连接
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
同时jedis的客户端默认增加了pool的连接池依赖包,所以Jedis默认你配置与否都会有连接池,而lettuce则需要配置文件中配置一下
首先声明一点当前使用的springboot的版本为2.2.4,在版本为1.x.x的时候redis的配置是不同的。
如果是1.x.x版本的可以参考:
https://zhuanlan.zhihu.com/p/30540686
默认CacheManager的值序列化方式为org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
。这个是jdk的序列化方式序列化结果为二进制的形式,所以我们直接去redis中查看数据,数据是二进制的形式没法看的。为了我们能够直接在redis中查看数据我们需要修改序列化方式让数据存储为json的格式。所以我们需要配置CacheManager。
添加配置类,并继承自CachingConfigurerSupport类,继承这个类是因为我们可以重写key的生成规则。
/**
* @author cdfan
* @version 1.0
* @date 2020/5/10
* @description: redis 配置类
*/
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
/**
* 功能描述: redis CacheManager配置
* @param connectionFactory RedisConnectionFactory
* @return org.springframework.cache.CacheManager
* @author cdfan
* @date 2020/5/11 11:41
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
//默认CacheManager的值序列化方式为JdkSerializationRedisSerializer
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),这里最好用链式调用的方式,因为serializeValuesWith()方法会返回一个新对象,而不是作用于原对象
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
//缓存超时为1小时
.entryTtl(Duration.ofHours(1))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
//初始化RedisCacheManager
RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
return cacheManager;
}
/**
* 功能描述: 自定义key的生成规则
* @return org.springframework.cache.interceptor.KeyGenerator
* @author cdfan
* @date 2020/5/11 16:53
*/
@Override
public KeyGenerator keyGenerator() {
return (o, method, args) -> {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName()).append("#");
sb.append(method.getName()).append("(");
for (int i=0;i<args.length;i++) {
sb.append(args[i].toString());
if(i==args.length-1){
sb.append(")");
}else{
sb.append(",");
}
}
return sb.toString();
};
}
}
这样通过缓存注解,存储对象之后在redis中查看到的数据就是json格式的了,如果你要用RedisTemplate那么也需要给redisTemplate设置序列化方式, 在配置类中重新提供了一个RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//在对象序列化的时候如果对象中的属性不是基本类型,这在这个属性的json结果上添加对象类型,从而得到的JSON串的值中带有对象的类型,保证反序列化可以正常进行
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
这样序列化之后就是json的格式了,不过后来我又发现了一个问题,由于我对象中日期使用的类型是LocalDateTime
,在序列化之后他的格式是这样的:
"createUser": "admin",
"createTime": {
"date": {
"year": 2020,
"month": "MARCH",
"day": 22,
"dayOfMonth": 22,
"monthValue": 3,
"dayOfWeek": "SUNDAY",
"era": [
"java.time.chrono.IsoEra",
"CE"
],
"dayOfYear": 82,
"leapYear": true,
"chronology": {
"id": "ISO",
"calendarType": "iso8601"
},
"prolepticMonth": 24242
},
"time": {
"hour": 10,
"minute": 57,
"second": 8,
"nano": 0
},
"dayOfMonth": 22,
"hour": 10,
"minute": 57,
"monthValue": 3,
"nano": 0,
"second": 8,
"month": "MARCH",
"year": 2020,
"dayOfWeek": "SUNDAY",
"dayOfYear": 82,
"chronology": [
"java.time.chrono.IsoChronology",
{
"id": "ISO",
"calendarType": "iso8601"
}
]
},
这就会导致反序列化的时候失败,所以需要对日期的LocalDateTime类型的序列化方式进行处理。
经过查找相关资料,发现处理的方式大概有3中
使用第三种方式,我们导入新的依赖后,只需要对ObjectMapper进行设置就行了,
依赖:
<dependency>
<groupId>com.fasterxml.jackson.datatypegroupId>
<artifactId>jackson-datatype-jsr310artifactId>
<version>2.10.2version>
dependency>
具体配置,在之前ObjectMapper配置的基础上添加:
ObjectMapper om = new ObjectMapper();
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(newJavaTimeModule());
这样就可以了,这样配置之后序列化结果就是:
"createUser": "admin",
"createTime": "2020-03-22T10:57:08",
具体可以参考:
https://www.2cto.com/net/201806/755322.html
前面我们配置RedisCacheManager的时候是设置了缓存的失效时间的RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1))
,但是如果通过哪种方式配置,则设置的是默认的缓存失效时间,有的时候我们可能会需要对某个缓存单独设置缓存的失效时间,例如系统中的验证码的缓存我们不可能让他缓存1个小时。
如果需要为某些缓存设置缓存失效时间我们就需要使用自定义的缓存配置初始化一个cacheManager。废话不多说上代码:
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//配置序列化方式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
//默认缓存超时为1小时
.entryTtl(Duration.ofHours(1)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
// 设置一个初始化的缓存空间set集合
Set<String> cacheNames = new HashSet<>();
//默认缓存
cacheNames.add("default_cache");
//临时缓存
cacheNames.add("temp_cache");
// 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>(2);
configMap.put("default_cache", defaultCacheConfig);
//设置缓存失效时间为2分钟
configMap.put("temp_cache", defaultCacheConfig.entryTtl(Duration.ofMinutes(2)));
// 使用自定义的缓存配置初始化一个cacheManager
RedisCacheManager cacheManager = RedisCacheManager.builder(connectinFactory)
.initialCacheNames(cacheNames)
.withInitialCacheConfigurations(configMap)
.build();
return cacheManager;
}
在shiro中是有自己的缓存管理的,他提供了类似于Spring的Cache抽象,即Shiro本身不实现Cache,但是对Cache进行了又抽象,方便更换不同的底层Cache实现。所以他有自己的Cache
和CacheManager
接口。因此如果shiro中要集成其他的缓存那么这个缓存就得自己添加这两个接口的实现,这样在shiro中才可以使用缓存。
shiro中官方建议使用的缓存为ehcache,并且也提供了相应的整合包
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
dependency>
导入这个包之后你会发现他对org.apache.shiro.cache.Cache
和org.apache.shiro.cach.CacheManager
接口都进行了实现,分别是:org.apache.shiro.cache.ehcache.Ehcache
和org.apache.shiro.cache.ehcache.EhCacheManager
,如果你去看源码你会发现org.apache.shiro.cache.ehcache.Ehcache
虽然实现了shiro的cache
接口但实质是还是对net.sf.ehcache.Ehcache
(ehcache的cache操作类)进行了封装,所以对org.apache.shiro.cache.ehcache.Ehcache
操作的时候实际上还是对net.sf.ehcache.Ehcache
进行操作,同理org.apache.shiro.cache.ehcache.EhCacheManager
是对net.sf.ehcache.CacheManager
(ehcache中的CacheManager)进行了封装。
所以在shiro中使用ehcache很简单直接导入整合包,然后进行将org.apache.shiro.cache.ehcache.EhCacheManager
设置到securityManager中就可以使用了。
在经过查找资料发现,shiro的官方是没有给我们提供redis和shiro的整合包的。不过其他的第三方倒是提供了一个
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
dependency>
我们可以直接使用这个。或者我们可以自己参考shiro和ehcache的整合实现shiro和redis的整合,因为在springboot中当我们配置好redis之后他会提供一个redisTemplate,所以我们可以自己定义一个cache实现shiro的cache接口,在cache中通过RedisTemplate来操作redis。然后自定义一个CacheManager实现shiro的CacheManager接口,在CacheManager中管理我们的cache,所以也不是很难哈。
具体可以参考:
https://blog.csdn.net/u010514380/article/details/82185451
在将ehcache以及redis这两种缓存整合到shiro的时候我突然想到了一个更简单解决方案,而且不用进行那么多麻烦的操作,首先在项目中由于弃用了session所以在shiro需要使用缓存的地方其实就只有每次请求鉴权的时候需要使用缓存来存储用户的授权信息(具体可以查看org.apache.shiro.realm.AuthorizingRealm
中的getAuthorizationInfo
的逻辑),也就是我们自定义realm中doGetAuthorizationInfo
中返回的信息,在doGetAuthorizationInfo
方法中我们需要根据principals去数据库中获取当前登录用户所拥有的权限以及角色。如果每次请求我们都得去数据库中查询权限信息那肯定会影响性能所以我们需要将这些权限信息缓存起来。
而我们的目的无非就是将数据缓存起来不去数据库中查询,shiro缓存的是我们自定义realm中的doGetAuthorizationInfo方法中返回的结果,那么我们是不是可以直接将doGetAuthorizationInfo方法中获取权限以及角色的方法的返回结果缓存起来呢。所以其实我们只需要将我们自定义realm中的doGetAuthorizationInfo方法中获取权限以及角色的方法加上spring缓存抽象提供的缓存注解就可以啦,然后再将shiro的缓存给禁用,那么就完美的实现我的需求了,由于使用的是spring缓存抽象提供的缓存注解那么我们切换成什么类型的缓存他就会使用什么类型的缓存。所以绕了一圈最后直接弃用shiro自带的缓存是最好的选择[捂脸],不过这波也不亏哈了解了一下shiro中整合缓存的方式。所以也就顺便提了一下shiro中如何整合缓存。当然还有一点要注意的就是在登出的时候由于我们弃用了shiro中提供的session和缓存所以登出的时候我们需要手动将权限及角色的缓存给清理掉因为缓存是我们自己管理的如果登出的时候不清理一下可能会影响后续操作。
项目地址:github 、gitee、演示环境(账号/密码:admin/123456)
上一篇:独立完成系统开发七:权限之鉴权
下一篇:独立完成系统开发九:安全问题