JAVA笔记JAVA笔记
首页
  • Java SE

    • 事务全知道
    • 全局异常处理
    • 数据类型转换
    • Synchronized底层实现
  • Java EE

    • 网络
  • Spring

    • Spring技术总结
  • Spring Boot

    • 异步编程
    • springboot基础配置
    • springboot瘦身部署
    • 如何保证缓存与数据库的双写一致性?
    • Spring Boot中使用redis缓存
    • springboot-mybaits-druid
  • 一次jvm问题
  • JVM基础
  • JVM问题排查
  • FGC问题排查
  • 数据库
  • 数据优化
  • aop读写分离实战
  • 分布式事务
  • 分布式限流
  • 分布式Id
  • 分布式Session
  • Dubbo
首页
  • Java SE

    • 事务全知道
    • 全局异常处理
    • 数据类型转换
    • Synchronized底层实现
  • Java EE

    • 网络
  • Spring

    • Spring技术总结
  • Spring Boot

    • 异步编程
    • springboot基础配置
    • springboot瘦身部署
    • 如何保证缓存与数据库的双写一致性?
    • Spring Boot中使用redis缓存
    • springboot-mybaits-druid
  • 一次jvm问题
  • JVM基础
  • JVM问题排查
  • FGC问题排查
  • 数据库
  • 数据优化
  • aop读写分离实战
  • 分布式事务
  • 分布式限流
  • 分布式Id
  • 分布式Session
  • Dubbo
  • Spring

    • spring总结
  • Spring Boot

    • Spring Boot中使用redis缓存
    • springboot-mybaits-druid
    • springboot基础配置
    • springboot瘦身部署
    • 异步编程
    • 面试官心理分析

在程序中可以使用缓存的技术来节省对数据库的开销。Spring Boot对缓存提供了很好的支持,我们几乎不用做过多的配置即可使用各种缓存实现。这里主要介绍Redis缓存实现。

一、准备工作

可根据Spring-Boot中使用Mybatis.html搭建一个Spring Boot项目,然后yml中配置日志输出级别以观察SQL的执行情况:

logging:
  level:
    com.example.mapper: debug

其中com.example.mapper为MyBatis的Mapper接口路径。

然后编写如下测试方法:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class RedisApplicationTests {

    @Autowired
    private StudentService studentService;
    
    @Test
    public void test() throws Exception {
        Student student1 = this.studentService.queryStudentBySno("001");
        System.out.println("学号" + student1.getSno() + "的学生姓名为:" + student1.getName());
        
        Student student2 = this.studentService.queryStudentBySno("001");
        System.out.println("学号" + student2.getSno() + "的学生姓名为:" + student2.getName());
    }
}

右键run as junit test:

2020-01-22 10:05:40.019  INFO 8741 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2020-01-22 10:05:40.032 DEBUG 8741 --- [           main] c.e.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2020-01-22 10:05:40.440 DEBUG 8741 --- [           main] c.e.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2020-01-22 10:05:40.500 DEBUG 8741 --- [           main] c.e.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:KangKang
2020-01-22 10:05:40.504 DEBUG 8741 --- [           main] c.e.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2020-01-22 10:05:40.504 DEBUG 8741 --- [           main] c.e.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2020-01-22 10:05:40.506 DEBUG 8741 --- [           main] c.e.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:KangKang

可发现第二个查询虽然和第一个查询完全一样,但其还是对数据库进行了查询。接下来引入缓存来改善这个结果。

二、Redis环境搭建

Redis的下载地址为https://github.com/MicrosoftArchive/redis/releases,Redis 支持 32 位和 64 位。这个需要根据你系统平台的实际情况选择,这里我们下载 Redis-x64-xxx.zip压缩包到C盘。打开一个CMD窗口,输入如下命令:

C:\Users\Administrator>cd c:\Redis-x64-3.2.100

c:\Redis-x64-3.2.100>redis-server.exe redis.windows.conf
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 3.2.100 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 6404
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

[6404] 25 Dec 09:47:58.890 # Server started, Redis version 3.2.100
[6404] 25 Dec 09:47:58.898 * DB loaded from disk: 0.007 seconds
[6404] 25 Dec 09:47:58.898 * The server is now ready to accept connections on port 6379

然后打开另外一个CMD终端,输入:

C:\Users\Administrator>cd c:\Redis-x64-3.2.100

c:\Redis-x64-3.2.100>redis-cli.exe -p 6379
127.0.0.1:6379>

三、Redis引入

准备工作做完后,接下来开始在Spring Boot项目里引入Redis:

        <!-- spring-boot redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.4.2</version>
        </dependency>

⚠️**==注意:==**

1、这里有个坑,如果不声明版本,springboot2.0默认因为redis版本为2.*,需要同时引入commons-pool2

在 springboot 1.5.x版本的默认的Redis客户端是 Jedis实现的,springboot 2.x版本中默认客户端是用 lettuce实现的。

2、Lettuce 和 jedis 的都是连接 Redis Server的客户端,Jedis 在实现上是直连 redis server,多线程环境下非线程安全,除非使用连接池,为每个 redis实例增加 物理连接。

Lettuce 是 一种可伸缩,线程安全,完全非阻塞的Redis客户端,多个线程可以共享一个RedisConnection,它利用Netty NIO 框架来高效地管理多个连接,从而提供了异步和同步数据访问方式,用于构建非阻塞的反应性应用程序。

在application.yml中配置Redis:

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

接着在Spring Boot入口类中加入@EnableCaching注解开启缓存功能:

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

更多关于Spring Boot Redis配置可参考:https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html# REDIS

四、利用RedisTemplate使用Redis

spring-data-redis针对redis提供了如下功能:

  • ValueOperations:简单K-V操作
  • SetOperations:set类型数据操作
  • ZSetOperations:zset类型数据操作
  • HashOperations:针对map类型的数据操作
  • ListOperations:针对list类型的数据操作

针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer):

JdkSerializationRedisSerializer:是默认的序列化策略。 以unicode编码形式进行存储。

StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。 JacksonJsonRedisSerializer:jackson-json工具提供了javabean与json之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-mapper-asl工具支持】

缓存实现

要使用上Spring Boot的缓存功能,还需要提供一个缓存的具体实现。Spring Boot根据下面的顺序去侦测缓存实现:

  • Generic
  • JCache (JSR-107)
  • EhCache 2.x
  • Hazelcast
  • Infinispan
  • Redis
  • Guava
  • Simple

除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。

运行测试

控制台输出:

2017-11-17 18:17:06.995 DEBUG 8836 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-17 18:17:07.128 DEBUG 8836 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2017-11-17 18:17:07.152 DEBUG 8836 --- [main] c.s.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:KangKang
学号001的学生姓名为:KangKang

第二次查询没有访问数据库,而是从缓存中获取的,在redis中查看该值:

127.0.0.1:6379>   keys *
1) "student~keys"
2) "001"
127.0.0.1:6379> get 001
"[\"com.springboot.bean.Student\",{\"sno\":\"001\",\"name\":\"KangKang\",\"sex\":\"M \"}]"

在测试方法中测试更新:

@Test
public void test() throws Exception {
    Student student1 = this.studentService.queryStudentBySno("001");
    System.out.println("学号" + student1.getSno() + "的学生姓名为:" + student1.getName());
    
    student1.setName("康康");
    this.studentService.update(student1);
    
    Student student2 = this.studentService.queryStudentBySno("001");
    System.out.println("学号" + student2.getSno() + "的学生姓名为:" + student2.getName());
}

控制台输出:

学号001的学生姓名为:KangKang
2017-11-17 19:30:05.813  INFO 11244 --- [main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2017-11-17 19:30:05.823 DEBUG 11244 --- [main] c.s.mapper.StudentMapper.update          : ==>  Preparing: update student set sname=?,ssex=? where sno=? 
2017-11-17 19:30:05.941 DEBUG 11244 --- [main] c.s.mapper.StudentMapper.update          : ==> Parameters: 康康(String), M (String), 001(String)
2017-11-17 19:30:05.953 DEBUG 11244 --- [main] c.s.mapper.StudentMapper.update          : <==    Updates: 1
2017-11-17 19:30:05.957 DEBUG 11244 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-17 19:30:05.959 DEBUG 11244 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2017-11-17 19:30:05.976 DEBUG 11244 --- [main] c.s.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:康康

在redis中查看:

127.0.0.1:6379> get 001
"[\"com.springboot.bean.Student\",{\"sno\":\"001\",\"name\":\"\xe5\xba\xb7\xe5\xba\xb7\",\"sex\":\"M \"}]"

可见更新数据库的同时,缓存也得到了更新。

源码链接:https://github.com/wuyouzhuguli/Spring-Boot-Demos/tree/master/09.Spring-Boot-Redis-Cache

Ehcache

引入Ehcache依赖:

<!-- ehcache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

在src/main/resources目录下新建ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="ehcache.xsd">
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120" />

    <cache 
        name="student"
        maxEntriesLocalHeap="2000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="0"
        overflowToDisk="false"
        statistics="true"/>
</ehcache>

关于Ehcahe的一些说明:

  • name:缓存名称。
  • maxElementsInMemory:缓存最大数目
  • maxElementsOnDisk:硬盘最大缓存个数。
  • eternal:对象是否永久有效,一但设置了,timeout将不起作用。
  • overflowToDisk:是否保存到磁盘。
  • timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
  • timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0,也就是对象存活时间无穷大。
  • diskPersistent:是否缓存虚拟机重启期数据,默认值为false。
  • diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
  • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
  • memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
  • clearOnFlush:内存数量最大时是否清除。
  • memoryStoreEvictionPolicy:Ehcache的三种清空策略:FIFO,first in first out,这个是大家最熟的,先进先出。LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。

接着在application.yml中指定ehcache配置的路径:

spring:
  cache:
    ehcache:
      config: 'classpath:ehcache.xml'

这样就可以开始使用ehcache了,运行测试类,观察控制台:

2017-11-18 09:10:40.201 DEBUG 3364 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-18 09:10:40.343 DEBUG 3364 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2017-11-18 09:10:40.364 DEBUG 3364 --- [main] c.s.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:KangKang
学号001的学生姓名为:KangKang

可看到第二次是从缓存中获取的。

测试更新:

2017-11-18 09:18:04.230 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-18 09:18:04.397 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2017-11-18 09:18:04.427 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:KangKang
2017-11-18 09:18:04.433 DEBUG 11556 --- [main] c.s.mapper.StudentMapper.update          : ==>  Preparing: update student set sname=?,ssex=? where sno=? 
2017-11-18 09:18:04.438 DEBUG 11556 --- [main] c.s.mapper.StudentMapper.update          : ==> Parameters: 康康(String), M (String), 001(String)
2017-11-18 09:18:04.440 DEBUG 11556 --- [main] c.s.mapper.StudentMapper.update          : <==    Updates: 1
2017-11-18 09:18:04.440 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==>  Preparing: select * from student where sno=? 
2017-11-18 09:18:04.441 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : ==> Parameters: 001(String)
2017-11-18 09:18:04.442 DEBUG 11556 --- [main] c.s.m.StudentMapper.queryStudentBySno    : <==      Total: 1
学号001的学生姓名为:康康

可见,即使更新方法加了@CachePut注解,第二次查询因为Student对象更新了,其是从数据库获取数据的,所以对于Ehcache来说,更新方法加不加@CachePut注解,结果都一样。

写在最后

参考文献:

  • [https://mrbird.cc/Spring-Boot%20cache.html](https://mrbird.cc/Spring-Boot cache.html)
最近更新:: 2025/4/3 14:27
Contributors: llengleng
Next
springboot-mybaits-druid