從你提供的文本來看,核心是 Redis 作為緩存的檢查機制,以及非原子性操作導致的不一致性問題。
我們可以拆解為兩個部分來理解:
📌 1. 邏輯:先查 Redis,再決定是否注冊
邏輯流程
- 先查詢 Redis 是否有某個 key(可能是用戶注冊標識、驗證碼等)。
- 如果 Redis 中有數據,則直接返回(避免重復操作)。
- 如果 Redis 中沒有數據,則執行新的注冊,并存入 Redis,設置60秒過期時間。
示例代碼
String key = "user:register:" + phoneNumber;
String cachedValue = redisTemplate.opsForValue().get(key);if (cachedValue == null) { // Redis 無數據,執行注冊邏輯// 執行新用戶注冊userService.register(phoneNumber);// 將 key 存入 Redis,防止重復注冊redisTemplate.opsForValue().set(key, "registered", 60, TimeUnit.SECONDS);
} else {System.out.println("該用戶已注冊");
}
? 作用
- 通過 Redis 緩存查詢結果,降低數據庫查詢次數,提高性能。
- 避免并發重復注冊。
📌 2. 非原子性操作,導致數據不一致
這一部分說的是 多個 Redis 操作不是原子的,可能會導致數據不一致問題,尤其是在并發情況下。
? 問題:非原子性操作
Redis
并不自動保證多個操作的原子性。如果有多個線程同時執行:
- A 線程查詢 Redis,發現 key 不存在(緩存失效)。
- B 線程也查詢 Redis,發現 key 也不存在(同樣緩存失效)。
- A 和 B 線程都開始執行注冊邏輯(導致重復注冊)。
- A 和 B 線程都將數據寫入 Redis,但只有一個生效,另一個可能會被覆蓋。
🛠 解決方案
-
使用
SETNX
實現分布式鎖SETNX
(SET if Not eXists)可以確保只有一個線程可以寫入 key,避免并發問題:
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "registered", 60, TimeUnit.SECONDS); if (Boolean.TRUE.equals(success)) {userService.register(phoneNumber); } else {System.out.println("該用戶已注冊"); }
作用:
- 只有第一個線程能成功寫入
key
,其余線程會失敗,避免重復注冊。
-
使用 Redis 事務 (
MULTI / EXEC
)MULTI
+EXEC
確保多個 Redis 操作在同一事務中執行:
redisTemplate.execute((RedisCallback<Object>) connection -> {connection.multi();connection.set(key.getBytes(), "registered".getBytes());connection.expire(key.getBytes(), 60);return connection.exec(); });
作用:
- 確保寫入 key 和設置過期時間是原子的,不會被中途修改。
-
使用
Redisson
分布式鎖- 適用于高并發場景,保證多個服務器之間不會重復注冊:
RLock lock = redissonClient.getLock("register-lock:" + phoneNumber); try {if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { userService.register(phoneNumber);} } finally {lock.unlock(); }
作用:
- 只有一個線程能拿到鎖,執行注冊,其他線程等待或直接返回。
🔍 結論
- 先查 Redis,若不存在則執行注冊并緩存數據,提升性能,減少數據庫壓力。
- 但多個 Redis 操作非原子性,可能會導致數據不一致(如并發下重復注冊)。
- 解決方案
- SETNX(互斥鎖):確保只有一個線程能注冊。
- Redis 事務:保證多個 Redis 操作同時執行。
- Redisson 分布式鎖:在多服務器環境下防止并發問題。
? 最終目標:確保并發情況下數據一致性,避免重復注冊或異常數據覆蓋。 🚀