集成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。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • gem install redis报错的解决方案

    在使用ruby脚本安装Redis集群时,需要先安装Ruby语言环境和redis插件,但是安装redis插件时遇到以下报错,下面记录一下解决过程. 因为执行Ruby脚本需要Ruby语言环境,所以首先安装Ruby语言环境和Ruby的包管理器Gems. 然后使用gem安装Redis和Ruby的接口. RubyGems 是 Ruby 的一个包管理器,它提供一个分发 Ruby 程序和库的标准格式,还提供一个管理程序包安装的工具. RubyGems 旨在方便地管理 gem 安装的工具,以及用于分发 gem

  • redis锁机制介绍与实例

    1 悲观锁 执行操作前假设当前的操作肯定(或有很大几率)会被打断(悲观).基于这个假设,我们在做操作前就会把相关资源锁定,不允许自己执行期间有其他操作干扰. Redis不支持悲观锁.Redis作为缓存服务器使用时,以读操作为主,很少写操作,相应的操作被打断的几率较少.不采用悲观锁是为了防止降低性能. 2 乐观锁 执行操作前假设当前操作不会被打断(乐观).基于这个假设,我们在做操作前不会锁定资源,万一发生了其他操作的干扰,那么本次操作将被放弃. 3. Redis中的锁策略 Redis采用了乐观锁策

  • 在Java中使用redisTemplate操作缓存的方法示例

    背景 在最近的项目中,有一个需求是对一个很大的数据库进行查询,数据量大概在几千万条.但同时对查询速度的要求也比较高. 这个数据库之前在没有使用Presto的情况下,使用的是Hive,使用Hive进行一个简单的查询,速度可能在几分钟.当然几分钟也并不完全是跑SQL的时间,这里面包含发请求,查询数据并且返回数据的时间的总和.但是即使这样,这样的速度明显不能满足交互式的查询需求. 我们的下一个解决方案就是Presto,在使用了Presto之后,查询速度降到了秒级.但是对于一个前端查询界面的交互式查询来

  • 使用Ruby脚本部署Redis Cluster集群步骤讲解

    安装Ruby和Gem 下载ruby wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.8.tar.gz 解压 tar xvf ruby-2.3.8.tar.gz 生成Makefile并且后面会被安装到/usr/local/ruby目录下 ./configure -prefix /usr/local/ruby 编译 make 安装 make install cd /usr/local/ruby cp bin/ruby /usr/local

  • Redis Cluster的图文讲解

    1.1 Redis-Cluster简介 1.1.1 什么是Redis-Cluster 为何要搭建Redis集群.Redis是在内存中保存数据的,而我们的电脑一般内存都不大,这也就意味着Redis不适合存储大数据,适合存储大数据的是Hadoop生态系统的Hbase或者是MogoDB.Redis更适合处理高并发,一台设备的存储能力是很有限的,但是多台设备协同合作,就可以让内存增大很多倍,这就需要用到集群. Redis集群搭建的方式有多种,例如使用客户端分片.Twemproxy.Codis等,但从re

  • SpringBoot AOP控制Redis自动缓存和更新的示例

    导入redis的jar包 <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.0.4.RELEASE</version> </dependency> 编写自定义缓存注解 /**

  • php成功操作redis cluster集群的实例教程

    前言 java操作redis cluster集群可使用jredis php要操作redis cluster集群有两种方式: 1.使用phpredis扩展,这是个c扩展,性能更高,但是phpredis2.x扩展不行,需升级phpredis到3.0,但这个方案参考资料很少 2.使用predis,纯php开发,使用了命名空间,需要php5.3+,灵活性高 我用的是predis,下载地址:点击这里 步骤如下: 下载好后重命名为predis, server1:192.168.1.198 server2:1

  • springboot与redis的简单整合实例

    前言 Redis是一个缓存.消息代理和功能丰富的键值存储.StringBoot提供了基本的自动配置.本文记录一下springboot与redis的简单整合实例 官方文档:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/ 前期准备 首先我们要有一个Redis服务,由于我没有Linux环境,为了方便搭建项目,直接在Windows下安装,参考这篇博客:Windows下安装Redis服务 安装步骤:一直

  • Redis cluster集群的介绍

    1.前言 Redis集群模式主要有2种: 主从集群.分布式集群. 前者主要是为了高可用或是读写分离,后者为了更好的存储数据,负载均衡. redis集群提供了以下两个好处 1.将数据自动切分(split)到多个节点 2.当集群中的某一个节点故障时,redis还可以继续处理客户端的请求. 一个 redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个数据都属于这16384个哈希槽中的一个.集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽.集群中

  • redis持久化的介绍

    1. RDB 1.1 RDB简介 RDB:在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里. 工作机制:每隔一段时间,就把内存中的数据保存到硬盘上的指定文件中. RDB是默认开启的! Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件.整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对

随机推荐