Spring Boot 缓存

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: Spring Boot缓存《Spring Boot 实战开发》—— 基于 Gradle + Kotlin的企业级应用开发最佳实践我们知道一个系统的瓶颈通常在与数据库交互的过程中。

Spring Boot缓存

《Spring Boot 实战开发》—— 基于 Gradle + Kotlin的企业级应用开发最佳实践

我们知道一个系统的瓶颈通常在与数据库交互的过程中。内存的速度远远快于硬盘速度。所以,当我们需要重复地获取相同的数据的时候,我们一次又一次的请求数据库或者远程服务,这无疑是性能上的浪费——会导致大量的时间耗费在数据库查询或者远程方法调用上(这些资源简直太奢侈了),导致程序性能的恶化——于是有了“缓存”。缓存(Cache)就是数据交换的缓冲区。
本章介绍在 Spring Boot 项目开发中怎样来使用Spring Cache 实现数据的缓存。

1.1 Spring Cache 简介

Spring 3.1 中,Costin Leau引入了对Cache的支持。在spring-context 包中定义了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口用来统一不同的缓存的技术。其中,CacheManager是Spring提供的各种缓存技术抽象接口,Cache接口包含缓存的常用操作: 增加、删除、读取等。

针对不同的缓存技术,需要实现不同的CacheManager,Spring定义了如表所示的CacheManager实现。Spring支持的常用CacheManager如下表所示

SimpleCacheManager
使用简单的Collection来存储缓存

ConcurrentMapCacheManager
使用java.util.concurrent.ConcurrentHashMap实现的Cache

NoOpCacheManager
仅测试用,不会实际存储缓存

EhCacheCacheManager
集成使用EhCache缓存技术。EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider,也是JAVA领域应用最为广泛的缓存。

JCacheCacheManager
支持JCache(JSR-107)标准的实现作为缓存技术,如Apache Commons JCS

CaffeineCacheManager
使用Caffeine来作为缓存技术。Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。使用spring.cache.cache-names属性可以在启动时创建缓存

CompositeCacheManager
CompositeCacheManager用于组合CacheManager,即可以从多个CacheManager中轮询得到相应的Cache

Spring Cache 的使用方法和原理都类似于Spring对事务管理的支持,都是AOP的方式。其核心思想是:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。
Spring Cache 提供了@Cacheable、@CachePut、@CacheEvict等注解,在方法上使用。通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成。

1.2 Cache 注解详解

Spring 中提供了4个注解来声明缓存规则。如下表

注解
描述
@Cacheable
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

@CachePut
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
@CacheEvict
主要针对方法配置,能够根据一定的条件对缓存进行清空
@Caching
用来组合使用其他注解,可以同时应用多个Cache注解

下面我们分别来简单介绍。

  1. @Cacheable

其中,注解中的属性值说明如下:
 value: 缓存名,必填。
 key:可选属性,可以使用SPEL标签自定义缓存的key。
 condition:属性指定发生的条件。

代码示例:

@Cacheable("userList") // 标识读缓存操作
override fun findAll(): List<User> {
    return userDao.findAll()
}

@Cacheable(cacheNames = ["user"], key = "#id") // 如果缓存存在,直接读取缓存值; 如果不存在调用目标方法,并将方法返回结果放入缓存

override fun findOne(id: Long): User {
    return userDao.getOne(id)
}
  1. @CachePut

使用该注解标识的方法,每次都会执行目标逻辑代码,并将结果存入指定的缓存中。之后另一个方法就可以直接从相应的缓存中取出缓存数据,而不需要再去查询数据库。@CachePut注解的属性说明如下:

 value:缓存名,必填。
 key:可选属性,可以使用SPEL标签自定义缓存的key。

代码示例:

@Transactional
@CachePut(cacheNames = ["user"], key = "#user.id")// 写入缓存,key 为 user.id ; 一般可以标注在save方法上面
override fun saveUser(user: User): User {
    return userDao.save(user)
}
  1. @CacheEvict

标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。@CacheEvict注解属性说明如下:

 value:必填
 key:可选(默认是所有参数的组合)
 condition:缓存的条件
 allEntries:是否清空所有缓存内容,默认为 false,如果指定为 true,则方法调用后将立即清空所有缓存。
 beforeInvocation:是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。

代码示例

@Transactional
@CacheEvict(cacheNames = ["user"], key = "#id")// 根据 key (值为id) 来清除缓存 ; 一般标注在delete,update方法上面
override fun updatePassword(id: Long, password: String): Int {
    return userDao.updatePassword(id, password)
}
  1. @Caching

@Caching注解的源码如下, 从中可以看到我们可以同时使用(cacheable/put/evict方法)

public @interface Caching {
    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};
}

使用@Caching注解可以实现在同一个方法上可以同时使用多种注解,例如

@Caching(evict={@CacheEvict(“u1”),@CacheEvict(“u2”,allEntries=true)})

1.3 项目实战讲解
本节我们通过完整的项目案例来讲解 Spring Cache 的具体使用方法。
1.3.1 准备工作
1.创建项目
首先使用 Spring Initializr 创建基于 Gradle、Kotlin的 Spring Boot 项目。使用的 Kotlin 版本和 Spring Boot版本如下

kotlinVersion = '1.2.20'
springBootVersion = '2.0.1.RELEASE'

2.添加依赖
添加spring-boot-starter-cache项目依赖如下

dependencies {
  compile('org.springframework.boot:spring-boot-starter-cache')
}

3.数据库配置
本项目需要连接真实的数据库,我们使用 MySQL,同时 ORM 框架选用 JPA。所以我们在项目依赖中添加如下依赖

  runtime('mysql:mysql-connector-java')
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-web')

本地测试数据库中创建 schema如下:

CREATE SCHEMA `demo_cache` DEFAULT CHARACTER SET utf8 ;
在application.properties中配置数据库连接信息如下
spring.datasource.url=jdbc:mysql://localhost:3306/demo_cache?useUnicode=true&characterEncoding=UTF8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.jpa.database=MYSQL
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy 

4.实体类
为了简单起见,我们设计一个用户实体,包含3个字段:id,username,password。具体的代码如下

package com.easy.springboot.demo_cache

import javax.persistence.*

@Entity
class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0

    @Column(unique = true, length = 100)
    var username: String = ""
    @Column(length = 100)
    var password: String = ""

} 

5.数据访问层
使用 JPA 写 Dao 层代码是一件相当快乐的事情——不需要我们去写那么多样板化的CRUD方法。代码如下

package com.easy.springboot.demo_cache

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param

interface UserDao : JpaRepository<User, Long> {
    @Query("update #{#entityName} a set a.password = :password where a.id=:id")
    @Modifying
    fun updatePassword(@Param("id") id: Long, @Param("password") password: String): Int
}

其中,需要注意的是这里的updatePassword()函数,需要添加@Modifying注解。否则会报如下错误:

org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations [update com.easy.springboot.demo_cache.User a set a.password = :password where id=:id]
at org.hibernate.hql.internal.ast.QueryTranslatorImpl.errorIfDML(QueryTranslatorImpl.java:311)

6.业务层代码

缓存服务我们通常是在业务逻辑层来使用。我们接口定义如下

interface UserService {
    fun findAll(): List<User>
    fun saveUser(u: User): User
    fun updatePassword(id:Long, password: String): Int
    fun findOne(id: Long): User
}

对应的实现类代码是

package com.easy.springboot.demo_cache

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.CachePut
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
open class UserServiceImpl : UserService {


    @Autowired lateinit var userDao: UserDao

    @Cacheable("userList") // 标识读缓存操作
    override fun findAll(): List<User> {
        return userDao.findAll()
    }

    @Transactional
    @CachePut(cacheNames = ["user"], key = "#user.id")// 写入缓存,key 为 user.id ; 一般可以标注在save方法上面
    override fun saveUser(user: User): User {
        return userDao.save(user)
    }

    @Transactional
    @CacheEvict(cacheNames = ["user"], key = "#id")// 根据 key (值为id) 来清除缓存 ; 一般标注在delete,update方法上面
    override fun updatePassword(id: Long, password: String): Int {
        return userDao.updatePassword(id, password)
    }

    @Cacheable(cacheNames = ["user"], key = "#id") // 如果缓存存在,直接读取缓存值; 如果不存在调用目标方法,并将方法返回结果放入缓存
    override fun findOne(id: Long): User {
        return userDao.getOne(id)
    }

}

7.测试 Controller
为了看到缓存的效果,我们编写UserController代码来进行测试缓存的效果。代码如下


package com.easy.springboot.demo_cache

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@RestController
class UserController {
    @Autowired lateinit var userService: UserService

    @GetMapping("/user/list")
    fun findAll(): List<User> {
        return userService.findAll()
    }

    @GetMapping("/user/save")
    fun save(user: User): User {
        return userService.saveUser(user)
    }

    @GetMapping("/user/updatePassword")
    fun updatePassword(id: Long, password: String): Int {
        return userService.updatePassword(id, password)
    }

    @GetMapping("/user/{id}")
    fun findOne(@PathVariable("id") id: Long): User {
        return userService.findOne(id)
    }

}

8.启用Cache功能
在 Spring Boot 项目中启用 Spring Cache 注解的功能非常简单。只需要在启动类上添加@EnableCaching注解即可。实例代码如下

@SpringBootApplication
@EnableCaching
open class DemoCacheApplication

fun main(args: Array<String>) {
    ...
}
  1. 数据库初始化测试数据
    为了方便测试,我们在数据库中初始化3条用户数据进行测试。初始化代码如下
fun main(args: Array<String>) {
    SpringApplicationBuilder().initializers(
            beans {
                bean {
                    ApplicationRunner {
                        initUser()
                    }
                }
            }
    ).sources(DemoCacheApplication::class.java).run(*args)
}

private fun BeanDefinitionDsl.BeanDefinitionContext.initUser() {
    val userDao = ref<UserDao>()
    try {
        val user = User()
        user.username = "user"
        user.password = "user"
        userDao.save(user)

        val jack = User()
        jack.username = "jack"
        jack.password = "123456"
        userDao.save(jack)

        val admin = User()
        admin.username = "admin"
        admin.password = "admin"
        userDao.save(admin)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

其中,BeanDefinitionDsl 是 Spring 5中提供的基于 Kotlin 的函数式风格的 Bean 注册 DSL(Functional bean definition Kotlin DSL)。

10.运行测试

启动项目,访问http://localhost:8080/user/list ,返回


[
  {
    "id": 1,
    "username": "user",
    "password": "user"
  },
  {
    "id": 2,
    "username": "jack",
    "password": "123456"
  },
  {
    "id": 3,
    "username": "admin",
    "password": "admin"
  }
]

当我们通过调用接口http://localhost:8080/user/save?username=who&password=xxx ,向数据库中新增一条记录。我们去数据库中查看,可以发现数据新增成功。但是在此访问http://localhost:8080/user/list ,依然返回上面的3条数据。这表明下面的

@Cacheable("userList") // 标识读缓存操作
override fun findAll(): List<User>

这里findAll()函数的执行确实是走了缓存,而没有去查询数据库。
我们再来测试一下@CacheEvict与@Cacheable注解的功能。对应的是下面的这段代码

@Transactional
@CacheEvict(cacheNames = ["user"], key = "#id")// 根据 key (值为id) 来清除缓存 ; 一般标注在delete,update方法上面
override fun updatePassword(id: Long, password: String): Int {
    return userDao.updatePassword(id, password)
}

@Cacheable(cacheNames = ["user"], key = "#id") // 如果缓存存在,直接读取缓存值; 如果不存在调用目标方法,并将方法返回结果放入缓存

override fun findOne(id: Long): User {
    return userDao.getOne(id)
}

首先,访问http://localhost:8080/user/1 得到的结果是

{
  "id": 1,
  "username": "user",
  "password": "user"
}

此时,我们调用被@CacheEvict标注的updatePassword()函数,该注解会清空 id=1的缓存。访问接口http://localhost:8080/user/updatePassword?id=1&password=ppp ,返回值为1,表明成功更新1条数据。此时,我们再次访问http://localhost:8080/user/1 得到的结果是

{
  "id": 1,
  "username": "user",
  "password": "ppp"
}

这表明缓存被成功更新了。最后,我们手工去数据库修改 id=1的用户数据
UPDATE demo_cache.user SET password='mmm' WHERE id='1';
更改完成后,我们再次访问http://localhost:8080/user/1 得到的结果依然是

{
  "id": 1,
  "username": "user",
  "password": "ppp"
}

这表明,此时id=1的 User数据依然是从缓存中读取的并没有去查询数据库。

1.4 本章小结

通常情况下,使用内置的Spring Cache 只适用于单体应用。因为这些缓存的对象是存储在内存中的。在大型分布式的系统中,缓存对象往往会非常大,这个时候我们就会有专门的缓存服务器(集群)来存储这些数据了,例如 Redis。
我们可以把一些经常查询的数据放到 Redis 中缓存起来,不用每次都查询数据库。这样也不用直接占用大量内存了。关于 Redis 的使用我们将在下一章 Spring Boot 的Session统一管理中介绍。
Spring Cache对这些缓存实现都做了非常好的集成适配,所以我们使用起来可以说是“相当平滑”。另外,我们通常会使用一级缓存、二级缓存,本书限于篇幅就不详细介绍了。

提示:本章示例工程源代码https://github.com/EasySpringBoot/demo_cache

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
26天前
|
缓存 NoSQL Java
什么是缓存?如何在 Spring Boot 中使用缓存框架
什么是缓存?如何在 Spring Boot 中使用缓存框架
39 0
|
4月前
|
缓存 NoSQL Java
【Azure Redis 缓存】示例使用 redisson-spring-boot-starter 连接/使用 Azure Redis 服务
【Azure Redis 缓存】示例使用 redisson-spring-boot-starter 连接/使用 Azure Redis 服务
|
22天前
|
缓存 NoSQL Java
Spring Boot中的分布式缓存方案
Spring Boot提供了简便的方式来集成和使用分布式缓存。通过Redis和Memcached等缓存方案,可以显著提升应用的性能和扩展性。合理配置和优化缓存策略,可以有效避免常见的缓存问题,保证系统的稳定性和高效运行。
38 3
|
24天前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
37 4
|
1月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
43 2
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
76 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
存储 缓存 Java
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
238 2
|
3月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
231 24
|
2月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
87 2
|
3月前
|
存储 缓存 Java
在Spring Boot中使用缓存的技术解析
通过利用Spring Boot中的缓存支持,开发者可以轻松地实现高效和可扩展的缓存策略,进而提升应用的性能和用户体验。Spring Boot的声明式缓存抽象和对多种缓存技术的支持,使得集成和使用缓存变得前所未有的简单。无论是在开发新应用还是优化现有应用,合理地使用缓存都是提高性能的有效手段。
49 1