Spring Boot (五): Redis快取使用姿勢盤點
1. Redis 簡介
Redis 是目前業界使用最廣泛的記憶體資料儲存。相比 Memcached,Redis 支援更豐富的資料結構,例如 hashes, lists, sets 等,同時支援資料持久化。除此之外,Redis 還提供一些類資料庫的特性,比如事務,HA,主從庫。可以說 Redis 兼具了快取系統和資料庫的一些特性,因此有著豐富的應用場景。本文介紹 Redis 在 Spring Boot 中兩個典型的應用場景。
2. Lettuce 簡介
如果在 Java 應用中使用過 Redis 快取,那麼對 Jedis
一定不陌生, Lettuce
和 Jedis
一樣,都是連線 Redis Server
Jedis
在實現上是直連 Redis Server
,多執行緒環境下非執行緒安全,除非使用連線池,為每個 Jedis
例項增加物理連線。 Lettuce
基於 Netty
的連線例項(StatefulRedisConnection),可以在多個執行緒間併發訪問,且執行緒安全,滿足多執行緒環境下的併發訪問,同時它是可伸縮的設計,一個連線例項不夠的情況也可以按需增加連線例項。
3. Spring Boot 應用中使用方式
- 直接通過
RedisTemplate
來使用 - 使用
Spring Cache
整合Redis
- 通過
Spring Session
做Session
共享
4. 工程實戰
4.1 工程依賴 pom.xml 如下:
程式碼清單:spring-boot-redis/pom.xml
***
<dependencies> <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.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </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>
spring-boot-starter-data-redis :在
Spring Boot 2.x
後底層不再是使用Jedis
,而是換成了Lettuce
,如圖:commons-pool2 : 用作
redis
連線池,如不引入啟動會報錯。spring-session-data-redis :
Spring Session
引入,用作共享Session
。
4.2 配置檔案 application.yml
程式碼清單:spring-boot-redis/src/main/resources/application.yml
***
server:
port: 8080
servlet:
session:
timeout: 30m
spring:
application:
name: spring-boot-redis
cache:
# 使用了Spring Cache後,能指定spring.cache.type就手動指定一下,雖然它會自動去適配已有Cache的依賴,但先後順序會對Redis使用有影響(JCache -> EhCache -> Redis -> Guava)
type: REDIS
redis:
host: 192.168.0.128
port: 6379
password: 123456
# 連線超時時間(ms)
timeout: 10000
# Redis預設情況下有16個分片,這裡配置具體使用的分片,預設是0
database: 0
lettuce:
pool:
# 連線池最大連線數(使用負值表示沒有限制) 預設 8
max-active: 100
# 連線池最大阻塞等待時間(使用負值表示沒有限制) 預設 -1
max-wait: -1
# 連線池中的最大空閒連線 預設 8
max-idle: 8
# 連線池中的最小空閒連線 預設 0
min-idle: 0
這裡的配置不多解釋,需要解釋的已經標註註釋。
4.3 RedisTemplate 使用方式
4.3.1 建立實體類 User.java
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/model/User.java
***
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 662692455422902539L;
private Long id;
private String name;
private int age;
}
4.3.2 自定義 RedisTemplate
預設情況下的模板只能支援 RedisTemplate<String, String>
,也就是隻能存入字串,這在開發中是不友好的,所以自定義模板是很有必要的,當自定義了模板又想使用 String
儲存這時候就可以使用 StringRedisTemplate
的方式,它們並不衝突,新增配置類 RedisCacheConfig.java
,程式碼如下:
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/config/RedisCacheConfig.java
***
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheConfig {
@Bean
public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
4.3.3 測試介面 UserController.java
程式碼清單:
***
@RestController
@Slf4j
public class UserController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate<String, Serializable> redisCacheTemplate;
@Autowired
UserService userService;
@GetMapping("/test")
public void test() {
stringRedisTemplate.opsForValue().set("geekdigging", "https://www.geekdigging.com/");
log.info("當前獲取物件:{}",stringRedisTemplate.opsForValue().get("geekdigging"));
redisCacheTemplate.opsForValue().set("geekdigging.com", new User(1L, "geekdigging", 18));
User user = (User) redisCacheTemplate.opsForValue().get("geekdigging.com");
log.info("當前獲取物件:{}", user);
}
}
4.3.4 測試
啟動服務,開啟瀏覽器訪問連結:http://localhost:8080/test ,檢視控制檯日誌列印,如下:
2019-09-24 23:49:30.191 INFO 19108 --- [nio-8080-exec-1] c.s.s.controller.UserController : 當前獲取物件:https://www.geekdigging.com/
2019-09-24 23:49:30.243 INFO 19108 --- [nio-8080-exec-1] c.s.s.controller.UserController : 當前獲取物件:User(id=1, name=geekdigging, age=18)
測試成功。
4.4 使用 Spring Cache 整合 Redis
4.4.1 Spring Cache 特點
Spring 3.1 引入了激動人心的基於註釋(annotation)的快取(cache)技術,它本質上不是一個具體的快取實現方案(例如 EHCache 或者 Redis),而是一個對快取使用的抽象,通過在既有程式碼中新增少量它定義的各種 annotation,即能夠達到快取方法的返回物件的效果。
Spring Cache 具備相當的好的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義快取的 key 和各種 condition,還提供開箱即用的快取臨時儲存方案,也支援和主流的專業快取例如 EHCache、Redis、Guava 的整合。
- 基於 annotation 即可使得現有程式碼支援快取
- 開箱即用 Out-Of-The-Box,不用安裝和部署額外第三方元件即可使用快取
- 支援 Spring Express Language,能使用物件的任何屬性或者方法來定義快取的 key 和 condition
- 支援 AspectJ,並通過其實現任何方法的快取支援
- 支援自定義 key 和自定義快取管理者,具有相當的靈活性和擴充套件性
4.4.2 定義介面 UserService.java
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/service/UserService.java
***
public interface UserService {
User save(User user);
User get(Long id);
void delete(Long id);
}
4.4.3 介面實現 UserServiceImpl.java
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/service/impl/UserServiceImpl.java
***
@Service
@Slf4j
public class UserServiceImpl implements UserService {
private static final Map<Long, User> USER_MAP = new HashMap<>();
static {
USER_MAP.put(1L, new User(1L, "geekdigging.com", 18));
USER_MAP.put(2L, new User(2L, "geekdigging.com", 19));
USER_MAP.put(3L, new User(3L, "geekdigging.com", 20));
}
@CachePut(value = "user", key = "#user.id")
@Override
public User save(User user) {
USER_MAP.put(user.getId(), user);
log.info("進入 save 方法,當前儲存物件:{}", user);
return user;
}
@Cacheable(value = "user", key = "#id")
@Override
public User get(Long id) {
log.info("進入 get 方法,當前獲取物件:{}", USER_MAP.get(id));
return USER_MAP.get(id);
}
@CacheEvict(value = "user", key = "#id")
@Override
public void delete(Long id) {
USER_MAP.remove(id);
log.info("進入 delete 方法,刪除成功");
}
}
為了方便演示資料庫操作,直接定義了一個 Map<Long, User> USER_MAP
,這裡的核心就是三個註解 @Cacheable
、 @CachePut
、 @CacheEvict
。
4.4.3.1 @Cacheable
根據方法的請求引數對其結果進行快取
- key: 快取的 key,可以為空,如果指定要按照 SpEL 表示式編寫,如果不指定,則預設按照方法的所有引數進行組合(如:
@Cacheable(value="user",key="#userName")
) - value: 快取的名稱,必須指定至少一個(如:
@Cacheable(value="user")
或者@Cacheable(value={"user1","use2"})
) - condition: 快取的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行快取(如:
@Cacheable(value = "user", key = "#id",condition = "#id < 10")
)
4.4.3.2 @CachePut
根據方法的請求引數對其結果進行快取,和 @Cacheable 不同的是,它每次都會觸發真實方法的呼叫
- key: 同上
- value: 同上
- condition: 同上
4.4.3.3 @CachEvict
根據條件對快取進行清空
- key: 同上
- value: 同上
- condition: 同上
- allEntries: 是否清空所有快取內容,預設為 false,如果指定為 true,則方法呼叫後將立即清空所有快取(如:
@CacheEvict(value = "user", key = "#id", allEntries = true)
) - beforeInvocation: 是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空快取,預設情況下,如果方法執行丟擲異常,則不會清空快取(如:
@CacheEvict(value = "user", key = "#id", beforeInvocation = true)
)
4.4.4 啟動主類
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/SpringBootRedisApplication.java
***
@SpringBootApplication
@EnableCaching
public class SpringBootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRedisApplication.class, args);
}
}
- 這裡需增加註解
@EnableCaching
開啟 Spring Session。
4.4.5 增加測試介面
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/controller/UserController.java
***
@GetMapping("/test1")
public void test1() {
User user = userService.save(new User(4L, "geekdigging.com", 35));
log.info("當前 save 物件:{}", user);
user = userService.get(1L);
log.info("當前 get 物件:{}", user);
userService.delete(5L);
}
4.4.6 測試
啟動服務,開啟瀏覽器訪問連結:http://localhost:8080/test ,重新整理頁面,控制檯日誌列印如下:
2019-09-25 00:07:21.887 INFO 21484 --- [nio-8080-exec-1] c.s.s.service.impl.UserServiceImpl : 進入 save 方法,當前儲存物件:User(id=4, name=geekdigging.com, age=35)
2019-09-25 00:07:21.897 INFO 21484 --- [nio-8080-exec-1] c.s.s.controller.UserController : 當前 save 物件:User(id=4, name=geekdigging.com, age=35)
2019-09-25 00:07:21.899 INFO 21484 --- [nio-8080-exec-1] c.s.s.service.impl.UserServiceImpl : 進入 get 方法,當前獲取物件:User(id=1, name=geekdigging.com, age=18)
2019-09-25 00:07:21.900 INFO 21484 --- [nio-8080-exec-1] c.s.s.controller.UserController : 當前 get 物件:User(id=1, name=geekdigging.com, age=18)
2019-09-25 00:07:21.901 INFO 21484 --- [nio-8080-exec-1] c.s.s.service.impl.UserServiceImpl : 進入 delete 方法,刪除成功
再次重新整理頁面,檢視控制檯日誌:
2019-09-25 00:08:54.076 INFO 21484 --- [nio-8080-exec-7] c.s.s.service.impl.UserServiceImpl : 進入 save 方法,當前儲存物件:User(id=4, name=geekdigging.com, age=35)
2019-09-25 00:08:54.077 INFO 21484 --- [nio-8080-exec-7] c.s.s.controller.UserController : 當前 save 物件:User(id=4, name=geekdigging.com, age=35)
2019-09-25 00:08:54.079 INFO 21484 --- [nio-8080-exec-7] c.s.s.controller.UserController : 當前 get 物件:User(id=1, name=geekdigging.com, age=18)
2019-09-25 00:08:54.079 INFO 21484 --- [nio-8080-exec-7] c.s.s.service.impl.UserServiceImpl : 進入 delete 方法,刪除成功
結果和我們期望的一致,可以看到增刪改查中,查詢是沒有日誌輸出的,因為它直接從快取中獲取的資料,而新增、修改、刪除都是會進入 UserServiceImpl
的方法內執行具體的業務程式碼。
4.5 Session 共享
4.5.1 Spring Session 簡介
Spring Session 提供了一套建立和管理 Servlet HttpSession 的方案。Spring Session 提供了叢集 Session(Clustered Sessions)功能,預設採用外接的 Redis 來儲存 Session 資料,以此來解決 Session 共享的問題。
4.5.2 啟動主類 SpringBootRedisApplication.java
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/SpringBootRedisApplication.java
***
@SpringBootApplication
@EnableCaching
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SpringBootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRedisApplication.class, args);
}
}
- maxInactiveIntervalInSeconds: 設定 Session 失效時間,使用 Spring Session 之後,原 Spring Boot 配置檔案
application.yml
中的server.session.timeout
屬性不再生效。
4.5.3 增加測試介面
程式碼清單:spring-boot-redis/src/main/java/com/springboot/springbootredis/controller/UserController.java
***
@GetMapping("/getBlogUrl")
public String getSessionId(HttpServletRequest request) {
String url = (String) request.getSession().getAttribute("url");
if (StringUtils.isEmpty(url)) {
request.getSession().setAttribute("url", "https://www.geekdigging.com/");
}
log.info("獲取session內容為: {}", request.getSession().getAttribute("url"));
return request.getRequestedSessionId();
}
4.5.4 測試
啟動服務,開啟瀏覽器訪問連結:http://localhost:8080/getBlogUrl ,檢視 Redis 當前儲存內容,如下圖:
其中 1569339180000 為失效時間,意思是這個時間後 Session 失效, b2522824-1094-478e-a435-554a551bc8bb 為 SessionId 。
4.5.6 如何在多臺服務中共享 Session
按照上面的步驟在另一個專案中再次配置一次,啟動後自動就進行了 Session 共享。
5. 示例程式碼
示例程式碼-Github
示例程式碼-Gitee
6. 參考
http://emacoo.cn/backend/spring-redis/
https://blog.battcn.com/2018/05/11/springboot/v2-nosql-red