目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

Spring for Redis

Spring 对 Redis 的支持

 Redis 是 ⼀款开源的内存 KV 存储,⽀持多种数据结构。
 Spring 对 Redis 的支持是通过 Spring Data Redis 项目。

  • Jedis / Lettuce
  • RedisTemplate
  • Repository

Reference

Docker 启动 Redis 容器

Reference

1# 拉取image
2docker pull redis
3
4# 启动 Redis
5docker run -p 6379:6379 --name myredis -d redis
6
7# 设置密码添加 --requirepass 参数
8docker run -p 6379:6379 --name myredis -d redis --requirepass "123456"

Jedis 客户端

  • Jedis 不是线程安全的:无法在多个线程之间共享同一个 Jedis 实例。
  • 通过 JedisPool 获得 Jedis 实例
  • 直接使用 Jedis 中的方法

引入依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.2.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring</groupId>
12	<artifactId>springbucks</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>springbucks</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-data-jpa</artifactId>
25		</dependency>
26
27		<dependency>
28			<groupId>redis.clients</groupId>
29			<artifactId>jedis</artifactId>
30		</dependency>
31
32		<dependency>
33			<groupId>org.joda</groupId>
34			<artifactId>joda-money</artifactId>
35			<version>1.0.1</version>
36		</dependency>
37		<dependency>
38			<groupId>org.jadira.usertype</groupId>
39			<artifactId>usertype.core</artifactId>
40			<version>6.0.1.GA</version>
41		</dependency>
42
43		<dependency>
44			<groupId>com.h2database</groupId>
45			<artifactId>h2</artifactId>
46			<scope>runtime</scope>
47		</dependency>
48		<dependency>
49			<groupId>org.projectlombok</groupId>
50			<artifactId>lombok</artifactId>
51			<optional>true</optional>
52		</dependency>
53		<dependency>
54			<groupId>org.springframework.boot</groupId>
55			<artifactId>spring-boot-starter-test</artifactId>
56			<scope>test</scope>
57		</dependency>
58	</dependencies>
59
60	<build>
61		<plugins>
62			<plugin>
63				<groupId>org.springframework.boot</groupId>
64				<artifactId>spring-boot-maven-plugin</artifactId>
65			</plugin>
66		</plugins>
67	</build>
68</project>

application.properties

1spring.jpa.hibernate.ddl-auto=none
2spring.jpa.properties.hibernate.show_sql=true
3spring.jpa.properties.hibernate.format_sql=true
4
5spring.redis.host=192.168.31.201
6spring.redis.port=6379
7spring.redis.jedis.pool.max-active=3
8spring.redis.jedis.pool.max-idle=3

schema.sql

 1drop table t_coffee if exists;
 2drop table t_order if exists;
 3drop table t_order_coffee if exists;
 4
 5create table t_coffee (
 6    id bigint auto_increment,
 7    create_time timestamp,
 8    update_time timestamp,
 9    name varchar(255),
10    price bigint,
11    primary key (id)
12);
13
14create table t_order (
15    id bigint auto_increment,
16    create_time timestamp,
17    update_time timestamp,
18    customer varchar(255),
19    state integer not null,
20    primary key (id)
21);
22
23create table t_order_coffee (
24    coffee_order_id bigint not null,
25    items_id bigint not null
26);
27
28insert into t_coffee (name, price, create_time, update_time) values ('espresso', 2000, now(), now());
29insert into t_coffee (name, price, create_time, update_time) values ('latte', 2500, now(), now());
30insert into t_coffee (name, price, create_time, update_time) values ('capuccino', 2500, now(), now());
31insert into t_coffee (name, price, create_time, update_time) values ('mocha', 3000, now(), now());
32insert into t_coffee (name, price, create_time, update_time) values ('macchiato', 3000, now(), now());

BaseEntity

 1package geektime.spring.springbucks.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Data;
 5import lombok.NoArgsConstructor;
 6import org.hibernate.annotations.CreationTimestamp;
 7import org.hibernate.annotations.UpdateTimestamp;
 8
 9import javax.persistence.Column;
10import javax.persistence.GeneratedValue;
11import javax.persistence.GenerationType;
12import javax.persistence.Id;
13import javax.persistence.MappedSuperclass;
14import java.io.Serializable;
15import java.util.Date;
16
17@MappedSuperclass
18@Data
19@NoArgsConstructor
20@AllArgsConstructor
21public class BaseEntity implements Serializable {
22    @Id
23    @GeneratedValue(strategy = GenerationType.IDENTITY)
24    private Long id;
25    @Column(updatable = false)
26    @CreationTimestamp
27    private Date createTime;
28    @UpdateTimestamp
29    private Date updateTime;
30}

Coffee

 1package geektime.spring.springbucks.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import lombok.ToString;
 8import org.hibernate.annotations.Type;
 9import org.joda.money.Money;
10
11import javax.persistence.Entity;
12import javax.persistence.Table;
13import java.io.Serializable;
14
15@Entity
16@Table(name = "T_COFFEE")
17@Builder
18@Data
19@ToString(callSuper = true)
20@NoArgsConstructor
21@AllArgsConstructor
22public class Coffee extends BaseEntity implements Serializable {
23    private String name;
24    @Type(type = "org.jadira.usertype.moneyandcurrency.joda.PersistentMoneyMinorAmount",
25            parameters = {@org.hibernate.annotations.Parameter(name = "currencyCode", value = "CNY")})
26    private Money price;
27}

CoffeeOrder

 1package geektime.spring.springbucks.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import lombok.ToString;
 8
 9import javax.persistence.Column;
10import javax.persistence.Entity;
11import javax.persistence.Enumerated;
12import javax.persistence.JoinTable;
13import javax.persistence.ManyToMany;
14import javax.persistence.OrderBy;
15import javax.persistence.Table;
16import java.io.Serializable;
17import java.util.List;
18
19@Entity
20@Table(name = "T_ORDER")
21@Data
22@ToString(callSuper = true)
23@NoArgsConstructor
24@AllArgsConstructor
25@Builder
26public class CoffeeOrder extends BaseEntity implements Serializable {
27    private String customer;
28    @ManyToMany
29    @JoinTable(name = "T_ORDER_COFFEE")
30    @OrderBy("id")
31    private List<Coffee> items;
32    @Enumerated
33    @Column(nullable = false)
34    private OrderState state;
35}

OrderState

1package geektime.spring.springbucks.model;
2
3public enum OrderState {
4    INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
5}

CoffeeOrderRepository

1package geektime.spring.springbucks.repository;
2
3import geektime.spring.springbucks.model.CoffeeOrder;
4import org.springframework.data.jpa.repository.JpaRepository;
5
6public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
7}

CoffeeRepository

1package geektime.spring.springbucks.repository;
2
3import geektime.spring.springbucks.model.CoffeeOrder;
4import org.springframework.data.jpa.repository.JpaRepository;
5
6public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
7}

CoffeeOrderService

 1package geektime.spring.springbucks.service;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.model.CoffeeOrder;
 5import geektime.spring.springbucks.model.OrderState;
 6import geektime.spring.springbucks.repository.CoffeeOrderRepository;
 7import lombok.extern.slf4j.Slf4j;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.stereotype.Service;
10
11import javax.transaction.Transactional;
12import java.util.ArrayList;
13import java.util.Arrays;
14
15@Slf4j
16@Service
17@Transactional
18public class CoffeeOrderService {
19    @Autowired
20    private CoffeeOrderRepository orderRepository;
21
22    public CoffeeOrder createOrder(String customer, Coffee...coffee) {
23        CoffeeOrder order = CoffeeOrder.builder()
24                .customer(customer)
25                .items(new ArrayList<>(Arrays.asList(coffee)))
26                .state(OrderState.INIT)
27                .build();
28        CoffeeOrder saved = orderRepository.save(order);
29        log.info("New Order: {}", saved);
30        return saved;
31    }
32
33    public boolean updateState(CoffeeOrder order, OrderState state) {
34        if (state.compareTo(order.getState()) <= 0) {
35            log.warn("Wrong State order: {}, {}", state, order.getState());
36            return false;
37        }
38        order.setState(state);
39        orderRepository.save(order);
40        log.info("Updated Order: {}", order);
41        return true;
42    }
43}

CoffeeService

 1package geektime.spring.springbucks.service;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.repository.CoffeeRepository;
 5import lombok.extern.slf4j.Slf4j;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.data.domain.Example;
 8import org.springframework.data.domain.ExampleMatcher;
 9import org.springframework.stereotype.Service;
10
11import java.util.List;
12import java.util.Optional;
13
14import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
15
16@Slf4j
17@Service
18public class CoffeeService {
19    @Autowired
20    private CoffeeRepository coffeeRepository;
21
22    public List<Coffee> findAllCoffee() {
23        return coffeeRepository.findAll();
24    }
25
26    public Optional<Coffee> findOneCoffee(String name) {
27        ExampleMatcher matcher = ExampleMatcher.matching()
28                .withMatcher("name", exact().ignoreCase());
29        Optional<Coffee> coffee = coffeeRepository.findOne(
30                Example.of(Coffee.builder().name(name).build(), matcher));
31        log.info("Coffee Found: {}", coffee);
32        return coffee;
33    }
34}

SpringBucksApplication 启动类

 1package geektime.spring.springbucks;
 2
 3import geektime.spring.springbucks.service.CoffeeService;
 4import lombok.extern.slf4j.Slf4j;
 5import org.joda.money.CurrencyUnit;
 6import org.joda.money.Money;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.beans.factory.annotation.Value;
 9import org.springframework.boot.ApplicationArguments;
10import org.springframework.boot.ApplicationRunner;
11import org.springframework.boot.SpringApplication;
12import org.springframework.boot.autoconfigure.SpringBootApplication;
13import org.springframework.boot.context.properties.ConfigurationProperties;
14import org.springframework.context.annotation.Bean;
15import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
16import org.springframework.transaction.annotation.EnableTransactionManagement;
17import redis.clients.jedis.Jedis;
18import redis.clients.jedis.JedisPool;
19import redis.clients.jedis.JedisPoolConfig;
20
21import java.util.Map;
22
23@Slf4j
24@EnableTransactionManagement
25@SpringBootApplication
26@EnableJpaRepositories
27public class SpringBucksApplication implements ApplicationRunner {
28    @Autowired
29    private CoffeeService coffeeService;
30    @Autowired
31    private JedisPool jedisPool;
32    @Autowired
33    private JedisPoolConfig jedisPoolConfig;
34
35    public static void main(String[] args) {
36        SpringApplication.run(SpringBucksApplication.class, args);
37    }
38
39    /**
40     * 配置 JedisPool
41     *
42     * @return
43     */
44    @Bean
45    @ConfigurationProperties("spring.redis")
46    public JedisPoolConfig jedisPoolConfig() {
47        return new JedisPoolConfig();
48    }
49
50    /**
51     * 构造并注入 JedisPool
52     *
53     * @param host
54     * @return
55     */
56    @Bean(destroyMethod = "close")
57    public JedisPool jedisPool(@Value("${spring.redis.host}") String host) {
58        return new JedisPool(jedisPoolConfig(), host);
59    }
60
61    @Override
62    public void run(ApplicationArguments args) throws Exception {
63        log.info(jedisPoolConfig.toString());
64
65        //jedis.hset: 从数据中查询出所有coffee,并且同步到redis中,保存到名为springbucks-menu hashset集合中
66        try (Jedis jedis = jedisPool.getResource()) {
67            coffeeService.findAllCoffee().forEach(c -> {
68                jedis.hset("springbucks-menu",
69                        c.getName(),
70                        Long.toString(c.getPrice().getAmountMinorLong()));
71            });
72
73			//hgetAll: 从redis中查询 key为 springbucks-menu 的value
74            Map<String, String> menu = jedis.hgetAll("springbucks-menu");
75            log.info("Menu: {}", menu);
76            // Menu: {mocha=3000, espresso=2000, capuccino=2500, latte=2500, macchiato=3000}
77
78            //查询hashset中key为springbucks-menu 的set 集合中field为espresso 的 value
79            String price = jedis.hget("springbucks-menu", "espresso");
80            log.info("espresso - {}", Money.ofMinor(
81                    CurrencyUnit.of("CNY"), Long.parseLong(price)
82                    )
83            );
84            //espresso - CNY 20.00
85        }
86    }
87}

使用 redis-client 连接 Redis 查看
testing-data

Redis 的哨兵模式

 Redis Sentinsel 是 Redis 的一种高可用解决方案。具备:监控、通知、自动故障转移服务发现。
 Jedis 中是通过 JedisSentinelPool 来处理 Redis 哨兵模式。

引入依赖

 1<parent>
 2<groupId>org.springframework.boot</groupId>
 3	<artifactId>spring-boot-starter-parent</artifactId>
 4	<version>2.1.1.RELEASE</version>
 5	<relativePath/> <!-- lookup parent from repository -->
 6</parent>
 7<groupId>com.boot.redis</groupId>
 8<artifactId>boot-redis</artifactId>
 9<version>0.0.1-SNAPSHOT</version>
10<name>boot-redis</name>
11<description>Demo project for Spring Boot</description>
12
13<properties>
14	<java.version>1.8</java.version>
15</properties>
16
17<dependencies>
18	<dependency>
19		<groupId>org.springframework.boot</groupId>
20		<artifactId>spring-boot-starter-data-redis</artifactId>
21		<exclusions>
22			<exclusion>
23				<groupId>redis.clients</groupId>
24				<artifactId>jedis</artifactId>
25			</exclusion>
26			<exclusion>
27				<groupId>io.lettuce</groupId>
28				<artifactId>lettuce-core</artifactId>
29			</exclusion>
30		</exclusions>
31	</dependency>
32	<dependency>
33		<groupId>org.springframework.boot</groupId>
34		<artifactId>spring-boot-starter-web</artifactId>
35	</dependency>
36	<dependency>
37		<groupId>redis.clients</groupId>
38		<artifactId>jedis</artifactId>
39	</dependency>
40
41	<dependency>
42		<groupId>org.springframework.boot</groupId>
43		<artifactId>spring-boot-starter-test</artifactId>
44		<scope>test</scope>
45	</dependency>
46	<dependency>
47		<groupId>org.apache.commons</groupId>
48		<artifactId>commons-pool2</artifactId>
49		<version>2.5.0</version>
50		<!--<version>2.4.2</version>-->
51	</dependency>
52</dependencies>

application.properties

 1spring:
 2  redis:
 3    host: 192.168.2.110 #哨兵模式下不用配置
 4    port: 6379 # 哨兵模式下不用配置
 5    password: admin
 6    jedis:
 7      pool:
 8        #最大连接数
 9        max-active: 1024
10        #最大阻塞等待时间(负数表示没限制)
11        max-wait: 20000
12        #最大空闲
13        max-idle: 200
14        #最小空闲
15        min-idle: 10
16    sentinel:
17      master: mymaster
18      nodes: 192.168.2.110:26379,192.168.2.110:26380,192.168.2.110:26381
19server:
20  port: 8088

Redis 的集群模式

  • 数据⾃动分⽚(分成 16384 个 Hash Slot )
  • 在部分节点失效时有⼀定可⽤性

JedisCluster
 Jedis 中,Redis Cluster 是通过 JedisCluster 来支持的。
 Jedis 只从 Master 读数据,如果想要⾃动读写分离,可以定制

引入依赖

1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <articleId>spring-boot-starter-data-redis</article>
4</dependency>

application.properties

 1spring:
 2  redis:
 3    jedis:
 4      pool:
 5        max-wait:5000
 6        max-Idle:50
 7        min-Idle:5
 8    cluster:
 9      nodes:127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
10    timeout:500

使用推荐的 RedisTemplate 使用 Redis-Cluster
RedisClusterConfiguration.java

 1@Configuration
 2public class RedisClusterConfiguration{
 3
 4       @Bean
 5       public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionfactory){
 6              RedisTemplate<String,String> redisTemplate=new RedisTemplate<>();
 7              redisTemplate.setConnectionFactory(redisConnectionFactory);
 8              redisTemplate.setKeySerializer(new StringRedisSerializer());
 9              redisTemplate.setValueSerializer(new StringRedisSerializer());
10              redisTemplate.afterPropertiesSet();
11              return redisTemplate;
12       }
13}

单元测试 RedisClusterTest.java

 1@RunWith(SpringRunner.class)
 2@SpringBootTest
 3public class RedisClusterTest{
 4        @Autowire
 5       private RedisTemplate<String,String> redisTemplate;
 6
 7       @Test
 8       public void getValue(){
 9            ValueOperations<String,String> operations=redisTemplate.opsForValue();
10            System.out.println(operations.get("key1"));
11      }
12}

Spring 的缓存抽象

 为不同的缓存提供一层抽象。

  • 为 Java 方法增加缓存,缓存执行结果
  • 支持 ConcurrentMap、EhCache、Caffffeine、JCache(JSR-107)
  • 接口:org.springframework.cache.Cacheorg.springframework.cache.CacheManager

不同缓存适用不同的场景

  • JVM 缓存:较长时间不会发生变化的、可以接收一定时间的延迟和消息不一致性的数据。
  • 分布式缓存:集群内部访问具备一定一致性的(值发生变化,集群所有节点都可以读取到最新的数据)
  • 不该使用缓存:读写比例 趋近于 1:1(这种数据没有必要缓存,最起码也要 写一次,读十次或者更多的数据才有缓存的意义。)

基于注解的缓存
 开启注解 @EnableCaching

  • @Cacheable:执行方法,如果该方法已经缓存,在走缓存,否则执行该方法,并且把执行结果放入缓存。
  • @CacheEvict:缓存清理。
  • @CachePut:总是设置缓存。
  • @Caching:打包多个缓存相关的操作。
  • @CacheConfig:对缓存做设置。

CoffeeService

 1package geektime.spring.springbucks.service;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.repository.CoffeeRepository;
 5import lombok.extern.slf4j.Slf4j;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.cache.annotation.CacheConfig;
 8import org.springframework.cache.annotation.CacheEvict;
 9import org.springframework.cache.annotation.Cacheable;
10import org.springframework.data.domain.Example;
11import org.springframework.data.domain.ExampleMatcher;
12import org.springframework.stereotype.Service;
13
14import java.util.List;
15import java.util.Optional;
16
17import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
18
19@Slf4j
20@Service
21//CoffeeService 使用缓存的名字 coffee
22@CacheConfig(cacheNames = "coffee")
23public class CoffeeService {
24    @Autowired
25    private CoffeeRepository coffeeRepository;
26
27    @Cacheable
28    public List<Coffee> findAllCoffee() {
29        return coffeeRepository.findAll();
30    }
31
32    @CacheEvict
33    public void reloadCoffee() {
34    }
35
36    public Optional<Coffee> findOneCoffee(String name) {
37        ExampleMatcher matcher = ExampleMatcher.matching()
38                .withMatcher("name", exact().ignoreCase());
39        Optional<Coffee> coffee = coffeeRepository.findOne(
40                Example.of(Coffee.builder().name(name).build(), matcher));
41        log.info("Coffee Found: {}", coffee);
42        return coffee;
43    }
44}

启动类

 1package geektime.spring.springbucks;
 2
 3import geektime.spring.springbucks.service.CoffeeService;
 4import lombok.extern.slf4j.Slf4j;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.boot.ApplicationArguments;
 7import org.springframework.boot.ApplicationRunner;
 8import org.springframework.boot.SpringApplication;
 9import org.springframework.boot.autoconfigure.SpringBootApplication;
10import org.springframework.cache.annotation.EnableCaching;
11import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
12import org.springframework.transaction.annotation.EnableTransactionManagement;
13
14@Slf4j
15@EnableTransactionManagement
16@SpringBootApplication
17@EnableJpaRepositories
18//标明拦截整个类的执行
19@EnableCaching(proxyTargetClass = true)
20public class SpringBucksApplication implements ApplicationRunner {
21	@Autowired
22	private CoffeeService coffeeService;
23
24	public static void main(String[] args) {
25		SpringApplication.run(SpringBucksApplication.class, args);
26	}
27
28	@Override
29	public void run(ApplicationArguments args) throws Exception {
30		//第一次调用 :会产生sql
31		log.info("Count: {}", coffeeService.findAllCoffee().size());
32		//Count: 5
33
34		//十次调用
35		for (int i = 0; i < 10; i++) {
36			log.info("Reading from cache.");
37			coffeeService.findAllCoffee();
38		}
39		//Reading from cache. x 10
40
41		//清理缓存
42		coffeeService.reloadCoffee();
43		log.info("Reading after refresh.");
44		//Reading after refresh.
45
46		//再次调用 :会产生sql
47		coffeeService.findAllCoffee().forEach(c -> log.info("Coffee {}", c.getName()));
48	}
49}

通过 Spring Boot 配置 Redis 缓存

引入依赖

1		<dependency>
2			<groupId>org.springframework.boot</groupId>
3			<artifactId>spring-boot-starter-cache</artifactId>
4		</dependency>
5		<dependency>
6			<groupId>org.springframework.boot</groupId>
7			<artifactId>spring-boot-starter-data-redis</artifactId>
8		</dependency>

application.properties

 1spring.jpa.hibernate.ddl-auto=none
 2spring.jpa.properties.hibernate.show_sql=true
 3spring.jpa.properties.hibernate.format_sql=true
 4
 5management.endpoints.web.exposure.include=*
 6
 7# 缓存类型设置为redis
 8spring.cache.type=redis
 9spring.cache.cache-names=coffee
10# 设置缓存生存时间
11spring.cache.redis.time-to-live=5000
12spring.cache.redis.cache-null-values=false
13
14spring.redis.host=192.168.31.201

CoffeeService

 1package geektime.spring.springbucks.service;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.repository.CoffeeRepository;
 5import lombok.extern.slf4j.Slf4j;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.cache.annotation.CacheConfig;
 8import org.springframework.cache.annotation.CacheEvict;
 9import org.springframework.cache.annotation.Cacheable;
10import org.springframework.data.domain.Example;
11import org.springframework.data.domain.ExampleMatcher;
12import org.springframework.stereotype.Service;
13
14import java.util.List;
15import java.util.Optional;
16
17import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
18
19@Slf4j
20@Service
21@CacheConfig(cacheNames = "coffee")
22public class CoffeeService {
23    @Autowired
24    private CoffeeRepository coffeeRepository;
25
26    @Cacheable
27    public List<Coffee> findAllCoffee() {
28        return coffeeRepository.findAll();
29    }
30
31    @CacheEvict
32    public void reloadCoffee() {
33    }
34
35    public Optional<Coffee> findOneCoffee(String name) {
36        ExampleMatcher matcher = ExampleMatcher.matching()
37                .withMatcher("name", exact().ignoreCase());
38        Optional<Coffee> coffee = coffeeRepository.findOne(
39                Example.of(Coffee.builder().name(name).build(), matcher));
40        log.info("Coffee Found: {}", coffee);
41        return coffee;
42    }
43}

SpringBucksApplication

 1package geektime.spring.springbucks;
 2
 3import geektime.spring.springbucks.service.CoffeeService;
 4import lombok.extern.slf4j.Slf4j;
 5import org.springframework.beans.factory.annotation.Autowired;
 6import org.springframework.boot.ApplicationArguments;
 7import org.springframework.boot.ApplicationRunner;
 8import org.springframework.boot.SpringApplication;
 9import org.springframework.boot.autoconfigure.SpringBootApplication;
10import org.springframework.cache.annotation.EnableCaching;
11import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
12import org.springframework.transaction.annotation.EnableTransactionManagement;
13
14@Slf4j
15@EnableTransactionManagement
16@SpringBootApplication
17@EnableJpaRepositories
18@EnableCaching(proxyTargetClass = true)
19public class SpringBucksApplication implements ApplicationRunner {
20	@Autowired
21	private CoffeeService coffeeService;
22
23	public static void main(String[] args) {
24		SpringApplication.run(SpringBucksApplication.class, args);
25	}
26
27	@Override
28	public void run(ApplicationArguments args) throws Exception {
29		//再次发起查询:产生sql
30		log.info("Count: {}", coffeeService.findAllCoffee().size());
31		//Count: 5
32		
33		//发起10次调用:走redis
34		for (int i = 0; i < 5; i++) {
35			log.info("Reading from cache.");
36			coffeeService.findAllCoffee();
37		}
38		//Reading from cache. x 5
39		
40		// 挂起5秒
41		Thread.sleep(5_000);
42
43		//清空缓存
44		log.info("Reading after refresh.");
45		//Reading after refresh.
46		
47		//再次发起查询:缓存过期,所以本次调用会生成sql
48		coffeeService.findAllCoffee().forEach(c -> log.info("Coffee {}", c.getName()));
49	}
50}

与 Redis 建立连接相关的配置

 Spring Data Redis 中已经采用 Lettuce 取代了 Jedis 作为默认的客户端。
连接工厂相关配置
 LettuceConnectionFactory 与 JedisConnectionFactory

  • RedisStandaloneConfiguration:单节点。
  • RedisSentinelConfiguration:哨兵。
  • RedisClusterConfiguration:集群。

Lettuce 内置支持读写分离

 只读主、只读从
 优先读主、优先读从

  • LettuceClientConfiguration
  • LettucePoolingClientConfiguration
  • LettuceClientConfigurationBuilderCustomizer:回调配置,配置 Lettuce 相关内容。

RedisTemplate 配置

 默认给出的是 Object 类型的 RedisTemplate,如果需要以 String 作为 key,自定义 POJO 类型作为 Value,可以自定义 RedisTemplate 。

StringRedisTemplate

 Spring 提供了一个 StringRedisTemplate,用于操作 key、value 都是 String 类型的操作。

示例(Lettuce、自定义 RedisTemplate)

依赖

1		<dependency>
2			<groupId>org.springframework.boot</groupId>
3			<artifactId>spring-boot-starter-data-redis</artifactId>
4		</dependency>
5
6		<dependency>
7			<groupId>org.apache.commons</groupId>
8			<artifactId>commons-pool2</artifactId>
9		</dependency>

application.properties

1spring.jpa.hibernate.ddl-auto=none
2spring.jpa.properties.hibernate.show_sql=true
3spring.jpa.properties.hibernate.format_sql=true
4
5management.endpoints.web.exposure.include=*
6
7spring.redis.host=192.168.31.201
8spring.redis.lettuce.pool.maxActive=5
9spring.redis.lettuce.pool.maxIdle=5

CoffeeService

 1package geektime.spring.springbucks.service;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.repository.CoffeeRepository;
 5import lombok.extern.slf4j.Slf4j;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.data.domain.Example;
 8import org.springframework.data.domain.ExampleMatcher;
 9import org.springframework.data.redis.core.HashOperations;
10import org.springframework.data.redis.core.RedisTemplate;
11import org.springframework.stereotype.Service;
12
13import java.util.List;
14import java.util.Optional;
15import java.util.concurrent.TimeUnit;
16
17import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
18
19@Slf4j
20@Service
21public class CoffeeService {
22    private static final String CACHE = "springbucks-coffee";
23    @Autowired
24    private CoffeeRepository coffeeRepository;
25    @Autowired
26    private RedisTemplate<String, Coffee> redisTemplate;
27
28    public List<Coffee> findAllCoffee() {
29        return coffeeRepository.findAll();
30    }
31
32    public Optional<Coffee> findOneCoffee(String name) {
33        //如果redis中有则返回
34        HashOperations<String, String, Coffee> hashOperations = redisTemplate.opsForHash();
35        if (redisTemplate.hasKey(CACHE) && hashOperations.hasKey(CACHE, name)) {
36            log.info("Get coffee {} from Redis.", name);
37            return Optional.of(hashOperations.get(CACHE, name));
38        }
39
40        //如果redis中没有,则使用coffeeRepository查询出Coffee,然后存入redis
41        ExampleMatcher matcher = ExampleMatcher.matching()
42                .withMatcher("name", exact().ignoreCase());
43        Optional<Coffee> coffee = coffeeRepository.findOne(
44                Example.of(Coffee.builder().name(name).build(), matcher));
45        log.info("Coffee Found from Database: {}", coffee);
46        if (coffee.isPresent()) {
47            log.info("Put coffee {} to Redis.", name);
48            hashOperations.put(CACHE, name, coffee.get());
49            redisTemplate.expire(CACHE, 1, TimeUnit.MINUTES);
50        }
51        return coffee;
52    }
53}

SpringBucksApplication

 1package geektime.spring.springbucks;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.service.CoffeeService;
 5import io.lettuce.core.ReadFrom;
 6import lombok.extern.slf4j.Slf4j;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.boot.ApplicationArguments;
 9import org.springframework.boot.ApplicationRunner;
10import org.springframework.boot.SpringApplication;
11import org.springframework.boot.autoconfigure.SpringBootApplication;
12import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
13import org.springframework.context.annotation.Bean;
14import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
15import org.springframework.data.redis.connection.RedisConnectionFactory;
16import org.springframework.data.redis.core.RedisTemplate;
17import org.springframework.transaction.annotation.EnableTransactionManagement;
18
19import java.net.UnknownHostException;
20import java.util.Optional;
21
22@Slf4j
23@EnableTransactionManagement
24@SpringBootApplication
25@EnableJpaRepositories
26public class SpringBucksApplication implements ApplicationRunner {
27	@Autowired
28	private CoffeeService coffeeService;
29
30	public static void main(String[] args) {
31		SpringApplication.run(SpringBucksApplication.class, args);
32	}
33
34	/**
35	 * 自定义 RedisTemplate: 因为RedisTemplate提供的是一个Objects类型的RedisTemplate,需要自定义自己需要的RedisTemplate
36	 * @param redisConnectionFactory
37	 * @return
38	 */
39	@Bean
40	public RedisTemplate<String, Coffee> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
41		RedisTemplate<String, Coffee> template = new RedisTemplate<>();
42		template.setConnectionFactory(redisConnectionFactory);
43		return template;
44	}
45
46	/**
47	 * 这里配置 Lettuce 优先读主节点
48	 * 这里和示例无关,在 Redis Cluster 中生效
49	 * @return
50	 */
51	@Bean
52	public LettuceClientConfigurationBuilderCustomizer customizer() {
53		return builder -> builder.readFrom(ReadFrom.MASTER_PREFERRED);
54	}
55
56	@Override
57	public void run(ApplicationArguments args) throws Exception {
58		//findOneCoffee : mocha
59		Optional<Coffee> c = coffeeService.findOneCoffee("mocha");
60		log.info("Coffee {}", c);
61
62		// 5次 findOneCoffee : 发现没有访问数据库
63		for (int i = 0; i < 5; i++) {
64			c = coffeeService.findOneCoffee("mocha");
65		}
66
67		log.info("Value from Redis: {}", c);
68	}
69}

控制台输出

 1//从DB中加载
 2Coffee Found from Database: Optional[Coffee(super=BaseEntity(id=4, createTime=2020-01-14 18:05:10.543, updateTime=2020-01-14 18:05:10.543), name=mocha, price=CNY 30.00)]
 3
 4//loading到缓存
 5Put coffee mocha to Redis.
 6
 7//打印查询到的对象
 8Coffee Optional[Coffee(super=BaseEntity(id=4, createTime=2020-01-14 18:05:10.543, updateTime=2020-01-14 18:05:10.543), name=mocha, price=CNY 30.00)]
 9
10//再次发起多次调用:缓存命中
11Get coffee mocha from Redis.
12Get coffee mocha from Redis.
13Get coffee mocha from Redis.
14Get coffee mocha from Redis.
15Get coffee mocha from Redis.
16
17//命中缓存: 55秒之后过期
18Value from Redis: Optional[Coffee(super=BaseEntity(id=4, createTime=2020-01-14 18:05:10.543, updateTime=2020-01-14 18:05:10.543), name=mocha, price=CNY 30.00)]

Redis Repository

实体注解

  • @RedisHash:类似 @Entity
  • @Id
  • @Indexed

处理不同类型数据源的 Repository

  • 根据实体的注解:@Entity(JPA)、@RedisHash(Redis)、@Document(MongoDB)
  • 根据继承的接⼝类型
  • 扫描不同的包

引入依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.2.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring</groupId>
12	<artifactId>springbucks</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>springbucks</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-data-jpa</artifactId>
25		</dependency>
26
27		<dependency>
28			<groupId>org.springframework.boot</groupId>
29			<artifactId>spring-boot-starter-data-redis</artifactId>
30		</dependency>
31
32		<dependency>
33			<groupId>org.apache.commons</groupId>
34			<artifactId>commons-pool2</artifactId>
35		</dependency>
36
37		<dependency>
38			<groupId>org.joda</groupId>
39			<artifactId>joda-money</artifactId>
40			<version>1.0.1</version>
41		</dependency>
42		<dependency>
43			<groupId>org.jadira.usertype</groupId>
44			<artifactId>usertype.core</artifactId>
45			<version>6.0.1.GA</version>
46		</dependency>
47
48		<dependency>
49			<groupId>com.h2database</groupId>
50			<artifactId>h2</artifactId>
51			<scope>runtime</scope>
52		</dependency>
53		<dependency>
54			<groupId>org.projectlombok</groupId>
55			<artifactId>lombok</artifactId>
56			<optional>true</optional>
57		</dependency>
58		<dependency>
59			<groupId>org.springframework.boot</groupId>
60			<artifactId>spring-boot-starter-test</artifactId>
61			<scope>test</scope>
62		</dependency>
63	</dependencies>
64
65	<build>
66		<plugins>
67			<plugin>
68				<groupId>org.springframework.boot</groupId>
69				<artifactId>spring-boot-maven-plugin</artifactId>
70			</plugin>
71		</plugins>
72	</build>
73</project>

application.properties

1spring.jpa.hibernate.ddl-auto=none
2spring.jpa.properties.hibernate.show_sql=true
3spring.jpa.properties.hibernate.format_sql=true
4
5management.endpoints.web.exposure.include=*
6
7spring.redis.host=192.168.31.201
8spring.redis.lettuce.pool.maxActive=5
9spring.redis.lettuce.pool.maxIdle=5

CoffeeCache

 1package geektime.spring.springbucks.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import org.joda.money.Money;
 8import org.springframework.data.annotation.Id;
 9import org.springframework.data.redis.core.RedisHash;
10import org.springframework.data.redis.core.index.Indexed;
11
12@RedisHash(value = "springbucks-coffee", timeToLive = 60)
13@Data
14@NoArgsConstructor
15@AllArgsConstructor
16@Builder
17public class CoffeeCache {
18    @Id //作为id
19    private Long id;
20    @Indexed //作为索引
21    private String name;
22    private Money price;
23}

CoffeeCacheRepository

 1package geektime.spring.springbucks.repository;
 2
 3import geektime.spring.springbucks.model.CoffeeCache;
 4import org.springframework.data.repository.CrudRepository;
 5
 6import java.util.Optional;
 7
 8public interface CoffeeCacheRepository extends CrudRepository<CoffeeCache, Long> {
 9    Optional<CoffeeCache> findOneByName(String name);
10}

BytesToMoneyConverter

 1package geektime.spring.springbucks.converter;
 2
 3import org.joda.money.CurrencyUnit;
 4import org.joda.money.Money;
 5import org.springframework.core.convert.converter.Converter;
 6import org.springframework.data.convert.ReadingConverter;
 7
 8import java.nio.charset.StandardCharsets;
 9
10@ReadingConverter
11public class BytesToMoneyConverter implements Converter<byte[], Money> {
12    @Override
13    public Money convert(byte[] source) {
14        String value = new String(source, StandardCharsets.UTF_8);
15        return Money.ofMinor(CurrencyUnit.of("CNY"), Long.parseLong(value));
16    }
17}

MoneyToBytesConverter

 1package geektime.spring.springbucks.converter;
 2
 3import org.joda.money.Money;
 4import org.springframework.core.convert.converter.Converter;
 5import org.springframework.data.convert.WritingConverter;
 6
 7import java.nio.charset.StandardCharsets;
 8
 9@WritingConverter
10public class MoneyToBytesConverter implements Converter<Money, byte[]> {
11    @Override
12    public byte[] convert(Money source) {
13        String value = Long.toString(source.getAmountMinorLong());
14        return value.getBytes(StandardCharsets.UTF_8);
15    }
16}

CoffeeService

 1package geektime.spring.springbucks.service;
 2
 3import geektime.spring.springbucks.model.Coffee;
 4import geektime.spring.springbucks.model.CoffeeCache;
 5import geektime.spring.springbucks.repository.CoffeeCacheRepository;
 6import geektime.spring.springbucks.repository.CoffeeRepository;
 7import lombok.extern.slf4j.Slf4j;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.data.domain.Example;
10import org.springframework.data.domain.ExampleMatcher;
11import org.springframework.stereotype.Service;
12
13import java.util.List;
14import java.util.Optional;
15
16import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
17
18@Slf4j
19@Service
20public class CoffeeService {
21    @Autowired
22    private CoffeeRepository coffeeRepository;
23    @Autowired
24    private CoffeeCacheRepository cacheRepository;
25
26    public List<Coffee> findAllCoffee() {
27        return coffeeRepository.findAll();
28    }
29
30    public Optional<Coffee> findSimpleCoffeeFromCache(String name) {
31        //cacheRepository :findOneByName 如果有就将查询结果转换为Coffee返回
32        Optional<CoffeeCache> cached = cacheRepository.findOneByName(name);
33        if (cached.isPresent()) {
34            CoffeeCache coffeeCache = cached.get();
35            Coffee coffee = Coffee.builder()
36                    .name(coffeeCache.getName())
37                    .price(coffeeCache.getPrice())
38                    .build();
39            log.info("Coffee {} found in cache.", coffeeCache);
40            return Optional.of(coffee);
41        } else { //如果缓存未命中,则使用coffeeRepository查询出Coffee对象,然后提取属性用于生成CoffeeCache,最后使用cacheRepository保存至Redis中。
42            Optional<Coffee> raw = findOneCoffee(name);
43            raw.ifPresent(c -> {
44                CoffeeCache coffeeCache = CoffeeCache.builder()
45                        .id(c.getId())
46                        .name(c.getName())
47                        .price(c.getPrice())
48                        .build();
49                log.info("Save Coffee {} to cache.", coffeeCache);
50                cacheRepository.save(coffeeCache);
51            });
52            return raw;
53        }
54    }
55
56    /**
57     * coffeeRepository
58     * @param name
59     * @return
60     */
61    public Optional<Coffee> findOneCoffee(String name) {
62        ExampleMatcher matcher = ExampleMatcher.matching()
63                .withMatcher("name", exact().ignoreCase());
64        Optional<Coffee> coffee = coffeeRepository.findOne(
65                Example.of(Coffee.builder().name(name).build(), matcher));
66        log.info("Coffee Found:  from DB {}", coffee);
67        return coffee;
68    }
69}

启动类

 1package geektime.spring.springbucks;
 2
 3import geektime.spring.springbucks.converter.BytesToMoneyConverter;
 4import geektime.spring.springbucks.converter.MoneyToBytesConverter;
 5import geektime.spring.springbucks.model.Coffee;
 6import geektime.spring.springbucks.service.CoffeeService;
 7import io.lettuce.core.ReadFrom;
 8import lombok.extern.slf4j.Slf4j;
 9import org.springframework.beans.factory.annotation.Autowired;
10import org.springframework.boot.ApplicationArguments;
11import org.springframework.boot.ApplicationRunner;
12import org.springframework.boot.SpringApplication;
13import org.springframework.boot.autoconfigure.SpringBootApplication;
14import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
15import org.springframework.context.annotation.Bean;
16import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
17import org.springframework.data.redis.core.convert.RedisCustomConversions;
18import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
19import org.springframework.transaction.annotation.EnableTransactionManagement;
20
21import java.util.Arrays;
22import java.util.Optional;
23
24@Slf4j
25@EnableTransactionManagement
26@SpringBootApplication
27@EnableJpaRepositories
28@EnableRedisRepositories //开启RedisRepository支持
29public class SpringBucksApplication implements ApplicationRunner {
30	@Autowired
31	private CoffeeService coffeeService;
32
33	public static void main(String[] args) {
34		SpringApplication.run(SpringBucksApplication.class, args);
35	}
36
37	/**
38	 * Lettuce优先从主节点读取 : 与本示例无关
39	 * @return
40	 */
41	@Bean
42	public LettuceClientConfigurationBuilderCustomizer customizer() {
43		return builder -> builder.readFrom(ReadFrom.MASTER_PREFERRED);
44	}
45
46	/**
47	 * 注入类型转换:@ReadingConverter、@WritingConverter
48	 * @return
49	 */
50	@Bean
51	public RedisCustomConversions redisCustomConversions() {
52		return new RedisCustomConversions(
53				Arrays.asList(new MoneyToBytesConverter(), new BytesToMoneyConverter()));
54	}
55
56	@Override
57	public void run(ApplicationArguments args) throws Exception {
58		//查询 mocha
59		Optional<Coffee> c = coffeeService.findSimpleCoffeeFromCache("mocha");
60		// Coffee Found from DB: Optional[Coffee(super=BaseEntity(id=4, createTime=2020-01-14 19:06:54.772, updateTime=2020-01-14 19:06:54.772), name=mocha, price=CNY 30.00)]
61		//Save Coffee CoffeeCache(id=4, name=mocha, price=CNY 30.00) to cache.
62
63		log.info("Coffee {}", c);
64		//Coffee Optional[Coffee(super=BaseEntity(id=4, createTime=2020-01-14 19:06:54.772, updateTime=2020-01-14 19:06:54.772), name=mocha, price=CNY 30.00)]
65
66		for (int i = 0; i < 5; i++) {
67			c = coffeeService.findSimpleCoffeeFromCache("mocha");
68		}
69		//Coffee CoffeeCache(id=4, name=mocha, price=CNY 30.00) found in cache.  x 5
70
71		log.info("Value from Redis: {}", c);
72		//Value from Redis: Optional[Coffee(super=BaseEntity(id=null, createTime=null, updateTime=null), name=mocha, price=CNY 30.00)]
73	}
74}

作者:Soulboy