Spring Boot进阶 之 Cache

本文的示例代码参考CacheDemo

目录

  • 开始

    • 添加Model

    • 添加Service

    • 添加Controller

  • 数据库

    • 数据库配置

    • 数据库服务

    • 数据库迁移

  • Cache使用

    • 添加Cache

    • 打开Cache

    • 使用Cache

  • Cache配置

    • 配置Key

    • 配置Ehcache

    • 配置Expiry

  • Cache清除

  • 再谈Cache

    • Cache原理

    • Cache缺点

    • Cache场景

开始

spring init -dweb,mysql,data-jpa,flyway,lombok --build gradle CacheDemo && cd CacheDemo

关于Flyway和DB Migration 详细参考Spring Boot开发 之 DB Migration

添加Model

vim src/main/java/com/example/CacheDemo/Product.java
package com.example.CacheDemo;

import lombok.Data;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;

@Entity
@Table(name = "products")
@Data
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotBlank
    private String productName;
}

关于Lombok更多介绍 可以参考Lombok简介

添加Service

vim src/main/java/com/example/CacheDemo/ProductRepository.java
package com.example.CacheDemo;

import org.springframework.data.repository.CrudRepository;

public interface ProductRepository extends CrudRepository {
}
vim src/main/java/com/example/CacheDemo/ProductService.java
package com.example.CacheDemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    public Iterable findAll() {
        return productRepository.findAll();
    }

    public Optional findById(Integer id) {
        return productRepository.findById(id);
    }
}

添加Controller

vim src/main/java/com/example/CacheDemo/ProductsController.java
package com.example.CacheDemo;

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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RestController
@RequestMapping("/products")
public class ProductsController {
    @Autowired
    private ProductService productService;

    @GetMapping
    public Iterable products() {
        return productService.findAll();
    }

    @GetMapping("/{productId}")
    public Optional product(@PathVariable("productId") String productId) {
        return productService.findById(Integer.valueOf(productId));
    }
}

数据库

数据库配置

vim src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/cache?userSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

数据库服务

docker run --name cache-demo -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7.17

docker exec -i cache-demo mysql -uroot -p123456  <<< "CREATE DATABASE IF NOT EXISTS cache DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;"

数据库迁移

mkdir -p src/main/resources/db/migration

vim src/main/resources/db/migration/V1_0_0__create_product_table.sql
CREATE TABLE products (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  product_name varchar(100) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY UK_product_name (product_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO products (product_name) VALUES ('Product01');

INSERT INTO products (product_name) VALUES ('Product02');

关于Flyway和DB Migration 详细参考Spring Boot开发 之 DB Migration

  • 测试
./gradlew bootrun
curl localhost:8080/products | json
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
curl localhost:8080/products/1 | json
{
  "id": 1,
  "productName": "Product01"
}
curl localhost:8080/products/2 | json
{
  "id": 2,
  "productName": "Product02"
}

这里使用nodejs的json工具格式化数据: npm i -g json

Cache使用

添加Cache

vim build.gradle
dependencies {
    compile("org.springframework.boot:spring-boot-starter-cache")
}

打开Cache

vim src/main/java/com/example/CacheDemo/DemoApplication.java
package com.example.CacheDemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

使用Cache

vim src/main/java/com/example/CacheDemo/ProductService.java
package com.example.CacheDemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@CacheConfig(cacheNames = "products")
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Cacheable
    public Iterable findAll() {
        return productRepository.findAll();
    }

    @Cacheable
    public Optional findById(Integer id) {
        return productRepository.findById(id);
    }
}
  • 测试用例1

为了了解Cache过程 首先需要打开SQL调试如下

echo "spring.jpa.show-sql=true" >> src/main/resources/application.properties
./gradlew bootrun
# 第一次
curl localhost:8080/products | json
# Hibernate: select product0_.id as id1_0_, product0_.product_name as product_2_0_ from products product0_
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
# 第二次
curl localhost:8080/products | json
# 没有Hibernate SQL语句
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
  • 测试用例2
# 第一次
curl localhost:8080/products/1 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 1,
  "productName": "Product01"
}
# 第二次
curl localhost:8080/products/1 | json
# 没有Hibernate SQL语句
{
  "id": 1,
  "productName": "Product01"
}
  • 测试用例3
# 第一次
curl localhost:8080/products/2 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 2,
  "productName": "Product02"
}
# 第二次
curl localhost:8080/products/2 | json
# 没有Hibernate SQL语句
{
  "id": 2,
  "productName": "Product02"
}

Cache配置

配置Key

默认配置会按照函数的所有参数组合作为key值

vim src/main/java/com/example/CacheDemo/ProductService.java
package com.example.CacheDemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@CacheConfig(cacheNames = "products")
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Cacheable(key = "'all'")
    public Iterable findAll() {
        return productRepository.findAll();
    }

    @Cacheable(key = "'one'")
    public Optional findById(Integer id) {
        return productRepository.findById(id);
    }
}
  • 测试用例1
./gradlew bootrun
# 第一次
curl localhost:8080/products | json
# Hibernate: select product0_.id as id1_0_, product0_.product_name as product_2_0_ from products product0_
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
# 第二次
curl localhost:8080/products | json
# 没有Hibernate SQL语句
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
  • 测试用例2
# 第一次
curl localhost:8080/products/1 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 1,
  "productName": "Product01"
}
# 第二次
curl localhost:8080/products/1 | json
# 没有Hibernate SQL语句
{
  "id": 1,
  "productName": "Product01"
}
  • 测试用例3
# 第一次
curl localhost:8080/products/2 | json
# 没有Hibernate SQL语句
{
  "id": 1,
  "productName": "Product01"
}
# 第二次
curl localhost:8080/products/2 | json
# 没有Hibernate SQL语句
{
  "id": 1,
  "productName": "Product01"
}
sed -i "" "s/'one'/#p0/g" src/main/java/com/example/CacheDemo/ProductService.java
  • 测试用例1
./gradlew bootrun
# 第一次
curl localhost:8080/products | json
# Hibernate: select product0_.id as id1_0_, product0_.product_name as product_2_0_ from products product0_
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
# 第二次
curl localhost:8080/products | json
# 没有Hibernate SQL语句
[
  {
    "id": 1,
    "productName": "Product01"
  },
  {
    "id": 2,
    "productName": "Product02"
  }
]
  • 测试用例2
# 第一次
curl localhost:8080/products/1 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 1,
  "productName": "Product01"
}
# 第二次
curl localhost:8080/products/1 | json
# 没有Hibernate SQL语句
{
  "id": 1,
  "productName": "Product01"
}
  • 测试用例3
# 第一次
curl localhost:8080/products/2 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 2,
  "productName": "Product02"
}
# 第二次
curl localhost:8080/products/2 | json
# 没有Hibernate SQL语句
{
  "id": 2,
  "productName": "Product02"
}

配置Ehcache

vim build.gradle
dependencies {
    compile('org.ehcache:ehcache:3.4.0')
    compile('javax.cache:cache-api')
}
vim src/main/resources/ehcache.xml

    
        2000
    

更多配置以及说明 可以参考Examples

echo "spring.cache.jcache.config=classpath:ehcache.xml" >> src/main/resources/application.properties
  • 测试
./gradlew bootrun
[           main] org.ehcache.xml.XmlConfiguration         : Loading Ehcache XML configuration from ***/CacheDemo/build/resources/main/ehcache.xml.
[           main] org.ehcache.core.EhcacheManager          : Cache 'products' created in Eh107InternalCacheManager.
[           main] org.ehcache.jsr107.Eh107CacheManager     : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=file.***/CacheDemo/build/resources/main/ehcache.xml,Cache=products
[           main] org.ehcache.jsr107.Eh107CacheManager     : Registering Ehcache MBean javax.cache:type=CacheStatistics,CacheManager=file.***/CacheDemo/build/resources/main/ehcache.xml,Cache=products

配置Expiry

sed -i "" "s/#p0/'one'/g" src/main/java/com/example/CacheDemo/ProductService.java
vim src/main/resources/ehcache.xml

    
        
            10
        
        2000
    

  • 测试
./gradlew bootrun
# 第一次
curl localhost:8080/products/1 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 1,
  "productName": "Product01"
}
# 第二次
curl localhost:8080/products/2 | json
# 没有Hibernate SQL语句
{
  "id": 1,
  "productName": "Product01"
}
# 第三次 (第一次请求10秒钟后)
curl localhost:8080/products/2 | json
# Hibernate: select product0_.id as id1_0_0_, product0_.product_name as product_2_0_0_ from products product0_ where product0_.id=?
{
  "id": 2,
  "productName": "Product02"
}

Cache清除

vim src/main/java/com/example/CacheDemo/CacheController.java
package com.example.CacheDemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CacheController {

    @Autowired
    private CacheManager cacheManager;

    @GetMapping(path = "/clear-cache")
    public String clearCache() {
        Iterable cacheNames = cacheManager.getCacheNames();
        System.out.println(cacheNames.toString());
        for (String cacheName : cacheNames) {
            cacheManager.getCache(cacheName).clear();
        }
        return "clear-cache";
    }
}
  • 测试
./gradlew bootrun
curl localhost:8080/products/1 | json
{
  "id": 1,
  "productName": "Product01"
}
curl localhost:8080/clear-cache
# 打印"[products]" & 返回"clear-cache"
curl localhost:8080/products/2 | json
{
  "id": 2,
  "productName": "Product02"
}

再谈Cache

Cache原理

高速存储代替慢速存储

小结果集快速查询代替大结果集普通查询

Cache缺点

高数据一致性导致低性能

高性能导致低数据一致性

Cache场景

更新不频繁的I/O密集型数据

增强系统可靠性 但同样需要预防缓存穿透和缓存雪崩的问题

关于更多"缓存穿透和缓存雪崩" 可以参考本文参考附录

参考

  • A Guide To Caching in Spring

  • Spring Boot中的缓存支持(一)注解配置与EhCache使用

  • Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用

  • Spring 4.3.7 and ehcache 3.3.1 can't autowire cache manager

  • 缓存穿透、雪崩、热点与Redis

  • 缓存穿透和缓存雪崩

  • 缓存雪崩问题

你可能感兴趣的:(Spring Boot进阶 之 Cache)