淺談Redis三種高效緩存讀寫策略的實(shí)現(xiàn)
在企業(yè)級(jí)應(yīng)用中,緩存是應(yīng)對(duì)高并發(fā)、提升系統(tǒng)性能的關(guān)鍵一環(huán)。而如何確保緩存與數(shù)據(jù)庫之間數(shù)據(jù)的一致性、高效性與可用性,正是我們?cè)O(shè)計(jì)緩存策略的核心。下面,我將循序漸進(jìn)地為您講解 Cache-Aside、Read/Write-Through 和 Write-Back 這三種主流策略。
準(zhǔn)備工作:環(huán)境與模型
為了讓代碼示例更貼近真實(shí)場(chǎng)景,我們先定義一個(gè)基礎(chǔ)模型和環(huán)境。
技術(shù)棧:
- Spring Boot 3.x
- Spring Data Redis
- MyBatis-Plus (或 JPA)
- MySQL
數(shù)據(jù)模型 (User.java):
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
}
數(shù)據(jù)訪問層 (UserMapper.java) (MyBatis-Plus 接口):
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
策略一:Cache-Aside (旁路緩存)
這是最經(jīng)典、最常用,也是最容易理解的緩存策略。它的核心思想是:應(yīng)用程序代碼直接負(fù)責(zé)維護(hù)緩存和數(shù)據(jù)庫。
1. 概念與工作流程
讀操作流程:
- 應(yīng)用程序先從緩存中讀取數(shù)據(jù)。
- 如果緩存命中(Cache Hit),則直接返回?cái)?shù)據(jù)。
- 如果緩存未命中(Cache Miss),則從數(shù)據(jù)庫中讀取數(shù)據(jù)。
- 將從數(shù)據(jù)庫中讀到的數(shù)據(jù)寫入緩存。
- 返回?cái)?shù)據(jù)給調(diào)用方。
寫操作流程 (關(guān)鍵點(diǎn)):
- 先更新數(shù)據(jù)庫。
- 再刪除(失效)緩存。
為什么是“刪除緩存”而不是“更新緩存”?
- 懶加載思想:只有在下次真實(shí)需要讀取該數(shù)據(jù)時(shí),才通過“讀操作流程”將其加載到緩存。如果每次更新都去刷新緩存,而這個(gè)數(shù)據(jù)后續(xù)又很少被讀取,就會(huì)造成不必要的緩存寫操作。
- 并發(fā)安全:考慮一個(gè)場(chǎng)景(寫-寫并發(fā)),如果線程A更新數(shù)據(jù)庫后更新緩存,同時(shí)線程B也更新數(shù)據(jù)庫并更新緩存。可能發(fā)生B先完成,A后完成,導(dǎo)致緩存中是A的舊數(shù)據(jù),而數(shù)據(jù)庫是B的新數(shù)據(jù),造成不一致。而“刪除緩存”能極大地降低這種不一致的概率。
2. 代碼示例 (UserServiceImpl.java)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.TimeUnit;
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String CACHE_KEY_PREFIX = "user:";
/**
* 讀取用戶 - 實(shí)現(xiàn)Cache-Aside讀策略
*/
public User getUserById(Long id) {
String key = CACHE_KEY_PREFIX + id;
// 1. 從緩存讀取
Object cachedUserObj = redisTemplate.opsForValue().get(key);
if (cachedUserObj != null) {
System.out.println("Cache Hit for user: " + id);
return objectMapper.convertValue(cachedUserObj, User.class);
}
// 2. 緩存未命中,從數(shù)據(jù)庫讀取
System.out.println("Cache Miss for user: " + id + ". Reading from DB.");
User userFromDb = userMapper.selectById(id);
// 3. 數(shù)據(jù)庫存在數(shù)據(jù),則寫入緩存
if (userFromDb != null) {
redisTemplate.opsForValue().set(key, userFromDb, 60, TimeUnit.MINUTES); // 設(shè)置60分鐘過期
}
return userFromDb;
}
/**
* 更新用戶 - 實(shí)現(xiàn)Cache-Aside寫策略
*/
public void updateUser(User user) {
if (user == null || user.getId() == null) {
throw new IllegalArgumentException("User or user ID cannot be null.");
}
// 1. 先更新數(shù)據(jù)庫
userMapper.updateById(user);
System.out.println("Updated user in DB: " + user.getId());
// 2. 再刪除緩存
String key = CACHE_KEY_PREFIX + user.getId();
redisTemplate.delete(key);
System.out.println("Invalidated cache for user: " + user.getId());
}
}
3. 優(yōu)缺點(diǎn)與適用場(chǎng)景
優(yōu)點(diǎn):
- 邏輯簡(jiǎn)單,易于實(shí)現(xiàn)和理解。
- 強(qiáng)一致性(在大多數(shù)場(chǎng)景下),因?yàn)閷懖僮髦苯硬僮鲾?shù)據(jù)庫,讀操作在緩存失效后會(huì)從數(shù)據(jù)庫加載最新數(shù)據(jù)。
- 靈活性高,緩存和數(shù)據(jù)庫的交互完全由應(yīng)用層控制。
缺點(diǎn):
- 代碼耦合,業(yè)務(wù)代碼中混入了大量緩存操作邏輯,不夠優(yōu)雅。
- 首次讀取延遲,對(duì)于冷數(shù)據(jù)(首次被訪問的數(shù)據(jù)),會(huì)經(jīng)歷一次“緩存未命中 -> 讀數(shù)據(jù)庫 -> 寫緩存”的完整過程,延遲較高。
- 可能存在一致性問題:在“更新DB”和“刪除緩存”這兩個(gè)非原子操作之間,如果發(fā)生異?;蚋卟l(fā)讀寫,可能導(dǎo)致緩存中的數(shù)據(jù)是舊的,而數(shù)據(jù)庫是新的。這被稱為“緩存-數(shù)據(jù)庫雙寫不一致”,但通過“先更新DB,再刪除緩存”已將風(fēng)險(xiǎn)降到最低。
適用場(chǎng)景:
- 絕大多數(shù)的讀多寫少的業(yè)務(wù)場(chǎng)景。
- 對(duì)數(shù)據(jù)一致性有較高要求,但能容忍極短暫不一致的場(chǎng)景。
- 這是大部分互聯(lián)網(wǎng)應(yīng)用的首選和默認(rèn)策略。
4. 常見陷阱與注意事項(xiàng)
- 緩存穿透:查詢一個(gè)數(shù)據(jù)庫和緩存中都不存在的數(shù)據(jù)。這會(huì)導(dǎo)致每次請(qǐng)求都直接打到數(shù)據(jù)庫,緩存形同虛設(shè)。
- 解決方案:對(duì)查詢結(jié)果為
null的數(shù)據(jù)也進(jìn)行緩存(緩存空對(duì)象),但設(shè)置一個(gè)較短的過期時(shí)間。
- 解決方案:對(duì)查詢結(jié)果為
- 緩存擊穿:某個(gè)熱點(diǎn)Key在緩存中過期失效的瞬間,大量并發(fā)請(qǐng)求同時(shí)涌入,直接打到數(shù)據(jù)庫上。
- 解決方案:使用互斥鎖(如分布式鎖),只允許一個(gè)線程去查詢數(shù)據(jù)庫并回寫緩存,其他線程等待。
- 緩存雪崩:大量的Key在同一時(shí)間集體過期,導(dǎo)致所有請(qǐng)求瞬間全部打到數(shù)據(jù)庫。
- 解決方案:在Key的過期時(shí)間上增加一個(gè)隨機(jī)值,避免集體失效。
策略二:Read/Write-Through (讀穿/寫穿)
這種策略將緩存作為主要的數(shù)據(jù)存儲(chǔ)。應(yīng)用程序只與緩存交互,由緩存服務(wù)自身來負(fù)責(zé)與底層數(shù)據(jù)庫的同步。
1. 概念與工作流程
Read-Through (讀穿):
- 應(yīng)用程序向緩存請(qǐng)求數(shù)據(jù)。
- 如果緩存命中,直接返回。
- 如果緩存未命中,由緩存服務(wù)自己負(fù)責(zé)從數(shù)據(jù)庫加載數(shù)據(jù)。
- 緩存服務(wù)將數(shù)據(jù)加載到緩存中,并返回給應(yīng)用程序。
- 這個(gè)過程對(duì)應(yīng)用程序是透明的。
Write-Through (寫穿):
- 應(yīng)用程序向緩存寫入數(shù)據(jù)。
- 緩存服務(wù)首先更新緩存。
- 然后緩存服務(wù)同步地將數(shù)據(jù)寫入數(shù)據(jù)庫。
- 操作完成后,緩存服務(wù)向應(yīng)用程序返回成功。
- 這個(gè)過程保證了緩存和數(shù)據(jù)庫的強(qiáng)一致性。
關(guān)鍵區(qū)別:Cache-Aside是應(yīng)用層維護(hù),Read/Write-Through是緩存服務(wù)(或一個(gè)封裝層)維護(hù)。
2. 代碼示例 (使用 Spring Cache 注解)
Spring Cache 的 @Cacheable, @CachePut, @CacheEvict 注解是 Read/Write-Through 和 Cache-Aside 寫策略思想的完美體現(xiàn)。它將緩存邏輯從業(yè)務(wù)代碼中解耦,使得代碼更簡(jiǎn)潔。
配置 (CacheConfig.java):
import org.springframework.cache.annotation.EnableCaching;
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.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60)) // 默認(rèn)緩存60分鐘
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // 不緩存null值
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
重構(gòu)后的 Service (UserServiceWithCacheAnnotations.java):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImplWithAnnotations {
@Autowired
private UserMapper userMapper;
/**
* @Cacheable 實(shí)現(xiàn)了 Read-Through 思想
* - `value` 或 `cacheNames`: 指定緩存的名稱(命名空間)
* - `key`: 緩存的key,這里使用SpEL表達(dá)式取方法參數(shù)id
* - `unless`: 結(jié)果為null時(shí)不緩存,防止緩存穿透
*/
@Cacheable(cacheNames = "user", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
System.out.println("Reading from DB for user: " + id);
return userMapper.selectById(id);
}
/**
* @CacheEvict 實(shí)現(xiàn)了 Cache-Aside 的寫策略(刪除緩存)
* - `key`: 指定要?jiǎng)h除的緩存key
*/
@CacheEvict(cacheNames = "user", key = "#user.id")
public void updateUser(User user) {
System.out.println("Updating user in DB: " + user.getId());
userMapper.updateById(user);
System.out.println("Cache evicted for user: " + user.getId());
}
// 如果需要Write-Through(每次都更新緩存),可以使用@CachePut
// @CachePut(cacheNames = "user", key = "#user.id")
// public User updateUserAndCache(User user) {
// userMapper.updateById(user);
// return user; // @CachePut 要求方法必須有返回值,返回值會(huì)被放入緩存
// }
}
3. 優(yōu)缺點(diǎn)與適用場(chǎng)景
優(yōu)點(diǎn):
- 代碼簡(jiǎn)潔,業(yè)務(wù)邏輯與緩存邏輯分離,可維護(hù)性高。
- 強(qiáng)一致性(對(duì)于Write-Through),因?yàn)閷懖僮魇窃拥模◤膽?yīng)用角度看)。
- 對(duì)應(yīng)用透明,開發(fā)者無需關(guān)心底層細(xì)節(jié)。
缺點(diǎn):
- 靈活性較低,緩存的讀寫行為由框架或緩存服務(wù)固定,不易定制。
- 寫操作延遲增加(對(duì)于Write-Through),因?yàn)樾枰綄懭霐?shù)據(jù)庫。
適用場(chǎng)景:
- 對(duì)代碼整潔度要求高的項(xiàng)目。
- 需要強(qiáng)一致性且能接受寫操作延遲的場(chǎng)景。
- 在Java生態(tài)中,使用Spring Cache進(jìn)行常規(guī)業(yè)務(wù)對(duì)象緩存是此模式的最佳實(shí)踐。
策略三:Write-Back (寫回)
這是一種以性能為先的策略,追求極致的寫性能,但犧牲了一定的數(shù)據(jù)一致性和可靠性。
1. 概念與工作流程
寫操作流程:
- 應(yīng)用程序?qū)?shù)據(jù)只寫入緩存,并立即返回。
- 緩存服務(wù)將此數(shù)據(jù)標(biāo)記為“臟數(shù)據(jù)”(Dirty)。
- 一個(gè)獨(dú)立的異步任務(wù)會(huì)批量地、或延遲地將這些“臟數(shù)據(jù)”刷回(flush)到數(shù)據(jù)庫中。
讀操作流程:
- 與 Read-Through 類似。如果緩存命中(無論是干凈數(shù)據(jù)還是臟數(shù)據(jù)),直接返回。如果未命中,從數(shù)據(jù)庫加載。
2. 代碼示例(概念性實(shí)現(xiàn))
原生 Redis 和 Spring Boot 不直接提供 Write-Back 機(jī)制,需要自己實(shí)現(xiàn)或借助第三方框架。下面是一個(gè)簡(jiǎn)化的概念性實(shí)現(xiàn),用 BlockingQueue 和 ExecutorService 模擬異步寫回。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
@Service
public class UserWriteBackService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_KEY_PREFIX = "user:";
// 使用阻塞隊(duì)列作為緩沖區(qū)
private final BlockingQueue<User> dirtyQueue = new LinkedBlockingQueue<>(10000);
// 使用單線程的Executor來順序處理寫回任務(wù)
private final ExecutorService writerExecutor = Executors.newSingleThreadExecutor();
// 初始化時(shí)啟動(dòng)異步寫回任務(wù)
@PostConstruct
public void init() {
writerExecutor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 每隔5秒或緩沖區(qū)達(dá)到100條時(shí),批量寫回?cái)?shù)據(jù)庫
List<User> userBatch = new ArrayList<>();
// 從隊(duì)列中取出最多100個(gè)元素,最多等待5秒
Queues.drain(dirtyQueue, userBatch, 100, 5, TimeUnit.SECONDS);
if (!userBatch.isEmpty()) {
System.out.println("Writing back batch of size: " + userBatch.size());
// 在實(shí)際應(yīng)用中,這里應(yīng)該是批量更新操作
for (User user : userBatch) {
userMapper.updateById(user);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢復(fù)中斷狀態(tài)
System.err.println("Write-back thread interrupted.");
} catch (Exception e) {
// 必須處理異常,否則線程可能終止
System.err.println("Error during write-back: " + e.getMessage());
}
}
});
}
// 更新操作:只寫緩存,并放入臟數(shù)據(jù)隊(duì)列
public void updateUser(User user) {
// 1. 更新緩存
redisTemplate.opsForValue().set(CACHE_KEY_PREFIX + user.getId(), user);
// 2. 放入異步寫回隊(duì)列
// 注意:為避免重復(fù)放入,可以先從隊(duì)列中移除舊的相同ID的項(xiàng)
dirtyQueue.removeIf(u -> u.getId().equals(user.getId()));
boolean offered = dirtyQueue.offer(user);
if(!offered){
System.err.println("Write-back queue is full. Data for user " + user.getId() + " might be lost!");
// 可以在此添加降級(jí)策略,例如同步寫入
}
}
public User getUserById(Long id) {
// 讀操作邏輯與Cache-Aside或Read-Through類似
Object user = redisTemplate.opsForValue().get(CACHE_KEY_PREFIX + id);
if (user != null) {
return (User) user;
}
return userMapper.selectById(id); // 此處簡(jiǎn)化,未回寫緩存
}
// 關(guān)閉服務(wù)時(shí),確保緩沖區(qū)數(shù)據(jù)被處理
@PreDestroy
public void shutdown() {
writerExecutor.shutdown();
try {
if (!writerExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
writerExecutor.shutdownNow();
}
} catch (InterruptedException e) {
writerExecutor.shutdownNow();
}
// 處理隊(duì)列中剩余的數(shù)據(jù)...
}
}
3. 優(yōu)缺點(diǎn)與適用場(chǎng)景
優(yōu)點(diǎn):
- 極高的寫性能,因?yàn)閼?yīng)用“寫”操作的耗時(shí)僅僅是寫入內(nèi)存(Redis)的時(shí)間,響應(yīng)極快。
- 降低數(shù)據(jù)庫壓力,通過批量異步寫入,大大減少了對(duì)數(shù)據(jù)庫的寫請(qǐng)求次數(shù)。
缺點(diǎn):
- 數(shù)據(jù)丟失風(fēng)險(xiǎn):如果 Redis 服務(wù)宕機(jī),且緩沖區(qū)中的“臟數(shù)據(jù)”還未寫回?cái)?shù)據(jù)庫,這部分?jǐn)?shù)據(jù)將永久丟失。
- 數(shù)據(jù)一致性差:是“最終一致性”,在數(shù)據(jù)寫回?cái)?shù)據(jù)庫之前,緩存和數(shù)據(jù)庫的數(shù)據(jù)是不同的。
- 實(shí)現(xiàn)復(fù)雜度高:需要自己實(shí)現(xiàn)異步隊(duì)列、批量寫入、失敗重試、服務(wù)關(guān)閉時(shí)的數(shù)據(jù)處理等機(jī)制,非常復(fù)雜。
適用場(chǎng)景:
- 寫密集型應(yīng)用,例如:高頻次的用戶行為記錄、點(diǎn)贊數(shù)、文章瀏覽量計(jì)數(shù)等。
- 對(duì)數(shù)據(jù)丟失有一定容忍度的業(yè)務(wù)。比如,丟失幾秒內(nèi)的點(diǎn)贊數(shù)或?yàn)g覽量通常是可以接受的。
- 絕對(duì)不能用于金融、交易等對(duì)數(shù)據(jù)可靠性和一致性要求極高的場(chǎng)景。
總結(jié)與策略選擇
| 特性 | Cache-Aside (旁路緩存) | Read/Write-Through (讀寫穿) | Write-Back (寫回) |
|---|---|---|---|
| 實(shí)現(xiàn)復(fù)雜度 | 中等 (業(yè)務(wù)代碼侵入) | 低 (框架支持,如Spring Cache) | 高 (需自行實(shí)現(xiàn)異步邏輯) |
| 數(shù)據(jù)一致性 | 準(zhǔn)實(shí)時(shí)一致性 | 強(qiáng)一致性 (Write-Through) | 最終一致性 |
| 數(shù)據(jù)可靠性 | 高 | 最高 | 低 (有數(shù)據(jù)丟失風(fēng)險(xiǎn)) |
| 讀性能 | 高 (命中時(shí)) | 高 (命中時(shí)) | 高 (命中時(shí)) |
| 寫性能 | 中等 (DB + Cache) | 慢 (同步寫DB+Cache) | 極高 (只寫內(nèi)存) |
| 適用場(chǎng)景 | 通用,讀多寫少,互聯(lián)網(wǎng)首選 | 代碼簡(jiǎn)潔性要求高,通用業(yè)務(wù) | 寫密集型,對(duì)性能要求極致,能容忍數(shù)據(jù)丟失 |
進(jìn)階建議與最佳實(shí)踐:
- 從 Cache-Aside 開始:對(duì)于絕大多數(shù)項(xiàng)目,Cache-Aside 是最穩(wěn)妥、最靈活的起點(diǎn)。
- 擁抱 Spring Cache:在 Spring 生態(tài)中,優(yōu)先使用
@Cacheable、@CacheEvict等注解來實(shí)踐 Read-Through 和 Cache-Aside 的思想,能極大簡(jiǎn)化代碼,提高開發(fā)效率。 - 謹(jǐn)慎使用 Write-Back:只有在寫性能成為明確瓶頸,且業(yè)務(wù)能容忍其數(shù)據(jù)丟失風(fēng)險(xiǎn)時(shí),才考慮自行實(shí)現(xiàn)或引入支持 Write-Back 的緩存組件。
- 一致性是關(guān)鍵挑戰(zhàn):深入理解“先更新DB,再刪除緩存”策略,并了解其在極端并發(fā)下的風(fēng)險(xiǎn)。對(duì)于要求更強(qiáng)一致性的場(chǎng)景,可以研究基于消息隊(duì)列(如Canal+RocketMQ/Kafka)的**訂閱數(shù)據(jù)庫變更日志(Binlog)**來異步更新緩存的方案,這是目前業(yè)界解決該問題的主流高級(jí)方案。
- 監(jiān)控不可或缺:無論使用哪種策略,都必須對(duì)緩存的命中率、內(nèi)存使用率、響應(yīng)時(shí)間等關(guān)鍵指標(biāo)進(jìn)行全面監(jiān)控,這是優(yōu)化和排查問題的基礎(chǔ)。
到此這篇關(guān)于淺談Redis三種高效緩存讀寫策略的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis 緩存讀寫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis?RESP?協(xié)議實(shí)現(xiàn)實(shí)例詳解
這篇文章主要為大家介紹了Redis?RESP?協(xié)議實(shí)現(xiàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Redisson實(shí)現(xiàn)Redis分布式鎖的幾種方式
本文在講解如何使用Redisson實(shí)現(xiàn)Redis普通分布式鎖,以及Redlock算法分布式鎖的幾種方式的同時(shí),也附帶解答這些同學(xué)的一些疑問,感興趣的可以了解一下2021-08-08
redis replication環(huán)形緩沖區(qū)算法詳解
這篇文章主要介紹了redis replication環(huán)形緩沖區(qū)算法的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
使用Redis實(shí)現(xiàn)點(diǎn)贊取消點(diǎn)贊的詳細(xì)代碼
這篇文章主要介紹了Redis實(shí)現(xiàn)點(diǎn)贊取消點(diǎn)贊的詳細(xì)代碼,通過查詢某實(shí)體(帖子、評(píng)論等)點(diǎn)贊數(shù)量,需要用到事務(wù)相關(guān)知識(shí),結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03

