在併發量比較高的情況下redis有很多應用場景,提升查詢效率,緩解底層DBio ,下面列舉兩個平時開發中應用過的兩個例子,歡迎各位一起討論改進。
1 . redis 驚羣處理
1.1 方案的由來
Redis的緩存數據庫是爲快速響應客戶端減輕數據庫壓力的有效手段之一,其中有一種功能是失效緩存,其優點是可以不定期的釋放使用頻率低的業務空間而增加有限的內存,但對於同步數據庫和緩存之間的數據來說需要面臨一個問題就是:在併發量比較大的情況下當一個緩存數據失效之後會導致同時有多個併發線程去向後端數據庫發起請求去獲取同一業務數據,這樣如果在一段時間內同時生成了大量的緩存,然後在另外一段時間內又有大量的緩存失效,這樣就會導致後端數據庫的壓力陡增,這種現象就可以稱爲“緩存過期產生的驚羣現象”!
1.2 處理邏輯
緩存內真實失效時間time1
緩存value中存放人爲失效時間戳 :time2 ( time2 永遠小於time1)
緩存value對應的lock鎖(就是一個與value 對應的 另一個key),主要用於判斷是第幾個線程來讀取redis的value
當把數據庫的數據寫入緩存後,這時有客戶端第一次來讀取緩存,取當前系統時間:system_time 如果system_time >= time2 則認爲默認緩存已過期(如果system_time< time1 則還沒真實失效 ),這時再獲取value的lock鎖,調用redis的incr函數(單線程自增函數)判斷是第幾個獲取鎖的線程,當且僅當是第一個線程時返回1,以後都逐漸遞增。第一個訪問的線程到數據庫中獲取最新值重新放入緩存並刪除lock鎖的key,並重新設置時間戳;在刪除lock之前所有訪問value客戶端線程獲取lock的value都大於1,這些線程仍然讀取redis中的舊值,而不會集中訪問數據庫。
1.3 僞代碼
private long expirt_time = 1000 * 40 ;//人爲過期時間
private long time = 1000 * 60;//一分鐘
private long second = 60 * 6;//六分鐘
KooJedisClient client =SpringContextUtils.getBean("redisClient", KooJedisClient.class);
private final String user_key ="USER_REDIS";
private final String user_key_lock ="USER_REDIS_lock";
public void setExpireTime( HttpServletRequestrequest ){
StringuserId = request.getParameter( "userId");
//數組裏存放:1:真實value ,2:過期時間
Stringkey = org.apache.commons.lang3.StringUtils.join(new Object[]{user_key,userId});
String[]info = client.get( key , String[].class);
longnowTime = System.currentTimeMillis();
if( null!= info ){
longexpireRealTime = new Long( info[1] );
//如果已過期並且是第一個訪問的線程
if(nowTime >= expireRealTime ){
Long lockNum = client.incr( user_key_lock+userId ); // 可以實現原子性的遞增,可應用於高併發的秒殺活動、分佈式序列號生成等場景
if( ( lockNum == 1 || lockNum ==null )){
//重新從數據庫獲取
User user = teacherDataMaintain.findUserInfo(new Integer(userId));
info[ 0 ] = user.getUserName();
info[ 1 ] =org.apache.commons.lang3.StringUtils.join(new Object[]{(nowTime + expirt_time),""});
client.setex( key ,60, info );//六分後過期
client.del( user_key_lock+userId );
}else{
System.out.println( "緩存過期但不是第一個線程,返回舊值" );
}
}else{
//返回緩存中舊值
System.out.println( "緩存未過期" );
}
}else{
Useruser = teacherDataMaintain.findUserInfo(new Integer(userId));
String[] userInfo = { user.getUserName() ,(nowTime + expirt_time ) +"" };
client.setex( key ,60, userInfo );// 過期
}
}
2.redis 分佈式鎖應用
2.1 分佈式鎖主要是解決分佈式環境對共享資源的同步訪問
在但進程的環境下程序上完全可以用synchronized同步鎖來限制多線程對共享資源的訪問,但在分佈式環境下同步鎖無法控制不同進程之間的線程,這種情況下就需要找一種單進程可串行處理的“鎖”,redis 就是其中的一種選擇,
2.2. 應用場景:
場景1: A 系統於B系統均是分佈式部署單臺服務器多實例,採用SOA接口方式通信,兩個系統需要對共享信息進行實時同步。
1):比如A系統的訂單信息需要共享給B,同時B系統會在系統中再保留一個副本,A系統設計到任何關於訂單的信息都需要同步給B系統
2):B 接收到A的信息變更後發送至rabbitMQ
3):B負責消費訂單信息或變更請求同時保存至數據庫。(因爲是分佈式部署,所有存在多個實例消費一個消息的可能)
4):技術的關鍵點在當B的多臺實例同時消費任務時有可能產生多個任務,但是數據庫裏只允許保存一條記錄。當RabbitMq發生阻塞時會造成消費不及時,等RabbitMq回覆後也可能存在多個B的server消費一個消息而對數據庫產生多個請求。
最終會導致:
1).數據庫事務瞬間處理過多,可能造成死鎖,
2).隊列中的對象信息有可能是有序的,可能會出現狀態的相互覆蓋。
例如:秒殺
Redis 緩存中與數據庫的庫存在秒殺前是一樣的,當秒殺開始的時候,同一時間點會有很多客戶端訪問緩存和數據庫,不同的進程同時訪問緩存或者數據庫,當緩存中的數據變化後並且沒被修改之前有可能又被另一個線程獲取,數據有可能出現髒讀和數據被覆蓋的可能。(髒讀 < 不可重複讀 < 幻讀)
2.3 解決思路:
對共享資源的操作要是互斥且良性的競爭,即在分佈式條件下怎樣做到一次只能有一個線程來處理共享資源,且線程之間不會出現死鎖的情況。
2.3.1 幾個基本的函數:
Setnx key value :如果沒有key 則可以獲得鎖並返回1 ,如果已存在key 則不做操作並返回0 。
Getset key value :設置value ,並返回key的舊值,若key不存在則返回null
Get key :
2.3.2 死鎖
Setnx是單線程處理的,但仍有可能出現死鎖
Eg:thread0 操作超時了,但它還持有着鎖,thread 1和thread 2讀取lock.foo檢查時間戳,然後發現超時了;
thread 1 發送DEL lock.foo;
thread 1 發送SETNX lock.foo並且成功了;
thread 2 發送DEL lock.foo;
thread 2 發送SETNX lock.foo並且成功了。
這樣一來,thread 1、thread 2都拿到了鎖!鎖安全性被破壞了!
2.3.3 解決死鎖
- thread 3 發送SETNX lock.foo 想要獲得鎖,由於thread 0 還持有鎖,所以Redis返回給thread 3 一個0;
- thread 3 發送GET lock.foo 以檢查鎖是否超時了,如果沒超時,則等待或重試;
- 反之,如果已超時,thread 3 通過下面的操作來嘗試獲得鎖:
GETSET lock.foo <current Unix time + lock timeout + 1>
通過getSet,thread 3 拿到的時間戳如果仍然是超時的,那就說明,thread 3 如願以償拿到鎖了。 - 如果在thread 3 之前,有個叫thread 4 的客戶端比thread 3 快一步執行了上面的操作,那麼thread 3 拿到的時間戳是個未超時的值,這時,thread 3 沒有如期獲得鎖,需要再次等待或重試。留意一下,儘管thread 3 沒拿到鎖,但它改寫了thread 4 設置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。
2.3.4
1).基於redisson分佈式鎖框架實現
2).基於SpringRedisTemplate實現分佈式鎖
3).基於Jedis實現分佈式鎖
原理一樣
public synchronized boolean acquire(Jedis jedis,String lockKey, long expires) throws InterruptedException {
inttimeoutMsecs = 10 * 1000;
inttimeout = timeoutMsecs;
while (timeout >= 0 ) {
String expiresStr = String.valueOf(expires ); // 鎖到期時間
if (jedis.setnx( lockKey, expiresStr ) == 1 ) {
// lock acquired
return true;
}
String currentValueStr = jedis.get(lockKey); //redis裏的時間
if(currentValueStr!=null&& Long.parseLong(currentValueStr) <System.currentTimeMillis()) {
// 判斷是否爲空,不爲空的情況下,如果被其他線程設置了值,則第二個條件判斷是過不去的
// lock is expired
//Getset 命令用於設置指定 key 的值,並返回 key 舊的值。
String oldValueStr = jedis.getSet(lockKey, expiresStr);
// 獲取上一個鎖到期時間,並設置現在的鎖到期時間
// 只有一個線程才能獲取上一個線上的設置時間,因爲jedis.getSet是同步的
if (oldValueStr != null && oldValueStr.equals(currentValueStr)){
// 如過這個時候,多個線程恰好都到了這裏,但是隻有一個線程的設置值和當前值相同,他纔有權利獲取鎖
// lock acquired
return true;
}
}
timeout -= 100;
Thread.sleep(100); // 每100毫秒重試一次,直至timeout用盡
}
//Expire命令用於設定鍵有效期。到期時間後鍵不會在Redis中使用。
returnfalse;
}
當然,方法不是唯一的,也可以不用GetSet方法,單用setnx也可以實現,在while循環裏處理線程的sleep時間,這裏就不舉例了