Spring Boot进阶 之 Cache

本文的示例代码参考CacheDemo

目录

开始

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<Product, Integer> {
}
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<Product> findAll() {
        return productRepository.findAll();
    }

    public Optional<Product> 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<Product> products() {
        return productService.findAll();
    }

    @GetMapping("/{productId}")
    public Optional<Product> 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<Product> findAll() {
        return productRepository.findAll();
    }

    @Cacheable
    public Optional<Product> 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<Product> findAll() {
        return productRepository.findAll();
    }

    @Cacheable(key = "'one'")
    public Optional<Product> 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
<ehcache:config
        xmlns:ehcache="http://www.ehcache.org/v3">
    <ehcache:cache alias="products">
        <ehcache:heap unit="entries">2000</ehcache:heap>
    </ehcache:cache>
</ehcache:config>

更多配置以及说明 可以参考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
<ehcache:config
        xmlns:ehcache="http://www.ehcache.org/v3">
    <ehcache:cache alias="products">
        <ehcache:expiry>
            <ehcache:ttl unit="seconds">10</ehcache:ttl>
        </ehcache:expiry>
        <ehcache:heap unit="entries">2000</ehcache:heap>
    </ehcache:cache>
</ehcache:config>
  • 测试
./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<String> 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密集型数据

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

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

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容