集成Spring Redis缓存的实现
这里的缓存主要是用于 Service 层的,所以下面的配置,都是针对 service 模块的。
本文来自内部分享,对特殊信息进行了简单处理。
本文都是在以缓存来讲 Redis 的使用,实际上 Redis 不仅仅用于缓存,本身还是 NoSQL 数据库,大家可以自己查找学习 Redis 的常用场景。
一、添加依赖
<!--缓存--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>4.3.14.RELEASE</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.10.RELEASE</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.3</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
二、配置
增加spring-redis.xml
配置文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.pool.maxIdle}"/> <property name="maxTotal" value="${redis.pool.maxTotal}"/> <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}"/> <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/> <property name="testOnReturn" value="${redis.pool.testOnReturn}"/> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="${redis.master.ip}"/> <property name="port" value="${redis.master.port}"/> <property name="poolConfig" ref="jedisPoolConfig"/> </bean> <bean id="redisKeySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <bean id="redisValueSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"/> <property name="keySerializer" ref="redisKeySerializer"/> <property name="hashKeySerializer" ref="redisKeySerializer"/> <property name="valueSerializer" ref="redisValueSerializer"/> <property name="hashValueSerializer" ref="redisValueSerializer"/> </bean> <!--在 redis.properties 配置缓存详细信息--> <util:properties id="redisExpires" location="classpath*:META-INF/spring/redis.properties"/> <bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager"> <constructor-arg index="0" ref="redisTemplate"/> <!--默认缓存 10 分钟--> <property name="defaultExpiration" value="600"/> <property name="usePrefix" value="true"/> <property name="expires" ref="redisExpires"/> </bean> <!--启用 cache 注解--> <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/> </beans>
在 src/main/resources/ 下面如果没有META-INF/spring/
目录就创建一个,然后增加 redis.properties 配置,示例如下:
# 缓存名=有效时间 halfHour=1800 oneHour=3600 oneDay=86400 webSession=1800 user=1800
除了上面配置外,在系统的 application.properties 中还需要提供下面几个配置:
# redis 连接配置 redis.master.ip=10.10.10.100 redis.master.port=6379 # redis 连接池配置 redis.pool.maxIdle=200 redis.pool.maxTotal=1024 redis.pool.maxWaitMillis=1000 redis.pool.testOnBorrow=true redis.pool.testOnReturn=true
三、通过注解方式使用缓存
示例中,redis.propreties 配置如下:
# 数据库定义,缓存 30 天 databaseDef=2592000 # 数据库元数据,缓存 1 小时 databaseMeta=3600
这个示例在数据库服务上配置的,数据库服务中,查询次数远远大于新增、修改、删除的次数,非常适合使用缓存。
1. 缓存数据 @Cacheable
@Override @Cacheable(value = "databaseDef", key = "'all'") public List<DatabaseDefinitionVo> selectAll() { return databaseDefinitionDao.selectAllVo(); }
特别注意:所有这些注解中,key 的值是 Spel 表达式,必须按照 Spel 要求来写。上面这个例子中,直接定义返回值的 key 是 all 字符串,需要加上单引号' 括起来,下面还有其他用法。
在例子中,下面的方法也使用了这个注解:
@Override @Cacheable(value = "databaseDef", key = "#id.toString()") public DatabaseDefinition selectByPrimaryKey(Long id) { Assert.notNull(id, "数据库 ID 不能为空!"); DatabaseDefinition definition = databaseDefinitionDao.selectByPrimaryKey(id); Assert.notNull(definition, "数据库定义不存在!"); return definition; }
在上面注解中,key
中的 #id
指的是参数中的 id,在 IDEA 中会有自动提示。.toString()
是调用 id
的方法,在系统中规定了 key 必须是字符串类型,所以当类型是 Long 的时候,需要转换。
使用缓存的目的就是为了减少上面两个方法调用时减少和数据库的交互,减小数据库的压力,这是两个主要的缓存数据的方法,下面的几个操作都和上面这两个方法有一定的关系。
重点:这里以及下面几个注解中,都指定了
value = "databaseDef"
,这里的意思是说,要使用前面配置中的 databaseDef 对应的配置,也就是会缓存 30天。
2. 更新缓存 @CachePut
@Override @CachePut(value = "databaseDef", key = "#result.id.toString()") public DatabaseDefinition save(DatabaseDefinition definition, CurrentUser userModel) throws ServiceException { //代码 return definition; }
更新缓存的方法需要注意的是返回值,在上面 save 方法中,有可能是新增,有可能是更新,不管是那个操作,当操作完成后,上面注解会根据 key 的值生成 key,然后将方法的返回值作为 value 保存到缓存中。
这里 key 的写法中 #result 指代返回值,.id 是返回值的属性,.toString() 是调用 id 属性的方法,在系统中规定了 key 必须是字符串类型,所以当类型是 Long 的时候,需要转换。
这个方法上加的缓存还有问题,当新增或者更新后,通过 selectAll() 返回的值已经发生了变化,但是这里没有清除 all 的缓存值,会导致 selectAll() 出现脏数据,下面会通过 @Caching 注解改造这里。
3. 清除缓存 @CacheEvict
@Override @CacheEvict(value = "databaseDef", key = "#id.toString()") public void deleteByPrimaryKey(Long id) throws ServiceException { DatabaseDefinition definition = selectByPrimaryKey(id); if (definition.getLoadState().equals(DatabaseDefinition.LoadState.UP)) { throw new ServiceException("请先卸载数据库!"); } databaseDefinitionDao.deleteByPrimaryKey(id); }
在上面新增或者修改的时候根据 id 缓存或者更新了缓存数据,这里当删除数据的时候,还需要清空对应的缓存数据。
在上面注解中,key 中的 #id 指的是参数中的 id,在 IDEA 中会有自动提示。
这个方法上加的缓存还有问题,当删除后,通过 selectAll() 返回的值已经发生了变化,但是这里没有清除 all 的缓存值,会导致 selectAll() 出现脏数据,下面会通过 @Caching 注解改造这里。
4. 组合使用 @Caching
上面两个注解中,都提到了脏数据,通过 @Caching 注解可以解决这个问题。
先修改第二个注解,来解决 save 时的脏数据:
@Override @Caching(put = @CachePut(value = "databaseDef", key = "#result.id.toString()"), evict = @CacheEvict(value = "databaseDef", key = "'all'")) public DatabaseDefinition save(DatabaseDefinition definition, CurrentUser userModel) throws ServiceException { //其他代码 return definition; }
前面说明,新增或者修改的时候,all 缓存中的数据已经不对了,因此这里在 put 的同时,使用 evict 将 'all' 中的数据清除,这就保证了 selelctAll 下次调用时,会重新从库中读取数据。
对上面的删除方法,也进行类似的修改:
@Override @Caching(evict = { @CacheEvict(value = "databaseDef", key = "#id.toString()"), @CacheEvict(value = "databaseDef", key = "'all'") }) public void deleteByPrimaryKey(Long id) throws ServiceException { DatabaseDefinition definition = selectByPrimaryKey(id); if (definition.getLoadState().equals(DatabaseDefinition.LoadState.UP)) { throw new ServiceException("请先卸载数据库!"); } databaseDefinitionDao.deleteByPrimaryKey(id); }
注意这里的 evict 是个数组,里面配置了两个清除缓存的配置。
5. 全局配置 @CacheConfig
在上面所有例子中,都指定了 value = "databaseDef",实际上可以通过在类上使用 @CacheConfig 注解配置当前类中的 cacheNames 值,配置后,如果和类上的 value 一样就不需要在每个注解单独配置。只有不同时再去指定,方法上的 value 值优先级更高。
@Service @CacheConfig(cacheNames = "databaseDef") public class DatabaseDefinitionServiceImpl implements DatabaseDefinitionService, DatabaseSqlExecuteService, ApplicationListener {
有了上面配置后,其他缓存名字相同的地方可以简化,例如删除方法修改后如下:
@Override @Caching(evict = { @CacheEvict(key = "#id.toString()"), @CacheEvict(key = "'all'") }) public void deleteByPrimaryKey(Long id) throws ServiceException { DatabaseDefinition definition = selectByPrimaryKey(id); if (definition.getLoadState().equals(DatabaseDefinition.LoadState.UP)) { throw new ServiceException("请先卸载数据库!"); } databaseDefinitionDao.deleteByPrimaryKey(id); }
其他例子
除了上面针对 databaseDef 的缓存外,还有 databaseMeta 的配置:
@Override @Cacheable(value = "databaseMeta", key = "#databaseId.toString()") public List<TableVo> selectTablesByDatabaseId(Long databaseId) throws Exception { //代码 } @Override @Cacheable(value = "databaseMeta", key = "#databaseId + '_' + #tableName") public TableVo selectTableByDatabaseIdAndTableName(Long databaseId, String tableName) throws Exception { //代码 }
这两个方法是获取数据库元数据的,只有修改数据库表的时候才会变化,因此不存在清除缓存的情况,但是万一修改表后,想要新的元数据,该怎么办?
因此增加了一个空的方法来清空数据,方法如下:
@Override @CacheEvict(value = "databaseMeta", allEntries = true) public void cleanTablesCache() { }
这里指定了 databaseMeta,通过 allEntries = true 清空所有 key 的缓存。通过这个方法可以保证在有需要的时候清空所有元数据的缓存。
实际上如果想要更精确的清除,可以传入要清除的 databaseId 和 tableName 来更精确的清除。
增加缓存后的效果
调用 selectTablesByDatabaseId 多次时,输出的日志如下:
INFO c.n.d.u.DynamicDataSource - 当前数据源:默认数据源
DEBUG c.n.d.DataScopeContextProviderFilter - 服务调用返回前清除数据权限信息
INFO c.n.d.u.DynamicDataSource - 当前数据源:默认数据源
DEBUG c.n.d.d.D.selectByPrimaryKey - ==> Preparing: SELECT xxx (隐藏完整 SQL)
DEBUG c.n.d.d.D.selectByPrimaryKey - ==> Parameters: 1(Long)
DEBUG c.n.d.d.D.selectByPrimaryKey - <== Total: 1
INFO c.n.d.u.DynamicDataSource - 当前数据源:10.10.10.130/datareporting
DEBUG c.n.d.DataScopeContextProviderFilter - 服务调用返回前清除数据权限信息
INFO c.n.d.u.DynamicDataSource - 当前数据源:默认数据源
DEBUG c.n.d.DataScopeContextProviderFilter - 服务调用返回前清除数据权限信息
INFO c.n.d.u.DynamicDataSource - 当前数据源:默认数据源
DEBUG c.n.d.DataScopeContextProviderFilter - 服务调用返回前清除数据权限信息
从日志可以看出来,只有第一次进行了数据库查询,后续通过日志看不到数据库操作。
调用清空缓存后,会再次查询数据库。
初次调用时,WEB请求花了 700多ms,后面再次调用时,平均不到 30 ms,这就是缓存最明显的作用。
连接到 redis 服务后,查看所有 key,结果如下:
redis@redissvr:~$ redis-cli
127.0.0.1:6379> keys *
1) "databaseMeta:1"
2) "databaseDef:all"
127.0.0.1:6379>
缓存中的数据都有 value 前缀,上面缓存了 all 和 id 为 1 的数据。
缓存注解是一种最简单的缓存方式,但是需要配合 value 属性的配置来使用,许多时候我们可能需要更精确的控制缓存,此时可以使用 RedisTemplate 来控制。
四、通过 RedisTemplate 使用缓存
有关这部分的详细用法可以从网上搜索相关内容进行学习,这里列举一个简单的例子。
针对前面的 selectAll 我们换一种方式进行缓存。
首先注入下面的接口:
@Resource(name = "redisTemplate") private ValueOperations<String, List> valueOper;
修改 selectAll 方法如下:
@Override //@Cacheable(key = "'all'") public List<DatabaseDefinitionVo> selectAll() { List<DatabaseDefinitionVo> vos = valueOper.get("databaseDef:all"); if(vos != null){ return vos; } vos = databaseDefinitionDao.selectAllVo(); //缓存 1 小时 valueOper.set("databaseDef:all", vos, 1, TimeUnit.HOURS); return vos; }
首先通过valueOper.get("databaseDef:all")
尝试获取缓存信息,如果存在就直接返回。
如果不存在,就查询数据库,然后将查询结果通过 set 进行缓存。
特别注意: 上面的 key,写的是 "databaseDef:all",也就是前缀需要自己加上,如果直接写成 all,在 Redis 中的 key 就是 all,不会自动增加前缀。
如果没有前缀,那么当不同系统都使用 all 时,数据就会混乱!
五、Redis 服务器配置注意事项
内网使用的服务器,特殊配置如下:
# By default protected mode is enabled. You should disable it only if # you are sure you want clients from other hosts to connect to Redis # even if no authentication is configured, nor a specific set of interfaces # are explicitly listed using the "bind" directive. # protected-mode yes protected-mode no
关闭了保护模式。
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES # JUST COMMENT THE FOLLOWING LINE. # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # bind 127.0.0.1
注释了绑定的 IP,这样可以让所有电脑访问 Redis。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接