使用Redis實現(xiàn)分布式鎖與緩存策略方式
一、引言
在分布式系統(tǒng)中,多個進程或服務常常需要訪問共享資源,為了保證數(shù)據(jù)的一致性和系統(tǒng)的穩(wěn)定性,需要引入分布式鎖機制。同時,為了提高系統(tǒng)的性能和響應速度,緩存策略也是必不可少的。
Redis憑借其原子性操作、內(nèi)存存儲、過期機制和分布式特性,成為實現(xiàn)分布式鎖和緩存策略的理想選擇。
二、Redis實現(xiàn)分布式鎖
(一)分布式鎖的意義
在單服務環(huán)境下,使用synchronized關鍵字可以保證線程安全,但在分布式系統(tǒng)中,多個節(jié)點訪問同一個公共資源時,synchronized就無法發(fā)揮作用。
分布式鎖能夠確保在任意時刻,只有一個客戶端能持有鎖,防止多個客戶端同時對共享資源進行操作,從而保證數(shù)據(jù)的一致性。
(二)Redis實現(xiàn)分布式鎖的常見方案
SETNX + EXPIRE方案
原理:
SETNX是SET IF NOT EXISTS的簡寫,即當指定的鍵不存在時,為其設置值。- 先使用
SETNX命令嘗試獲取鎖,如果返回1,表示獲取成功,再使用EXPIRE命令為鎖設置一個過期時間,防止鎖忘記釋放導致死鎖。
代碼示例(Java):
import redis.clients.jedis.Jedis;
public class RedisLockExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String key = "resource_lock";
String value = "lock_value";
int expireTime = 100; // 過期時間,單位秒
if (jedis.setnx(key, value) == 1) {
jedis.expire(key, expireTime);
try {
// 業(yè)務代碼
System.out.println("獲取鎖成功,執(zhí)行業(yè)務邏輯");
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.del(key);
System.out.println("釋放鎖");
}
} else {
System.out.println("獲取鎖失敗");
}
jedis.close();
}
}
- **缺點**:`SETNX`和`EXPIRE`兩個命令不是原子操作,如果在執(zhí)行完`SETNX`后,進程崩潰或重啟,`EXPIRE`命令未執(zhí)行,鎖將無法釋放,導致其他客戶端永遠無法獲取鎖。
SETNX + value(系統(tǒng)時間 + 過期時間)方案
原理:
- 把過期時間放在
SETNX的value值里。如果加鎖失敗,拿出value值校驗是否過期。 - 加鎖成功時,將系統(tǒng)時間加上設置的過期時間作為
value存入Redis。 - 如果鎖已存在,獲取鎖的過期時間,若過期時間小于系統(tǒng)當前時間,表示鎖已過期,通過
getSet命令嘗試獲取鎖。
代碼示例(Java):
import redis.clients.jedis.Jedis;
public class RedisLockWithTimeExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String key = "resource_lock";
long expireTime = 10000; // 過期時間,單位毫秒
long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);
if (jedis.setnx(key, expiresStr) == 1) {
System.out.println("獲取鎖成功");
} else {
String currentValueStr = jedis.get(key);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
String oldValueStr = jedis.getSet(key, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
System.out.println("獲取鎖成功");
} else {
System.out.println("獲取鎖失敗,其他線程已更新鎖");
}
} else {
System.out.println("獲取鎖失敗,鎖未過期");
}
}
jedis.close();
}
}
- **缺點**:過期時間是客戶端自己生成的,依賴系統(tǒng)時間,要求分布式環(huán)境下每個客戶端的時間必須同步。鎖過期時,并發(fā)多個客戶端同時請求,都執(zhí)行`getSet`,最終只有一個客戶端加鎖成功,但該客戶端鎖的過期時間可能被別的客戶端覆蓋。且該鎖沒有保存持有者的唯一標識,可能被別的客戶端釋放。
使用Lua腳本方案
原理:
- Lua腳本可以將一組Redis命令放在一次請求里完成,Redis會將腳本作為一個整體執(zhí)行,保證了原子性。
- 通過Lua腳本實現(xiàn)
SETNX和EXPIRE兩條指令的原子操作。
代碼示例(Java):
import redis.clients.jedis.Jedis;
import java.util.Collections;
public class RedisLockWithLuaExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String key = "resource_lock";
String value = "lock_value";
int expireTime = 100; // 過期時間,單位秒
String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
"redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
"return 0 " +
"end";
Object result = jedis.eval(luaScript, Collections.singletonList(key),
Collections.singletonList(value + "," + expireTime));
if (result.equals(1L)) {
System.out.println("獲取鎖成功");
try {
// 業(yè)務代碼
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.del(key);
System.out.println("釋放鎖");
}
} else {
System.out.println("獲取鎖失敗");
}
jedis.close();
}
}
SET的擴展命令(SET EX PX NX)方案
原理:
- Redis的
SET指令有擴展參數(shù)[EX seconds][PX milliseconds][NX|XX],其中EX表示設置鍵的過期時間,單位為秒;PX表示設置鍵的過期時間,單位為毫秒;NX表示只有鍵不存在時才能設置成功。 - 使用該命令可以原子性地完成設置鍵值和過期時間的操作。
代碼示例(Java):
import redis.clients.jedis.Jedis;
public class RedisSetLockExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String key = "resource_lock";
String value = "lock_value";
int expireTime = 100; // 過期時間,單位秒
String result = jedis.set(key, value, "NX", "EX", expireTime);
if ("OK".equals(result)) {
System.out.println("獲取鎖成功");
try {
// 業(yè)務代碼
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.del(key);
System.out.println("釋放鎖");
}
} else {
System.out.println("獲取鎖失敗");
}
jedis.close();
}
}
開源框架Redisson方案
原理:
- Redisson是一個在Redis的基礎上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In - Memory Data Grid)框架,它提供了多種分布式鎖的實現(xiàn)。
- 當一個線程獲得鎖后,會開啟一個定時守護線程,每隔一段時間檢查鎖是否還存在,若存在則延長鎖的過期時間,防止鎖過期提前釋放。
代碼示例(Java):
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonLockExample {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("resource_lock");
try {
boolean isLocked = lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
System.out.println("獲取鎖成功");
// 業(yè)務代碼
} else {
System.out.println("獲取鎖失敗");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
redissonClient.shutdown();
}
}
}
三、Redis緩存策略
(一)旁路緩存(Cache - Aside)策略
工作原理:
- 由應用層負責緩存和數(shù)據(jù)庫的交互邏輯。
- 讀取數(shù)據(jù)時,先查詢緩存,命中則直接返回;未命中則查詢數(shù)據(jù)庫,將結果寫入緩存并返回。
- 更新數(shù)據(jù)時,先更新數(shù)據(jù)庫,再刪除緩存(或更新緩存)。
代碼示例(Java):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class UserServiceCacheAside {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserRepository userRepository;
private static final String CACHE_KEY_PREFIX = "user:";
private static final long CACHE_EXPIRATION = 30; // 緩存過期時間(分鐘)
public User getUserById(Long userId) {
String cacheKey = CACHE_KEY_PREFIX + userId;
// 1. 查詢緩存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
// 2. 緩存命中,直接返回
if (user != null) {
return user;
}
// 3. 緩存未命中,查詢數(shù)據(jù)庫
user = userRepository.findById(userId).orElse(null);
// 4. 將數(shù)據(jù)庫結果寫入緩存(設置過期時間)
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, CACHE_EXPIRATION, java.util.concurrent.TimeUnit.MINUTES);
}
return user;
}
public void updateUser(User user) {
// 1. 先更新數(shù)據(jù)庫
userRepository.save(user);
// 2. 再刪除緩存
String cacheKey = CACHE_KEY_PREFIX + user.getId();
redisTemplate.delete(cacheKey);
}
}
優(yōu)缺點分析
- 優(yōu)點:實現(xiàn)簡單,控制靈活;適合讀多寫少的業(yè)務場景;只緩存必要的數(shù)據(jù),節(jié)省內(nèi)存空間。
- 缺點:首次訪問會有一定延遲(緩存未命中);存在并發(fā)問題,如果先刪除緩存后更新數(shù)據(jù)庫,可能導致數(shù)據(jù)不一致;需要應用代碼維護緩存一致性,增加了開發(fā)復雜度。
適用場景:
- 讀多寫少的業(yè)務場景;對數(shù)據(jù)一致性要求不是特別高的應用;分布式系統(tǒng)中需要靈活控制緩存策略的場景。
(二)緩存穿透解決方案
- 緩存空值:當查詢的數(shù)據(jù)在數(shù)據(jù)庫中不存在時,也在Redis中存入一個空值,并設置一個較短的過期時間。這樣下次再查詢該數(shù)據(jù)時,直接從緩存中返回空值,避免訪問數(shù)據(jù)庫。
- 布隆過濾器:布隆過濾器是一種概率型數(shù)據(jù)結構,用于快速判斷一個元素是否存在于集合中。在訪問緩存前,先通過布隆過濾器判斷數(shù)據(jù)是否存在,若不存在則直接返回,避免訪問緩存和數(shù)據(jù)庫。
(三)緩存雪崩解決方案
- 設置不同的過期時間:為不同的緩存數(shù)據(jù)設置不同的過期時間,或者在過期時間上加上一個隨機數(shù),避免大量緩存數(shù)據(jù)同時過期。
- 部署高可用的Redis集群:通過主從節(jié)點的方式構建Redis高可靠集群,如果主節(jié)點故障,從節(jié)點可以切換為主節(jié)點繼續(xù)提供服務,同時完善監(jiān)控報警體系。
(四)緩存擊穿解決方案
- 互斥鎖:當緩存失效時,通過互斥鎖保證同一時間只有一個請求去構建緩存,其他請求等待鎖釋放后再讀取緩存。
- 邏輯過期:將緩存數(shù)據(jù)的過期時間存儲在緩存中,當緩存過期時,不立即刪除緩存,而是啟動一個后臺線程異步更新緩存。在讀取緩存時,判斷緩存是否過期,若過期則返回舊數(shù)據(jù),并異步更新緩存。
四、總結
Redis在實現(xiàn)分布式鎖和緩存策略方面具有顯著的優(yōu)勢。通過多種分布式鎖實現(xiàn)方案,可以滿足不同場景下對鎖的要求,保證共享資源的原子性訪問。同時,合理的緩存策略能夠提高系統(tǒng)的性能和響應速度,解決緩存穿透、雪崩和擊穿等問題。在實際應用中,應根據(jù)具體的業(yè)務需求和系統(tǒng)特點,選擇合適的分布式鎖實現(xiàn)方案和緩存策略,以構建高效、穩(wěn)定的分布式系統(tǒng)。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Redis定時監(jiān)控與數(shù)據(jù)處理的實踐指南
在現(xiàn)代分布式系統(tǒng)中,Redis作為高性能的內(nèi)存數(shù)據(jù)庫,常用于緩存、消息隊列和實時數(shù)據(jù)處理,合理使用Redis數(shù)據(jù)結構,可以極大提升系統(tǒng)性能,本文將通過一個實際案例,介紹如何將Redis存儲結構從 Set 遷移到Hash,并實現(xiàn)定時任務監(jiān)控數(shù)據(jù)變化,需要的朋友可以參考下2025-06-06
Spring?Boot?整合Redis?實現(xiàn)優(yōu)惠卷秒殺?一人一單功能
這篇文章主要介紹了Spring?Boot?整合Redis?實現(xiàn)優(yōu)惠卷秒殺?一人一單,在分布式系統(tǒng)下,高并發(fā)的場景下,會出現(xiàn)此類庫存超賣問題,本篇文章介紹了采用樂觀鎖來解決,需要的朋友可以參考下2022-09-09
Redis全文搜索教程之創(chuàng)建索引并關聯(lián)源數(shù)據(jù)的教程
RediSearch提供了一種簡單快速的方法對 hash 或者 json 類型數(shù)據(jù)的任何字段建立二級索引,然后就可以對被索引的 hash 或者 json 類型數(shù)據(jù)字段進行搜索和聚合操作,這篇文章主要介紹了Redis全文搜索教程之創(chuàng)建索引并關聯(lián)源數(shù)據(jù),需要的朋友可以參考下2023-12-12

