一,使用緩存的必要性
二,redis作為緩存的優勢
三,redis作為緩存的一般邏輯圖
四,redis整合SpringBoot使用的兩種方式
4.1,純代碼手工實現
4.1.1,創建maven項目并導入相關的mysql,redis依賴
4.1.2,配置yml文件
4.1.3,配置RedisTemplate模板
4.1.4,根據數據庫構建相對應的實體類,在service層上編寫相關業務。
4.1.5,測試
4.2,注解實現緩存
4.2.1,構建數據庫并配置yml文件
4.2.2,配置緩存管理器
4.2.3,開啟注解支持
4.2.4,SpringCache的一些相關注解說明
4.2.5,創建pojo類,編寫dao與mapper.xml
4.2.6,編寫業務接口及其實現類
4.2.7,編寫controller層并進行測試
五,緩存中常見問題
5.1,緩存穿透
5.2,緩存擊穿
5.3,緩存雪崩
一,使用緩存的必要性
當服務器端收到客戶端請求量變多時,某些數據請求量大也會隨之變大,這些熱點數據要頻繁的從數據庫中讀取,給數據庫造成壓力,自然會導致服務器響應客戶端變慢。因此,在一些不考慮實時性的數據中,我們通常會將這些數據臨時存儲存在內存中,當請求時候,我們就能夠直接讀取內存中的數據及時響應。這就是使用緩存的初衷。
緩存主要用于解決高性能與高并發以時減少數據庫壓力的作用。它的本質就是將數據存儲在內存中,當數據沒有發生本質變化的時候,應盡量避免直接連接數據庫進行查詢,因為并發高時很可能會將數據庫壓塌,而是應去緩存中讀取數據,只有緩存中未查找到時再去數據庫中查詢,這樣就大大降低了數據庫的讀寫次數,增加系統的性能和能提供的并發量。
二,redis作為緩存的優勢
Redis提供了高性能的數據存儲功能。單個Redis server對請求的處理是基于單線程工作模型的,但由于是純內存操作,并且單線程的工作模式避免了線程上下文切換帶來的額外開銷,同時使用NIO多路復用機制(單線程維護多個I/O socket的狀態,socket event handler統一進行event分發,并通知到各個event listener),所以即使是單臺Redis server的性能也是非常的快,可支持11萬次/秒的SET操作,8.1萬次/秒的GET操作。這樣可以減少網絡的IO次數和數據體積。
Redis將其數據完全保存在內存中,僅使用磁盤進行持久化。與其它鍵值數據存儲相比,Redis有一組相對豐富的數據類型。支持string,list,set,sorted set,hash
Redis支持數據持久化和數據恢復,支持master/slave主/從機制,sentinal哨兵模式以及cluster集群模式,允許單點故障,雖然同時也會付出性能的代價,但是相比于MemCached,其穩定性還是有保證的(MemCached不支持數據持久化,斷電或重啟后數據消失)
Redis支持事務,操作具有原子性 - 所有Redis操作都是原子操作,這確保如果兩個客戶端并發訪問,Redis服務器能接收更新的值。
三,redis作為緩存的一般邏輯圖
如下:
采用redis作為Mysql數據庫的緩存,在查找的時候,首先查找redis緩存,如果找到則返回結果;如果在redis中沒有找到,那么查找Mysql數據庫,找到的花則返回結果并且更新redis;如果沒有找到則返回空。對于寫入的情況,直接寫入mysql數據庫,mysql數據庫通過觸發器及UDF機制自動把變更的內容更新到redis中。
四,redis整合SpringBoot使用的兩種方式
4.1,純代碼手工實現
由于這種方法筆者比較復雜,代碼量也多,因此并不推薦,只做簡單實現,供大家了解一下即可。
4.1.1,創建maven項目并導入相關的mysql,redis依賴
<?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 https://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.6.4</version>
<relativePath/>
</parent>
<groupId>com.yy</groupId>
<artifactId>springboot-08-jedis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-08-jedis</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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</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>
4.1.2,配置yml文件
spring:
datasource:
username: root
password: 123456
#時區報錯,需要添加時區
url: jdbc:mysql://localhost:3306/school?serverTimezone=UTC&useSSL=true&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
springboot-redis配置
spring:
redis:
host: redis地址
port: 6379
password: 123456
database: 0
4.1.3,配置RedisTemplate模板
從依賴項中我們可以看到,SpringBoot集成redis的依賴中為我們提供了redisTamplate類來對redis進行操作。
其中他為我們提供了默認的序列化方式 :JdkSerializationRedisSerializer,作為JDK提供的序列化功能。 這個序列化方式優點是反序列化時不需要提供類型信息(class),但缺點是需要實現Serializable接口,還有序列化后的結果非常龐大,是JSON格式的5倍左右,這樣就會消耗redis服務器的大量內存。并且這種默認序列化方式在RDM工具中查看k-v值時會出現“亂碼”,不方便查看。
因此我們需要自己去自定義序列化方式來更好的方便我們操作。在此,我更推薦去配置Jackson2JsonRedisSerializer序列化方式,因為它是使用Jackson庫將對象序列化為JSON字符串。優點是速度快,序列化后的字符串短小精悍,不需要實現Serializable接口。但缺點也非常致命,那就是此類的構造函數中有一個類型參數,必須提供要序列化對象的類型信息(.class對象)。 通過查看源代碼,發現其只在反序列化過程中用到了類型信息。
在redis配置類中配置自定義的序列化方式
package com.yy.myconfig;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
-
自定義一個redis template模板
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
//為了使開發方便,直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);//Json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //string的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //key采用string的序列化方式 template.setKeySerializer(stringRedisSerializer); //hash的key也是用string的序列化方式 template.setHashKeySerializer(stringRedisSerializer); //value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); //hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template;
}
}
為了方便我們對reids不同的數據類型的操作,有需要的同學也可以將redisTamplate封裝為一個工具類,以便后面直接調用。
package com.yy.utils;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.Date;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return this.redisTemplate;
}
/** -------------------key相關操作--------------------- */
/**
* 刪除key
*
* @param key
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 批量刪除key
*
* @param keys
*/
public void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}
/**
* 序列化key
*
* @param key
* @return
*/
public byte[] dump(String key) {
return redisTemplate.dump(key);
}
/**
* 是否存在key
*
* @param key
* @return
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 設置過期時間
*
* @param key
* @param timeout
* @param unit
* @return
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 設置過期時間
*
* @param key
* @param date
* @return
*/
public Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}
/**
* 查找匹配的key
*
* @param pattern
* @return
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 將當前數據庫的 key 移動到給定的數據庫 db 當中
*
* @param key
* @param dbIndex
* @return
*/
public Boolean move(String key, int dbIndex) {
return redisTemplate.move(key, dbIndex);
}
/**
* 移除 key 的過期時間,key 將持久保持
*
* @param key
* @return
*/
public Boolean persist(String key) {
return redisTemplate.persist(key);
}
/**
* 返回 key 的剩余的過期時間
*
* @param key
* @param unit
* @return
*/
public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}
/**
* 返回 key 的剩余的過期時間
*
* @param key
* @return
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
/**
* 從當前數據庫中隨機返回一個 key
*
* @return
*/
public String randomKey() {
return (String) redisTemplate.randomKey();
}
/**
* 修改 key 的名稱
*
* @param oldKey
* @param newKey
*/
public void rename(String oldKey, String newKey) {
redisTemplate.rename(oldKey, newKey);
}
/**
* 僅當 newkey 不存在時,將 oldKey 改名為 newkey
*
* @param oldKey
* @param newKey
* @return
*/
public Boolean renameIfAbsent(String oldKey, String newKey) {
return redisTemplate.renameIfAbsent(oldKey, newKey);
}
/**
* 返回 key 所儲存的值的類型
*
* @param key
* @return
*/
public DataType type(String key) {
return redisTemplate.type(key);
}
/** -------------------string相關操作--------------------- */
/**
* 設置指定 key 的值
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 獲取指定 key 的值
*
* @param key
* @return
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 返回 key 中字符串值的子字符
*
* @param key
* @param start
* @param end
* @return
*/
public String getRange(String key, long start, long end) {
return redisTemplate.opsForValue().get(key, start, end);
}
/**
* 將給定 key 的值設為 value ,并返回 key 的舊值(old value)
*
* @param key
* @param value
* @return
*/
public String getAndSet(String key, String value) {
return (String) redisTemplate.opsForValue().getAndSet(key, value);
}
/**
* 對 key 所儲存的字符串值,獲取指定偏移量上的位(bit)
*
* @param key
* @param offset
* @return
*/
public Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
/**
* 批量獲取
*
* @param keys
* @return
*/
public List<Object> multiGet(Collection<String> keys) {
return redisTemplate.opsForValue().multiGet(keys);
}
/**
* 設置ASCII碼, 字符串'a'的ASCII碼是97, 轉為二進制是'01100001', 此方法是將二進制第offset位值變為value
*
* @param key 位置
* @param value 值,true為1, false為0
* @return
*/
public boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 將值 value 關聯到 key ,并將 key 的過期時間設為 timeout
*
* @param key
* @param value
* @param timeout 過期時間
* @param unit 時間單位, 天:TimeUnit.DAYS 小時:TimeUnit.HOURS 分鐘:TimeUnit.MINUTES
* 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
*/
public void setEx(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 只有在 key 不存在時設置 key 的值
*
* @param key
* @param value
* @return 之前已經存在返回false, 不存在返回true
*/
public boolean setIfAbsent(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 用 value 參數覆寫給定 key 所儲存的字符串值,從偏移量 offset 開始
*
* @param key
* @param value
* @param offset 從指定位置開始覆寫
*/
public void setRange(String key, String value, long offset) {
redisTemplate.opsForValue().set(key, value, offset);
}
/**
* 獲取字符串的長度
*
* @param key
* @return
*/
public Long size(String key) {
return redisTemplate.opsForValue().size(key);
}
/**
* 批量添加
*
* @param maps
*/
public void multiSet(Map<String, String> maps) {
redisTemplate.opsForValue().multiSet(maps);
}
/**
* 同時設置一個或多個 key-value 對,當且僅當所有給定 key 都不存在
*
* @param maps
* @return 之前已經存在返回false, 不存在返回true
*/
public boolean multiSetIfAbsent(Map<String, String> maps) {
return redisTemplate.opsForValue().multiSetIfAbsent(maps);
}
/**
* 增加(自增長), 負數則為自減
*
* @param key
* @return
*/
public Long incrBy(String key, long increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* @param key
* @return
*/
public Double incrByFloat(String key, double increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* 追加到末尾
*
* @param key
* @param value
* @return
*/
public Integer append(String key, String value) {
return redisTemplate.opsForValue().append(key, value);
}
/** -------------------hash相關操作------------------------- */
/**
* 獲取存儲在哈希表中指定字段的值
*
* @param key
* @param field
* @return
*/
public Object hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
/**
* 獲取所有給定字段的值
*
* @param key
* @return
*/
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 獲取所有給定字段的值
*
* @param key
* @param fields
* @return
*/
public List<Object> hMultiGet(String key, Collection<Object> fields) {
return redisTemplate.opsForHash().multiGet(key, fields);
}
public void hPut(String key, String hashKey, String value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public void hPutAll(String key, Map<String, String> maps) {
redisTemplate.opsForHash().putAll(key, maps);
}
/**
* 僅當hashKey不存在時才設置
*
* @param key
* @param hashKey
* @param value
* @return
*/
public Boolean hPutIfAbsent(String key, String hashKey, String value) {
return redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
}
/**
* 刪除一個或多個哈希表字段
*
* @param key
* @param fields
* @return
*/
public Long hDelete(String key, Object... fields) {
return redisTemplate.opsForHash().delete(key, fields);
}
/**
* 查看哈希表 key 中,指定的字段是否存在
*
* @param key
* @param field
* @return
*/
public boolean hExists(String key, String field) {
return redisTemplate.opsForHash().hasKey(key, field);
}
/**
* 為哈希表 key 中的指定字段的整數值加上增量 increment
*
* @param key
* @param field
* @param increment
* @return
*/
public Long hIncrBy(String key, Object field, long increment) {
return redisTemplate.opsForHash().increment(key, field, increment);
}
/**
* 為哈希表 key 中的指定字段的整數值加上增量 increment
*
* @param key
* @param field
* @param delta
* @return
*/
public Double hIncrByFloat(String key, Object field, double delta) {
return redisTemplate.opsForHash().increment(key, field, delta);
}
/**
* 獲取所有哈希表中的字段
*
* @param key
* @return
*/
public Set<Object> hKeys(String key) {
return redisTemplate.opsForHash().keys(key);
}
/**
* 獲取哈希表中字段的數量
*
* @param key
* @return
*/
public Long hSize(String key) {
return redisTemplate.opsForHash().size(key);
}
/**
* 獲取哈希表中所有值
*
* @param key
* @return
*/
public List<Object> hValues(String key) {
return redisTemplate.opsForHash().values(key);
}
/**
* 迭代哈希表中的鍵值對
*
* @param key
* @param options
* @return
*/
public Cursor<Map.Entry<Object, Object>> hScan(String key, ScanOptions options) {
return redisTemplate.opsForHash().scan(key, options);
}
/** ------------------------list相關操作---------------------------- */
/**
* 通過索引獲取列表中的元素
*
* @param key
* @param index
* @return
*/
public String lIndex(String key, long index) {
return (String) redisTemplate.opsForList().index(key, index);
}
/**
* 獲取列表指定范圍內的元素
*
* @param key
* @param start 開始位置, 0是開始位置
* @param end 結束位置, -1返回所有
* @return
*/
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 存儲在list頭部
*
* @param key
* @param value
* @return
*/
public Long lLeftPush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lLeftPushAll(String key, String... value) {
return redisTemplate.opsForList().leftPushAll(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lLeftPushAll(String key, Collection<String> value) {
return redisTemplate.opsForList().leftPushAll(key, value);
}
/**
* 當list存在的時候才加入
*
* @param key
* @param value
* @return
*/
public Long lLeftPushIfPresent(String key, String value) {
return redisTemplate.opsForList().leftPushIfPresent(key, value);
}
/**
* 如果pivot存在,再pivot前面添加
*
* @param key
* @param pivot
* @param value
* @return
*/
public Long lLeftPush(String key, String pivot, String value) {
return redisTemplate.opsForList().leftPush(key, pivot, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lRightPush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lRightPushAll(String key, String... value) {
return redisTemplate.opsForList().rightPushAll(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lRightPushAll(String key, Collection<String> value) {
return redisTemplate.opsForList().rightPushAll(key, value);
}
/**
* 為已存在的列表添加值
*
* @param key
* @param value
* @return
*/
public Long lRightPushIfPresent(String key, String value) {
return redisTemplate.opsForList().rightPushIfPresent(key, value);
}
/**
* 在pivot元素的右邊添加值
*
* @param key
* @param pivot
* @param value
* @return
*/
public Long lRightPush(String key, String pivot, String value) {
return redisTemplate.opsForList().rightPush(key, pivot, value);
}
/**
* 通過索引設置列表元素的值
*
* @param key
* @param index 位置
* @param value
*/
public void lSet(String key, long index, String value) {
redisTemplate.opsForList().set(key, index, value);
}
/**
* 移出并獲取列表的第一個元素
*
* @param key
* @return 刪除的元素
*/
public String lLeftPop(String key) {
return (String) redisTemplate.opsForList().leftPop(key);
}
/**
* 移出并獲取列表的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止
*
* @param key
* @param timeout 等待時間
* @param unit 時間單位
* @return
*/
public String lBLeftPop(String key, long timeout, TimeUnit unit) {
return (String) redisTemplate.opsForList().leftPop(key, timeout, unit);
}
/**
* 移除并獲取列表最后一個元素
*
* @param key
* @return 刪除的元素
*/
public String lRightPop(String key) {
return (String) redisTemplate.opsForList().rightPop(key);
}
/**
* 移出并獲取列表的最后一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止
*
* @param key
* @param timeout 等待時間
* @param unit 時間單位
* @return
*/
public String lBRightPop(String key, long timeout, TimeUnit unit) {
return (String) redisTemplate.opsForList().rightPop(key, timeout, unit);
}
/**
* 移除列表的最后一個元素,并將該元素添加到另一個列表并返回
*
* @param sourceKey
* @param destinationKey
* @return
*/
public String lRightPopAndLeftPush(String sourceKey, String destinationKey) {
return (String) redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
destinationKey);
}
/**
* 從列表中彈出一個值,將彈出的元素插入到另外一個列表中并返回它; 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止
*
* @param sourceKey
* @param destinationKey
* @param timeout
* @param unit
* @return
*/
public String lBRightPopAndLeftPush(String sourceKey, String destinationKey,
long timeout, TimeUnit unit) {
return (String) redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
destinationKey, timeout, unit);
}
/**
* 刪除集合中值等于value得元素
*
* @param key
* @param index index=0, 刪除所有值等于value的元素; index>0, 從頭部開始刪除第一個值等于value的元素;
* index<0, 從尾部開始刪除第一個值等于value的元素;
* @param value
* @return
*/
public Long lRemove(String key, long index, String value) {
return redisTemplate.opsForList().remove(key, index, value);
}
/**
* 裁剪list
*
* @param key
* @param start
* @param end
*/
public void lTrim(String key, long start, long end) {
redisTemplate.opsForList().trim(key, start, end);
}
/**
* 獲取列表長度
*
* @param key
* @return
*/
public Long lLen(String key) {
return redisTemplate.opsForList().size(key);
}
/** --------------------set相關操作-------------------------- */
/**
* set添加元素
*
* @param key
* @param values
* @return
*/
public Long sAdd(String key, String... values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* set移除元素
*
* @param key
* @param values
* @return
*/
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
/**
* 移除并返回集合的一個隨機元素
*
* @param key
* @return
*/
public String sPop(String key) {
return (String) redisTemplate.opsForSet().pop(key);
}
/**
* 將元素value從一個集合移到另一個集合
*
* @param key
* @param value
* @param destKey
* @return
*/
public Boolean sMove(String key, String value, String destKey) {
return redisTemplate.opsForSet().move(key, value, destKey);
}
/**
* 獲取集合的大小
*
* @param key
* @return
*/
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 判斷集合是否包含value
*
* @param key
* @param value
* @return
*/
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 獲取兩個集合的交集
*
* @param key
* @param otherKey
* @return
*/
public Set<Object> sIntersect(String key, String otherKey) {
return redisTemplate.opsForSet().intersect(key, otherKey);
}
/**
* 獲取key集合與多個集合的交集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sIntersect(String key, Collection<String> otherKeys) {
return redisTemplate.opsForSet().intersect(key, otherKeys);
}
/**
* key集合與otherKey集合的交集存儲到destKey集合中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long sIntersectAndStore(String key, String otherKey, String destKey) {
return redisTemplate.opsForSet().intersectAndStore(key, otherKey,
destKey);
}
/**
* key集合與多個集合的交集存儲到destKey集合中
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long sIntersectAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForSet().intersectAndStore(key, otherKeys,
destKey);
}
/**
* 獲取兩個集合的并集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sUnion(String key, String otherKeys) {
return redisTemplate.opsForSet().union(key, otherKeys);
}
/**
* 獲取key集合與多個集合的并集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sUnion(String key, Collection<String> otherKeys) {
return redisTemplate.opsForSet().union(key, otherKeys);
}
/**
* key集合與otherKey集合的并集存儲到destKey中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long sUnionAndStore(String key, String otherKey, String destKey) {
return redisTemplate.opsForSet().unionAndStore(key, otherKey, destKey);
}
/**
* key集合與多個集合的并集存儲到destKey中
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long sUnionAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 獲取兩個集合的差集
*
* @param key
* @param otherKey
* @return
*/
public Set<Object> sDifference(String key, String otherKey) {
return redisTemplate.opsForSet().difference(key, otherKey);
}
/**
* 獲取key集合與多個集合的差集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sDifference(String key, Collection<String> otherKeys) {
return redisTemplate.opsForSet().difference(key, otherKeys);
}
/**
* key集合與otherKey集合的差集存儲到destKey中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long sDifference(String key, String otherKey, String destKey) {
return redisTemplate.opsForSet().differenceAndStore(key, otherKey,
destKey);
}
/**
* key集合與多個集合的差集存儲到destKey中
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long sDifference(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForSet().differenceAndStore(key, otherKeys,
destKey);
}
/**
* 獲取集合所有元素
*
* @param key
* @return
*/
public Set<Object> setMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 隨機獲取集合中的一個元素
*
* @param key
* @return
*/
public String sRandomMember(String key) {
return (String) redisTemplate.opsForSet().randomMember(key);
}
/**
* 隨機獲取集合中count個元素
*
* @param key
* @param count
* @return
*/
public List<Object> sRandomMembers(String key, long count) {
return redisTemplate.opsForSet().randomMembers(key, count);
}
/**
* 隨機獲取集合中count個元素并且去除重復的
*
* @param key
* @param count
* @return
*/
public Set<Object> sDistinctRandomMembers(String key, long count) {
return redisTemplate.opsForSet().distinctRandomMembers(key, count);
}
/**
* @param key
* @param options
* @return
*/
public Cursor<Object> sScan(String key, ScanOptions options) {
return redisTemplate.opsForSet().scan(key, options);
}
/**------------------zSet相關操作--------------------------------*/
/**
* 添加元素,有序集合是按照元素的score值由小到大排列
*
* @param key
* @param value
* @param score
* @return
*/
public Boolean zAdd(String key, String value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
/**
* @param key
* @param values
* @return
*/
public Long zAdd(String key, Set<ZSetOperations.TypedTuple<Object>> values) {
return redisTemplate.opsForZSet().add(key, values);
}
/**
* @param key
* @param values
* @return
*/
public Long zRemove(String key, Object... values) {
return redisTemplate.opsForZSet().remove(key, values);
}
/**
* 增加元素的score值,并返回增加后的值
*
* @param key
* @param value
* @param delta
* @return
*/
public Double zIncrementScore(String key, String value, double delta) {
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
/**
* 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
*
* @param key
* @param value
* @return 0表示第一位
*/
public Long zRank(String key, Object value) {
return redisTemplate.opsForZSet().rank(key, value);
}
/**
* 返回元素在集合的排名,按元素的score值由大到小排列
*
* @param key
* @param value
* @return
*/
public Long zReverseRank(String key, Object value) {
return redisTemplate.opsForZSet().reverseRank(key, value);
}
/**
* 獲取集合的元素, 從小到大排序
*
* @param key
* @param start 開始位置
* @param end 結束位置, -1查詢所有
* @return
*/
public Set<Object> zRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* 獲取集合元素, 并且把score值也獲取
*
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScores(String key, long start,
long end) {
return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
}
/**
* 根據Score值查詢集合元素
*
* @param key
* @param min 最小值
* @param max 最大值
* @return
*/
public Set<Object> zRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
/**
* 根據Score值查詢集合元素, 從小到大排序
*
* @param key
* @param min 最小值
* @param max 最大值
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key,
double min, double max) {
return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
}
/**
* @param key
* @param min
* @param max
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key,
double min, double max, long start, long end) {
return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max,
start, end);
}
/**
* 獲取集合的元素, 從大到小排序
*
* @param key
* @param start
* @param end
* @return
*/
public Set<Object> zReverseRange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
/**
* 獲取集合的元素, 從大到小排序, 并返回score值
*
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeWithScores(String key,
long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start,
end);
}
/**
* 根據Score值查詢集合元素, 從大到小排序
*
* @param key
* @param min
* @param max
* @return
*/
public Set<Object> zReverseRangeByScore(String key, double min,
double max) {
return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
}
/**
* 根據Score值查詢集合元素, 從大到小排序
*
* @param key
* @param min
* @param max
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeByScoreWithScores(
String key, double min, double max) {
return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
min, max);
}
/**
* @param key
* @param min
* @param max
* @param start
* @param end
* @return
*/
public Set<Object> zReverseRangeByScore(String key, double min,
double max, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max,
start, end);
}
/**
* 根據score值獲取集合元素數量
*
* @param key
* @param min
* @param max
* @return
*/
public Long zCount(String key, double min, double max) {
return redisTemplate.opsForZSet().count(key, min, max);
}
/**
* 獲取集合大小
*
* @param key
* @return
*/
public Long zSize(String key) {
return redisTemplate.opsForZSet().size(key);
}
/**
* 獲取集合大小
*
* @param key
* @return
*/
public Long zZCard(String key) {
return redisTemplate.opsForZSet().zCard(key);
}
/**
* 獲取集合中value元素的score值
*
* @param key
* @param value
* @return
*/
public Double zScore(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 移除指定索引位置的成員
*
* @param key
* @param start
* @param end
* @return
*/
public Long zRemoveRange(String key, long start, long end) {
return redisTemplate.opsForZSet().removeRange(key, start, end);
}
/**
* 根據指定的score值的范圍來移除成員
*
* @param key
* @param min
* @param max
* @return
*/
public Long zRemoveRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
}
/**
* 獲取key和otherKey的并集并存儲在destKey中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long zUnionAndStore(String key, String otherKey, String destKey) {
return redisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey);
}
/**
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long zUnionAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForZSet()
.unionAndStore(key, otherKeys, destKey);
}
/**
* 交集
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long zIntersectAndStore(String key, String otherKey,
String destKey) {
return redisTemplate.opsForZSet().intersectAndStore(key, otherKey,
destKey);
}
/**
* 交集
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long zIntersectAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForZSet().intersectAndStore(key, otherKeys,
destKey);
}
/**
* @param key
* @param options
* @return
*/
public Cursor<ZSetOperations.TypedTuple<Object>> zScan(String key, ScanOptions options) {
return redisTemplate.opsForZSet().scan(key, options);
}
}
4.1.4,根據數據庫構建相對應的實體類,在service層上編寫相關業務。
dao層上的簡單操作
package com.yy.service;
import com.yy.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface US {
@Select("select * from user where id=#{id}")
User selectUserById(Integer id);
}
業務類上進行實現
package com.yy.service;
import com.yy.pojo.User;
import com.yy.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ServiceImpl {
@Autowired
private US us;
@Autowired
RedisUtils redisUtils;
public User select(Integer id) {
if (Boolean.FALSE.equals(redisUtils.hasKey(String.valueOf(id)))) {
log.info("查詢mysql數據庫");
User user = us.selectUserById(id);
user.setId(id);
user.setUsername("張三");
user.setPwd("147852");
redisUtils.hPut("user", String.valueOf(id), String.valueOf(user));
return user;
} else {
log.info("查詢redis");
return (User) redisUtils.hGet("user", String.valueOf(id));
}
}
}
4.1.5,測試
編寫一個測試類進行測試:
@Test
void test() {
System.out.println(service.select(1002));
}
運行結果可以看到數據已經查詢出來,并且將數據存儲在redis中了
但是這種方式,代碼量和操作都不太方便。因此,在項目中我跟推薦使用注解去實現。
4.2,注解實現緩存
因為導入依賴與上面大致一樣,需要注意的是我在這個新的測試上使用了Mybatis-plus插件,所以依賴上還需要加上相關依賴,其他地方不重復詳述。
4.2.1,構建數據庫并配置yml文件
server:
port: 8090
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
password: 123456
username: root
url: jdbc:mysql://localhost:3306/db_user?&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
redis:
database: 0
port: 6379
host: redis的ip地址
timeout: 50000
lettuce:
pool:
max-active: 8 #連接池最大連接數
max-wait: -1 #最大阻塞等待時間(負數表示無限制)
max-idle: 8 #最大空閑連接數
password: 123456
aop:
proxy-target-class: true
mybatis-plus:
mapper-locations: classPath:/mapper/*.xml
configuration:
map-underscore-to-camel-case: false # 禁止大寫變小寫時自動添加下劃線
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.yy.pojo
為了測試一下緩存效果,這次筆者重新構建了一個數據庫,并向數據庫中導入10萬條測試數據。以備后面測試緩存讀取數據時的效果。
4.2.2,配置緩存管理器
package com.yy.redisCache.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
/**
* 配置緩存管理器
* @param factory 線程連接工廠
* @return 緩存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//設置緩存過期時間5分鐘
config.entryTtl(Duration.ofMinutes(5))
//設置緩存前綴
.prefixCacheNameWith("redis:cache:")
//禁止緩存null值
.disableCachingNullValues()
//添加鍵值序列化配置
.serializeKeysWith(keyPair())
.serializeValuesWith(valuePair());
return RedisCacheManager.builder(factory)
.withCacheConfiguration("test",config)
.build();
}
/**
* 配置鍵序列化
* @return
*/
@Bean
RedisSerializationContext.SerializationPair<String> keyPair(){
return RedisSerializationContext.SerializationPair.fromSerializer( new StringRedisSerializer());
}
/**
* 配置值序列化
* @return
*/
@Bean
RedisSerializationContext.SerializationPair<Object> valuePair(){
return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
}
}
4.2.3,開啟注解支持
在啟動類上添加@EnableCache,開啟緩存功能
package com.yy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableCaching//開啟緩存
@MapperScan({"com.yy.dao","com.yy.redisCache.dao"})
@EnableTransactionManagement
public class SpringbootMongodbStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootMongodbStudyApplication.class, args);
}
}
4.2.4,SpringCache的一些相關注解說明
類型 名稱 解釋
注解
@CacheConfig
一般配置在指定類上用于指定緩存名稱,并且名稱與緩存管理器中的緩存名保持一致
@Cacheable 可標記在方法上,也可以標記在類上。標記在類上時表示該類的所有方法都支持緩存。標記在方法上時,該方法的返回結果將會進行緩存。如果已經存在該緩存,則會直接從緩存中讀取數據。可以通過key指定入參,value為方法返回值緩存在得 Cache名稱
@CachePut 可以標注在類上和方法上。無論是否存在緩存,每次都會重新添加緩存,常用于更新。
@CacheEvit 可以標記在一個方法上,也可以標記在一個類上。標記方法上標識方法執行時候觸發清楚緩存操作,當標記在一個類上時表示其中所有的方法的執行都會觸發緩存的清除操作。
@Caching 可以讓我們在一個方法或者類上同時指定多個Spring Cache相關的注解。
注解配置參數 value,cacheNames 指定(緩存管理器) Cache名稱,效果類似于@CacheConfig中的指定名稱。如果已經用@CacheConfig在類上指定了,方法中就不需要使用了,就像提取公共名稱一樣
key 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合。未設置則為默認值。
condition 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存
unless 可以用來是決定是否添加到緩存,與 condition不同的是,unless表達式是在方法調用之后進行評估的。如果返回false,才放入緩存 (與condition相反)
sync 在多線程環境下,某些操作可能使用相同參數同步調用。默認情況下,緩存不鎖定任何資源,可能導致多次計算,而違反了緩存的目的。對于這些特定的情況,屬性 sync 可以指示底層將緩存鎖住,使只有一個線程可以進入計算,而其他線程堵塞,直到返回結果更新到緩存中
EL表達式可以使用方法參數及對應的屬性。使用方法參數時,可以直接使用 "#參數名" 或者 "#p參數index9(參數下標)",#result為返回值
4.2.5,創建pojo類,編寫dao與mapper.xml
package com.yy.redisCache.pojo;
import lombok.Data;
import java.io.Serializable;
/**
- @author young
- @date 2022/9/4 13:02
- @description:
*/
@Data
public class RedisTest implements Serializable {
private static final long serialVersionUID = -7120480143440693836L;
private Integer id;
private String redis_name;
private String redis_pwd;
private String address;
}
4.2.6,編寫業務接口及其實現類
package com.yy.redisCache.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yy.redisCache.pojo.RedisTest;
/**
- @author young
- @date 2022/9/4 13:06
- @description: 業務接口
*/
public interface RedisTestService extends IService<RedisTest> {
}
在實現類中添加緩存注解,指定的業務的返回結果存入緩存。
package com.yy.redisCache.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yy.redisCache.dao.RedisTestDao;
import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.RedisTestService;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
@author young
@date 2022/9/4 13:08
-
@description: service實現類
*/
@Service
@CacheConfig(cacheNames = "test")//緩存名,和管理器中配置的一致
public class RedisTestServiceImpl extends ServiceImpl<RedisTestDao, RedisTest> implements RedisTestService {
@Resource
private RedisTestDao redisTestDao;/**
- 查尋10000條數據
- @return
- key 可為空,指定需按照SpEL編寫
- unless 不緩存的條件 條件為true不緩存
*/
@Override
@Cacheable(key = "'allUser'",unless = "#result==null")//#result返回結果
public List<RedisTest> list() {
QueryWrapper<RedisTest> wrapper = new QueryWrapper<>();
wrapper.le("id",10000);
return redisTestDao.selectList(wrapper);
}
/**
* 批量查詢
* @param idList
* @return
*/
@Override
public List<RedisTest> listByIds(Collection<? extends Serializable> idList) {
return super.listByIds(idList);
}
/**
* 刪除批量信息
* @param list
* @return
*/
@CacheEvict
@Override
public boolean removeByIds(Collection<?> list) {
return super.removeByIds(list);
}
/**
* 修改某個對象
* @param entity
* @return
*/
@Override
@CachePut
public boolean updateById(RedisTest entity) {
return super.updateById(entity);
}
}
4.2.7,編寫controller層并進行測試
package com.yy.redisCache.controller;
import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.impl.RedisTestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@author young
@date 2022/9/4 13:13
-
@description:
*/
@RestController
@RequestMapping("/Cache")
@Slf4j
public class RedisCacheController {
@Autowired
private RedisTestServiceImpl service;/**
- 刪除指定id以下的所有數據及緩存
- @param id id
- @return
*/
@DeleteMapping("/del/{id}")
public Boolean remove(@PathVariable Integer id){
List<Integer> list = new ArrayList<>();
for (int i = 0; i <= id; i++) {
list.add(i);
}
return service.removeByIds(list);
}
/**
- 更新數據
- @param redisTest 更新對象
- @return
*/
@PutMapping("/update")
public Boolean update(RedisTest redisTest){
return service.updateById(redisTest);
}
/**
- 測試從數據庫/緩存獲取10000條數據的時間
- @return
*/
@GetMapping("/get")
public List<RedisTest> getBody(){
Long startTime = System.currentTimeMillis();
List<RedisTest> list = service.list();
//第一次存入數據庫查詢3875ms 第二次緩存查詢:1131ms
log.info("查詢時間10000條數據的時間是:{}",System.currentTimeMillis()-startTime);
return list;
}
/**
- 批量獲取指定的數據
- @param id
- @return
*/
@GetMapping("/getOne/{id}")
public String getOne(@PathVariable Integer... id){
List<RedisTest> ids = service.listByIds(Arrays.asList(id));
return ids.isEmpty()?"獲取成功":"獲取失敗";
}
}
通過接口測試工具測試獲取10000條數據的測試后,可以看到第一次獲取10000條數據后,時間為3875ms,并且redis里已經有了相關緩存。
再次執行該請求后,獲取時間大大減少為1131ms。此時,表示是從緩存讀取的數據。
可以猜想到,由于數據庫執行查詢操作時,需要進行數據庫連接等操作,因次直接從數據庫獲取數據的時間會比redis緩存慢。最后我們測試一下更新刪除緩存等操作,成功達到我們的預期結果。
五,緩存中常見問題
5.1,緩存穿透
緩存穿透是指查詢一個不存在的數據,在緩存層和持久層都查詢不到,這樣查不到的數據不會寫入緩存,這樣一個不存在的數據每次的請求都會去查詢持久層,緩存也就失去了保護后端持久層的意義了。最嚴重的后果就是是后端存儲負載加大,造成后端存儲宕機。
形成原因:
業務代碼或數據出現問題,比如讀寫key不一致
網絡爬蟲或惡意攻擊
解決方法:
1,緩存空對象:對于查詢不到的數據給它設置一個空值。但是可能會造成內存空間緊張,因為value為null也會占用內存空間,不過我們可以給它設置一個過期時間,讓空值緩存自動過期。這種方法使用與數據經常變化,實時性較高的場景。
2,布隆過濾器攔截:在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截,當收到一個對key請求時先用布隆過濾器驗證是key否存在,如果存在在進入緩存層、存儲層。可以使用bitmap做布隆過濾器。這種方法適用于數據命中不高、數據相對固定、實時性低的應用場景,代碼維護較為復雜,但是緩存空間占用少。
5.2,緩存擊穿
某個key成為一個熱點(商品秒殺時)時,這樣處于一個集中式高并發的情況下,如果key突然失效一瞬間,請求就會馬上擊穿緩存層,直接請求數據庫,這樣后端負載會很快過載甚至崩潰。
解決辦法:
1,根據實際情況設置二級緩存或者熱點緩存永不過期。
2,設置分布式互斥鎖,只允許一個線程重建緩存,其他線程等待重建緩存的線程執行完,重新從緩存獲取數據
5.3,緩存雪崩
當緩存層由于某些原因不可用(宕機)或者大量緩存由于超時時間相同在同一時間段失效(大批key失效/熱點數據失效),大量請求直接到達存儲層,存儲層壓力過大導致系統雪崩。
解決辦法:
1,設置多級緩存,不同的緩存設置不同的過期時間。
2,將緩存層設計為高可用,例如使用sentinel哨兵模式或者cluster,這樣即使個別節點或機器宕機也不會影響緩存效果實現。
3,將緩存的過期時間設置隨機分布的,避免在同一時間內緩存集體失效。
————————————————
版權聲明:本文為CSDN博主「深情不及里子」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_42263280/article/details/126760489