Spring for Redis
Spring 对 Redis 的支持
Redis 是 ⼀款开源的内存 KV 存储,⽀持多种数据结构。
Spring 对 Redis 的支持是通过 Spring Data Redis 项目。
- Jedis / Lettuce
- RedisTemplate
- Repository
Reference
Docker 启动 Redis 容器
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 查看
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.Cache
、org.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}