redis從入門到踩坑

背景前端

Redis在互聯網項目的使用也是很是廣泛的,做爲最經常使用的NO-SQL數據庫,對Redis的瞭解已經成爲了後端開發的必備技能。小編對Redis的使用時間不長,可是項目中確兩次踩中了Redis的坑,今天特地從基礎知識層面到實戰層面對Redis知識進行梳理,可以達到對Redis的知識體系有更全面和深刻的理解。java

Redis的特色redis

優勢:
  1. Key-Value類型的內存數據庫,是增強版的Memcached。
  2. 整個數據庫都是在內存中進行操做的,而且按期異步持久化數據到硬盤上進行保存。
  3. 在內存中進行操做,不存在磁盤IO,性能方面是很是出色的,讀取操做處理速度能夠超過10萬次每秒,是已知性能最快的Key-Value 數據庫。
  4. Redis還提供豐富的數據結構類型。
  5. Redis利用隊列技術將併發訪問變爲串行訪問,消除了傳統數據庫串行控制的開銷。
缺點:
  1. 數據庫容量受到物理內存的限制,不能用做海量數據的高性能讀寫,所以Redis適合的場景主要侷限在較小數據量的高性能操做和運算上。

Redis和Memcached的比較spring

這裏寫圖片描述

Redis的經常使用數據結構及使用場景數據庫

String

String是Redis最基本的類型,一個key對應一個value,也是最經常使用的數據結構,在定義每一個String的key的時候,記得加上前綴。一個Key最大能存儲512MB,一個Value最大能存儲1G。json

Set

Redis的Set是string類型的無序集合,集合是經過哈希表實現的,因此添加、刪除和查找的複雜度都是O(1)。Set集合取交集、差集和並集能夠完成兩組數據的比較,因此Redis借用Set數據結構經常使用於兩組數據的比較。後端

ZSet

Redis ZSet和Set同樣也是String類型元素的結合,而且不容許重複的成員。不一樣的是ZSet中每一個元素都會關聯一個double類型的分數,Redis經過分數(score)爲集合中的全部元素進行大小排序。注意ZSet的成員是惟一的,但分數(score)卻能夠重複。經常使用語排行榜、分頁查詢和獲取指定範圍數據等應用場景。數組

Hash

Redis hash是一個string類型的field和value的映射表,hash特別適合用於存儲對象。緩存

List

Redis 列表是簡單的字符串列表,按照插入順序排序。你能夠添加一個元素到列表的頭部(左邊)或者尾部(右邊)。經常使用於構建異步隊列。服務器

Redis實現分佈式鎖

場景:

用戶在使用APP的時候,頁面很是的卡頓,就會隨便狂點,因爲接口沒有作重複提交,會出現好幾個相同的請求,在service層,一個線程沒有insert完成,另外一個線程一查,空的。因而也插入一條進來。本來每一個人一條的,某個業務員出現了三條,致使業務邏輯錯誤。在業務邏輯中常常會有先查詢判空再插入的場景,可是在高併發的時候,很容易出現插入記錄重複的狀況。

爲了保證一個方法或屬性在高併發狀況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的狀況下,可使用Java併發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。可是隨着業務發展的須要,原單機部署的系統被演化成分佈式集羣系統後,因爲分佈式系統多線程、多進程而且分佈在不一樣機器上,這將使原單機部署狀況下的併發控制鎖策略失效,單純的Java API並不能提供分佈式鎖的能力。爲了解決這個問題就須要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題!

分佈式鎖機制經常使用的有三種方式,redis分佈式鎖、zookeeper和數據庫表。
在這裏簡單介紹基於redis的分佈式鎖。

  1. setnx
    setnx key val:當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不作,返回0。

  2. expire
    expire key timeout : 設置key爲一個超時時間,單位爲second,超過這個時間鎖就會自動釋放,避免出現因爲客戶端crash,不釋放鎖,致使死鎖的現象。

  3. delete
    delete key : 刪除key

  4. 實現思想
    (1)獲取鎖的時候,使用setnx加鎖,並使用expire命令爲鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,經過此在釋放鎖的時候進行判斷。
    (2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
    (3)釋放鎖的時候,經過UUID判斷是否是該鎖,如果該鎖,則執行delete進行鎖釋放。

此處須要補上相關的代碼

這裏寫圖片描述

這裏寫圖片描述

上圖所示爲採用redis緩存實現分佈式系統下,分佈式鎖的效果圖。

序列化和反序列化

實體對象等存入到Redis數據庫中,並非直接存儲的,是以byte數組的形式存儲的,因此存儲到Redis中的時候,須要序列化成byte數據,從Redis讀取數據的時候,須要進行反序列化操做。
spring-data-redis包中存在

public interface RedisSerializer<T> {
    byte[] serialize(T var1) throws SerializationException;
    T deserialize(byte[] var1) throws SerializationException;
}

實現此接口的類有以下:

  1. GenericToStringSerializer
    能夠將任何對象泛化爲字符串並序列化

  2. StringRedisSerializer
    簡單的字符串序列化

  3. JdkSerializationRedisSerializer
    JDK提供的序列化功能,被序列化的對象必須實現Serializable接口。
    優勢: 優勢是反序列化時不須要提供類型信息(class),而且速度最快。
    缺點: 序列化後的結果很是龐大,是JSON格式的5倍左右,這樣就會消耗redis服務器的大量內存,且經過redis客戶端也不容易閱讀。
  4. JacksonJsonRedisSerializer、Jackson2JsonRedisSerializer 和GenericJackson2JsonRedisSerializer
    使用Jackson庫將對象序列化爲JSON字符串。
    優勢: 速度快,序列化後的字符串短小精悍,而且易於閱讀。
    缺點: 但缺點也很是致命,那就是此類的構造函數中有一個類型參數,必須提供要序列化對象的類型信息(.class對象)。

項目中Redis的踩坑記

下面分享兩個項目中使用Redis時候踩到坑。

坑1

【問題背景】
在生產環境的Redis常常會報出RedisConnectionFailureException: java.net.SocketException: Broken pipe

【異常打印】

11:28:29 INFO  - get data from redis, key = c15aad89-4a1a-4cb0-82a5-2027b990c1ca
11:28:29 WARN  - /market/info/eForum/getIndexList
org.springframework.data.redis.RedisConnectionFailureException: java.net.SocketException: Broken pipe; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Broken pipe
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:67) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:41) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:37) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:37) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:212) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.connection.jedis.JedisConnection.get(JedisConnection.java:1117) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperation

【問題緣由】

Redis底層也建立了鏈接池,獲取到了失效的鏈接,而且Redis客戶端嘗試經過此鏈接池跟服務端進行通訊, 致使拋出上面的異常。

【解決辦法】
Redis配置的鏈接池使用jar包commons-pool-2.4.2.jar方式,其中BaseObjectPoolConfig類爲基礎配置類。

private boolean testOnCreate = false;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = false;

如上述四個屬性參數默認都是false,能夠經過修改 testOnBorrow = true 和 testWhileIdle = true 來解決獲取無效連接的問題。其中 testOnBorrow = true 是獲取連接的時候對連接的有效性進行檢查,會影響效率,在高併發的前提下。因此通常只是配置 testWhileIdle = true , 這個是在閒暇的時候進行檢查,去除無效的連接。

坑2

【問題背景】
版本日那天提交了代碼閒來無事,看到用戶信息類UserInfoExt,存儲在common的util目錄下,有強迫症的我,硬是把它移到了entity包下。覺得完美的重構了,沒想到挖出了一個巨大的坑。打預發版的包到測試環境,立馬全部的已登陸用戶,都不能進行其餘操做。只要切換頁面就會拋出「網絡服務異常狀況」,整個預發版的測試環境被我搞癱瘓了,你們都無法測試。 預發版測試不完成,就無法正常發版,說實話那時候壓力還挺大的,全項目的人都盯着你。之後要重構代碼,千萬別發版前重構,最好是版本迭代開始的前幾天就重構好,這樣即便重構帶來的bug,也有足夠的時候去發現和解決。

【異常打印】

19:32:47 INFO  - Started Application in 10.932 seconds (JVM running for 12.296)
19:32:50 INFO  - get data from redis, key = 10d044f9-0e94-420b-9631-b83f5ca2ed30
19:32:50 WARN  - /market/renewal/homePage/index
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve type id 'com.pa.market.common.util.UserInfoExt' into a subtype of [simple type, class java.lang.Object]: no such class found
 at [Source: [B@641a684c; line: 1, column: 11]; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'com.pa.market.common.util.UserInfoExt' into a subtype of [simple type, class java.lang.Object]: no such class found at [Source: [B@641a684c; line: 1, column: 11]

【問題緣由】
項目中使用了攔截器,對每一個http請求進行攔截。經過前端傳遞過來的token,去redis緩存中獲取用戶信息UserInfoExt,用戶信息是在用戶登陸的時候存入到redis緩存中的。根據獲取到的用戶信息來判斷是否存是登陸狀態。
因此除白名單外的url,其餘請求都須要進行這個操做。經過日誌打印,很明顯出如今UserInfoExt對象存儲到redis中序列化和反序列化的操做步驟。
【解決辦法】

@Bean
public RedisTemplate<K, V> redisTemplate() {
    RedisTemplate<K, V> redisTemplate = new RedisTemplate<K, V>();
    redisTemplate.setConnectionFactory(jedisConnectionFactory());
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return redisTemplate;
 }

查看Redis的Bean定義發現,對key的序列化使用的是StringRedisSerializer系列化,value值的序列化是GenericJackson2JsonRedisSerializer的序列化方法。

其中GenericJackson2JsonRedisSerializer序列化方法會在redis中記錄類的class信息,以下所示:

{
"@class": "com.pa.market.common.util.UserInfoExt",
"url": "www.baidu.com",
"name": "baidu"
}

"@class": "com.pa.market.common.util.UserInfoExt",每一個對象都會有這個id存在(能夠經過源碼看出爲嘛有這個@class),若是用戶一直處在登陸狀態,是以com.pa.market.common.util.UserInfoExt這個路徑進行的序列化操做。可是移動了UserInfoExt的類路徑後,包全名變了。因此會拋出no such class found的異常。這樣在判斷用戶是否存在的地方就拋出了異常,故而全部的請求都失敗了,已經登陸的用戶無法進行任何操做。

【總結】
對於上面的序列化的坑,貌似沒有很好的解決方案。從比較經常使用的序列化和反序列化類,能夠發現每一個都有各自的優勢和缺點。若是在redis層面把對象轉成json,那麼每條記錄中都會有@class這個標記,若是之後代碼重構,移動類路徑,確定是不行的,是個巨坑。若是在入redis以前,就把對象直接轉成json,而後用StringRedisSerializer的方式對value進行序列化和反序列化,這樣可讀性好,也不會跟對象的類路徑有強關聯。可是須要中間作一道處理,寫的時候須要對象轉json,讀的時候又須要json轉對象,會下降效率。

Redis的高級特性

一、集羣
二、發佈訂購
三、持久化
四、Redis服務器如何容災,如何預防單點故障等
五、讀寫分離操做
六、異步隊列
七、Redis的雪崩和穿透

以上特性有待後續的解鎖,敬請期待!

相關文章
相關標籤/搜索