Spring for Redis
Spring 对 Redis 的支持
Redis 是 ⼀款开源的内存 KV 存储,⽀持多种数据结构。
Spring 对 Redis 的支持是通过 Spring Data Redis 项目。
- Jedis / Lettuce
- RedisTemplate
- Repository
Reference
Docker 启动 Redis 容器
# 拉取image
docker pull redis
# 启动 Redis
docker run -p 6379:6379 --name myredis -d redis
# 设置密码添加 --requirepass 参数
docker run -p 6379:6379 --name myredis -d redis --requirepass "123456"
Jedis 客户端
- Jedis 不是线程安全的:无法在多个线程之间共享同一个 Jedis 实例。
- 通过 JedisPool 获得 Jedis 实例
- 直接使用 Jedis 中的方法
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring</groupId>
<artifactId>springbucks</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbucks</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>6.0.1.GA</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.redis.host=192.168.31.201
spring.redis.port=6379
spring.redis.jedis.pool.max-active=3
spring.redis.jedis.pool.max-idle=3
schema.sql
drop table t_coffee if exists;
drop table t_order if exists;
drop table t_order_coffee if exists;
create table t_coffee (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
name varchar(255),
price bigint,
primary key (id)
);
create table t_order (
id bigint auto_increment,
create_time timestamp,
update_time timestamp,
customer varchar(255),
state integer not null,
primary key (id)
);
create table t_order_coffee (
coffee_order_id bigint not null,
items_id bigint not null
);
insert into t_coffee (name, price, create_time, update_time) values ('espresso', 2000, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('latte', 2500, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('capuccino', 2500, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('mocha', 3000, now(), now());
insert into t_coffee (name, price, create_time, update_time) values ('macchiato', 3000, now(), now());
BaseEntity
package geektime.spring.springbucks.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.util.Date;
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(updatable = false)
@CreationTimestamp
private Date createTime;
@UpdateTimestamp
private Date updateTime;
}
Coffee
package geektime.spring.springbucks.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
@Entity
@Table(name = "T_COFFEE")
@Builder
@Data
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Coffee extends BaseEntity implements Serializable {
private String name;
@Type(type = "org.jadira.usertype.moneyandcurrency.joda.PersistentMoneyMinorAmount",
parameters = {@org.hibernate.annotations.Parameter(name = "currencyCode", value = "CNY")})
private Money price;
}
CoffeeOrder
package geektime.spring.springbucks.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.List;
@Entity
@Table(name = "T_ORDER")
@Data
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CoffeeOrder extends BaseEntity implements Serializable {
private String customer;
@ManyToMany
@JoinTable(name = "T_ORDER_COFFEE")
@OrderBy("id")
private List<Coffee> items;
@Enumerated
@Column(nullable = false)
private OrderState state;
}
OrderState
package geektime.spring.springbucks.model;
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}
CoffeeOrderRepository
package geektime.spring.springbucks.repository;
import geektime.spring.springbucks.model.CoffeeOrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}
CoffeeRepository
package geektime.spring.springbucks.repository;
import geektime.spring.springbucks.model.CoffeeOrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CoffeeOrderRepository extends JpaRepository<CoffeeOrder, Long> {
}
CoffeeOrderService
package geektime.spring.springbucks.service;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.model.CoffeeOrder;
import geektime.spring.springbucks.model.OrderState;
import geektime.spring.springbucks.repository.CoffeeOrderRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
@Slf4j
@Service
@Transactional
public class CoffeeOrderService {
@Autowired
private CoffeeOrderRepository orderRepository;
public CoffeeOrder createOrder(String customer, Coffee...coffee) {
CoffeeOrder order = CoffeeOrder.builder()
.customer(customer)
.items(new ArrayList<>(Arrays.asList(coffee)))
.state(OrderState.INIT)
.build();
CoffeeOrder saved = orderRepository.save(order);
log.info("New Order: {}", saved);
return saved;
}
public boolean updateState(CoffeeOrder order, OrderState state) {
if (state.compareTo(order.getState()) <= 0) {
log.warn("Wrong State order: {}, {}", state, order.getState());
return false;
}
order.setState(state);
orderRepository.save(order);
log.info("Updated Order: {}", order);
return true;
}
}
CoffeeService
package geektime.spring.springbucks.service;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
@Slf4j
@Service
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
public List<Coffee> findAllCoffee() {
return coffeeRepository.findAll();
}
public Optional<Coffee> findOneCoffee(String name) {
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", exact().ignoreCase());
Optional<Coffee> coffee = coffeeRepository.findOne(
Example.of(Coffee.builder().name(name).build(), matcher));
log.info("Coffee Found: {}", coffee);
return coffee;
}
}
SpringBucksApplication 启动类
package geektime.spring.springbucks;
import geektime.spring.springbucks.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Map;
@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
public class SpringBucksApplication implements ApplicationRunner {
@Autowired
private CoffeeService coffeeService;
@Autowired
private JedisPool jedisPool;
@Autowired
private JedisPoolConfig jedisPoolConfig;
public static void main(String[] args) {
SpringApplication.run(SpringBucksApplication.class, args);
}
/**
* 配置 JedisPool
*
* @return
*/
@Bean
@ConfigurationProperties("spring.redis")
public JedisPoolConfig jedisPoolConfig() {
return new JedisPoolConfig();
}
/**
* 构造并注入 JedisPool
*
* @param host
* @return
*/
@Bean(destroyMethod = "close")
public JedisPool jedisPool(@Value("${spring.redis.host}") String host) {
return new JedisPool(jedisPoolConfig(), host);
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.info(jedisPoolConfig.toString());
//jedis.hset: 从数据中查询出所有coffee,并且同步到redis中,保存到名为springbucks-menu hashset集合中
try (Jedis jedis = jedisPool.getResource()) {
coffeeService.findAllCoffee().forEach(c -> {
jedis.hset("springbucks-menu",
c.getName(),
Long.toString(c.getPrice().getAmountMinorLong()));
});
//hgetAll: 从redis中查询 key为 springbucks-menu 的value
Map<String, String> menu = jedis.hgetAll("springbucks-menu");
log.info("Menu: {}", menu);
// Menu: {mocha=3000, espresso=2000, capuccino=2500, latte=2500, macchiato=3000}
//查询hashset中key为springbucks-menu 的set 集合中field为espresso 的 value
String price = jedis.hget("springbucks-menu", "espresso");
log.info("espresso - {}", Money.ofMinor(
CurrencyUnit.of("CNY"), Long.parseLong(price)
)
);
//espresso - CNY 20.00
}
}
}
使用 redis-client 连接 Redis 查看
Redis 的哨兵模式
Redis Sentinsel 是 Redis 的一种高可用解决方案。具备:监控、通知、自动故障转移服务发现。
Jedis中是通过 JedisSentinelPool 来处理 Redis 哨兵模式。
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.boot.redis</groupId>
<artifactId>boot-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-redis</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.5.0</version>
<!--<version>2.4.2</version>-->
</dependency>
</dependencies>
application.properties
spring:
redis:
host: 192.168.2.110 #哨兵模式下不用配置
port: 6379 # 哨兵模式下不用配置
password: admin
jedis:
pool:
#最大连接数
max-active: 1024
#最大阻塞等待时间(负数表示没限制)
max-wait: 20000
#最大空闲
max-idle: 200
#最小空闲
min-idle: 10
sentinel:
master: mymaster
nodes: 192.168.2.110:26379,192.168.2.110:26380,192.168.2.110:26381
server:
port: 8088
Redis 的集群模式
- 数据⾃动分⽚(分成16384个 Hash Slot )
- 在部分节点失效时有⼀定可⽤性
JedisCluster
Jedis 中,Redis Cluster 是通过 JedisCluster 来支持的。
Jedis 只从 Master 读数据,如果想要⾃动读写分离,可以定制
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<articleId>spring-boot-starter-data-redis</article>
</dependency>
application.properties
spring:
redis:
jedis:
pool:
max-wait:5000
max-Idle:50
min-Idle:5
cluster:
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
timeout:500
使用推荐的RedisTemplate使用Redis-Cluster
RedisClusterConfiguration.java
@Configuration
public class RedisClusterConfiguration{
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionfactory){
RedisTemplate<String,String> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
单元测试 RedisClusterTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisClusterTest{
@Autowire
private RedisTemplate<String,String> redisTemplate;
@Test
public void getValue(){
ValueOperations<String,String> operations=redisTemplate.opsForValue();
System.out.println(operations.get("key1"));
}
}
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
package geektime.spring.springbucks.service;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
@Slf4j
@Service
//CoffeeService 使用缓存的名字 coffee
@CacheConfig(cacheNames = "coffee")
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
@Cacheable
public List<Coffee> findAllCoffee() {
return coffeeRepository.findAll();
}
@CacheEvict
public void reloadCoffee() {
}
public Optional<Coffee> findOneCoffee(String name) {
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", exact().ignoreCase());
Optional<Coffee> coffee = coffeeRepository.findOne(
Example.of(Coffee.builder().name(name).build(), matcher));
log.info("Coffee Found: {}", coffee);
return coffee;
}
}
启动类
package geektime.spring.springbucks;
import geektime.spring.springbucks.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
//标明拦截整个类的执行
@EnableCaching(proxyTargetClass = true)
public class SpringBucksApplication implements ApplicationRunner {
@Autowired
private CoffeeService coffeeService;
public static void main(String[] args) {
SpringApplication.run(SpringBucksApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
//第一次调用 :会产生sql
log.info("Count: {}", coffeeService.findAllCoffee().size());
//Count: 5
//十次调用
for (int i = 0; i < 10; i++) {
log.info("Reading from cache.");
coffeeService.findAllCoffee();
}
//Reading from cache. x 10
//清理缓存
coffeeService.reloadCoffee();
log.info("Reading after refresh.");
//Reading after refresh.
//再次调用 :会产生sql
coffeeService.findAllCoffee().forEach(c -> log.info("Coffee {}", c.getName()));
}
}
通过Spring Boot 配置 Redis 缓存
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
management.endpoints.web.exposure.include=*
# 缓存类型设置为redis
spring.cache.type=redis
spring.cache.cache-names=coffee
# 设置缓存生存时间
spring.cache.redis.time-to-live=5000
spring.cache.redis.cache-null-values=false
spring.redis.host=192.168.31.201
CoffeeService
package geektime.spring.springbucks.service;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
@Slf4j
@Service
@CacheConfig(cacheNames = "coffee")
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
@Cacheable
public List<Coffee> findAllCoffee() {
return coffeeRepository.findAll();
}
@CacheEvict
public void reloadCoffee() {
}
public Optional<Coffee> findOneCoffee(String name) {
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", exact().ignoreCase());
Optional<Coffee> coffee = coffeeRepository.findOne(
Example.of(Coffee.builder().name(name).build(), matcher));
log.info("Coffee Found: {}", coffee);
return coffee;
}
}
SpringBucksApplication
package geektime.spring.springbucks;
import geektime.spring.springbucks.service.CoffeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
@EnableCaching(proxyTargetClass = true)
public class SpringBucksApplication implements ApplicationRunner {
@Autowired
private CoffeeService coffeeService;
public static void main(String[] args) {
SpringApplication.run(SpringBucksApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
//再次发起查询:产生sql
log.info("Count: {}", coffeeService.findAllCoffee().size());
//Count: 5
//发起10次调用:走redis
for (int i = 0; i < 5; i++) {
log.info("Reading from cache.");
coffeeService.findAllCoffee();
}
//Reading from cache. x 5
// 挂起5秒
Thread.sleep(5_000);
//清空缓存
log.info("Reading after refresh.");
//Reading after refresh.
//再次发起查询:缓存过期,所以本次调用会生成sql
coffeeService.findAllCoffee().forEach(c -> log.info("Coffee {}", c.getName()));
}
}
与 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)
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
management.endpoints.web.exposure.include=*
spring.redis.host=192.168.31.201
spring.redis.lettuce.pool.maxActive=5
spring.redis.lettuce.pool.maxIdle=5
CoffeeService
package geektime.spring.springbucks.service;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
@Slf4j
@Service
public class CoffeeService {
private static final String CACHE = "springbucks-coffee";
@Autowired
private CoffeeRepository coffeeRepository;
@Autowired
private RedisTemplate<String, Coffee> redisTemplate;
public List<Coffee> findAllCoffee() {
return coffeeRepository.findAll();
}
public Optional<Coffee> findOneCoffee(String name) {
//如果redis中有则返回
HashOperations<String, String, Coffee> hashOperations = redisTemplate.opsForHash();
if (redisTemplate.hasKey(CACHE) && hashOperations.hasKey(CACHE, name)) {
log.info("Get coffee {} from Redis.", name);
return Optional.of(hashOperations.get(CACHE, name));
}
//如果redis中没有,则使用coffeeRepository查询出Coffee,然后存入redis
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", exact().ignoreCase());
Optional<Coffee> coffee = coffeeRepository.findOne(
Example.of(Coffee.builder().name(name).build(), matcher));
log.info("Coffee Found from Database: {}", coffee);
if (coffee.isPresent()) {
log.info("Put coffee {} to Redis.", name);
hashOperations.put(CACHE, name, coffee.get());
redisTemplate.expire(CACHE, 1, TimeUnit.MINUTES);
}
return coffee;
}
}
SpringBucksApplication
package geektime.spring.springbucks;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.service.CoffeeService;
import io.lettuce.core.ReadFrom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.net.UnknownHostException;
import java.util.Optional;
@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
public class SpringBucksApplication implements ApplicationRunner {
@Autowired
private CoffeeService coffeeService;
public static void main(String[] args) {
SpringApplication.run(SpringBucksApplication.class, args);
}
/**
* 自定义 RedisTemplate: 因为RedisTemplate提供的是一个Objects类型的RedisTemplate,需要自定义自己需要的RedisTemplate
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Coffee> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Coffee> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 这里配置 Lettuce 优先读主节点
* 这里和示例无关,在 Redis Cluster 中生效
* @return
*/
@Bean
public LettuceClientConfigurationBuilderCustomizer customizer() {
return builder -> builder.readFrom(ReadFrom.MASTER_PREFERRED);
}
@Override
public void run(ApplicationArguments args) throws Exception {
//findOneCoffee : mocha
Optional<Coffee> c = coffeeService.findOneCoffee("mocha");
log.info("Coffee {}", c);
// 5次 findOneCoffee : 发现没有访问数据库
for (int i = 0; i < 5; i++) {
c = coffeeService.findOneCoffee("mocha");
}
log.info("Value from Redis: {}", c);
}
}
控制台输出
//从DB中加载
Coffee 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)]
//loading到缓存
Put coffee mocha to Redis.
//打印查询到的对象
Coffee 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)]
//再次发起多次调用:缓存命中
Get coffee mocha from Redis.
Get coffee mocha from Redis.
Get coffee mocha from Redis.
Get coffee mocha from Redis.
Get coffee mocha from Redis.
//命中缓存: 55秒之后过期
Value 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)
- 根据继承的接⼝类型
- 扫描不同的包
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring</groupId>
<artifactId>springbucks</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbucks</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>6.0.1.GA</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
management.endpoints.web.exposure.include=*
spring.redis.host=192.168.31.201
spring.redis.lettuce.pool.maxActive=5
spring.redis.lettuce.pool.maxIdle=5
CoffeeCache
package geektime.spring.springbucks.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.money.Money;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
@RedisHash(value = "springbucks-coffee", timeToLive = 60)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CoffeeCache {
@Id //作为id
private Long id;
@Indexed //作为索引
private String name;
private Money price;
}
CoffeeCacheRepository
package geektime.spring.springbucks.repository;
import geektime.spring.springbucks.model.CoffeeCache;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface CoffeeCacheRepository extends CrudRepository<CoffeeCache, Long> {
Optional<CoffeeCache> findOneByName(String name);
}
BytesToMoneyConverter
package geektime.spring.springbucks.converter;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import java.nio.charset.StandardCharsets;
@ReadingConverter
public class BytesToMoneyConverter implements Converter<byte[], Money> {
@Override
public Money convert(byte[] source) {
String value = new String(source, StandardCharsets.UTF_8);
return Money.ofMinor(CurrencyUnit.of("CNY"), Long.parseLong(value));
}
}
MoneyToBytesConverter
package geektime.spring.springbucks.converter;
import org.joda.money.Money;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
import java.nio.charset.StandardCharsets;
@WritingConverter
public class MoneyToBytesConverter implements Converter<Money, byte[]> {
@Override
public byte[] convert(Money source) {
String value = Long.toString(source.getAmountMinorLong());
return value.getBytes(StandardCharsets.UTF_8);
}
}
CoffeeService
package geektime.spring.springbucks.service;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.model.CoffeeCache;
import geektime.spring.springbucks.repository.CoffeeCacheRepository;
import geektime.spring.springbucks.repository.CoffeeRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.exact;
@Slf4j
@Service
public class CoffeeService {
@Autowired
private CoffeeRepository coffeeRepository;
@Autowired
private CoffeeCacheRepository cacheRepository;
public List<Coffee> findAllCoffee() {
return coffeeRepository.findAll();
}
public Optional<Coffee> findSimpleCoffeeFromCache(String name) {
//cacheRepository :findOneByName 如果有就将查询结果转换为Coffee返回
Optional<CoffeeCache> cached = cacheRepository.findOneByName(name);
if (cached.isPresent()) {
CoffeeCache coffeeCache = cached.get();
Coffee coffee = Coffee.builder()
.name(coffeeCache.getName())
.price(coffeeCache.getPrice())
.build();
log.info("Coffee {} found in cache.", coffeeCache);
return Optional.of(coffee);
} else { //如果缓存未命中,则使用coffeeRepository查询出Coffee对象,然后提取属性用于生成CoffeeCache,最后使用cacheRepository保存至Redis中。
Optional<Coffee> raw = findOneCoffee(name);
raw.ifPresent(c -> {
CoffeeCache coffeeCache = CoffeeCache.builder()
.id(c.getId())
.name(c.getName())
.price(c.getPrice())
.build();
log.info("Save Coffee {} to cache.", coffeeCache);
cacheRepository.save(coffeeCache);
});
return raw;
}
}
/**
* coffeeRepository
* @param name
* @return
*/
public Optional<Coffee> findOneCoffee(String name) {
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", exact().ignoreCase());
Optional<Coffee> coffee = coffeeRepository.findOne(
Example.of(Coffee.builder().name(name).build(), matcher));
log.info("Coffee Found: from DB {}", coffee);
return coffee;
}
}
启动类
package geektime.spring.springbucks;
import geektime.spring.springbucks.converter.BytesToMoneyConverter;
import geektime.spring.springbucks.converter.MoneyToBytesConverter;
import geektime.spring.springbucks.model.Coffee;
import geektime.spring.springbucks.service.CoffeeService;
import io.lettuce.core.ReadFrom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.util.Arrays;
import java.util.Optional;
@Slf4j
@EnableTransactionManagement
@SpringBootApplication
@EnableJpaRepositories
@EnableRedisRepositories //开启RedisRepository支持
public class SpringBucksApplication implements ApplicationRunner {
@Autowired
private CoffeeService coffeeService;
public static void main(String[] args) {
SpringApplication.run(SpringBucksApplication.class, args);
}
/**
* Lettuce优先从主节点读取 : 与本示例无关
* @return
*/
@Bean
public LettuceClientConfigurationBuilderCustomizer customizer() {
return builder -> builder.readFrom(ReadFrom.MASTER_PREFERRED);
}
/**
* 注入类型转换:@ReadingConverter、@WritingConverter
* @return
*/
@Bean
public RedisCustomConversions redisCustomConversions() {
return new RedisCustomConversions(
Arrays.asList(new MoneyToBytesConverter(), new BytesToMoneyConverter()));
}
@Override
public void run(ApplicationArguments args) throws Exception {
//查询 mocha
Optional<Coffee> c = coffeeService.findSimpleCoffeeFromCache("mocha");
// 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)]
//Save Coffee CoffeeCache(id=4, name=mocha, price=CNY 30.00) to cache.
log.info("Coffee {}", c);
//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)]
for (int i = 0; i < 5; i++) {
c = coffeeService.findSimpleCoffeeFromCache("mocha");
}
//Coffee CoffeeCache(id=4, name=mocha, price=CNY 30.00) found in cache. x 5
log.info("Value from Redis: {}", c);
//Value from Redis: Optional[Coffee(super=BaseEntity(id=null, createTime=null, updateTime=null), name=mocha, price=CNY 30.00)]
}
}