Redis的模糊查詢在生產環境出現嚴重的性能問題

Redis是一個高性能高效率的key-value存儲的nosql數據庫,由于數據是存儲在內存中,因此訪問速度非常快,由于項目涉及到數據庫的查詢非常多,而數據變大并不是非常頻繁,所以在項目中采用Redis分擔大部分MySQL的壓力。
在項目中實際使用我用的Redis提供的客戶端連接工具包jedis,在項目中引入jedis.Jar即可。
- public static Set<String> searchLike(String like_key) {
- //線上環境模糊查詢帶來嚴重的性能問題,杜絕使用
- if(!Config.IS_BUG){
- return null;
- }
- Jedis jedis = RedisApi.getJedis();
- boolean is_ok = true;
- try {
- if (jedis == null) {
- return null;
- }
- return jedis.keys(like_key);
- } catch (Exception e) {
- // TODO: handle exception
- is_ok = false;
- return null;
- } finally {
- close(jedis, is_ok);
- }
- }
每當用戶登錄成功之后,都會生成一個cookie,分別存在客戶端和Redis數據庫,cookie的key由cookie值+用戶ID組成:cookie字符串+"_"+用戶ID,例如用戶cookie為“d9fb0ea5955fcf0a2183c5076”,用戶ID為 19092,那Redis中存儲的key就是 d9fb0ea5955fcf0a2183c5076_19092,最終的key-vlaue就是:
- {"d9fb0ea5955fcf0a2183c5076_19092":d9fb0ea5955fcf0a2183c5076}
而在用戶不斷的登錄成功,就不斷地產生這樣的記錄,久而久之,會積累出非常多的無用的key,浪費redis的空間,也加重了redis查詢的負擔,因此想到使用Redis的模糊查詢來清掉無用的cookie的key。
而Redis的客戶端jedis操作是通過jedis.keys(keys)來完成的,keys可以使用通配符來匹配Redis中的key。
通配符說明:
*: 0到任意多個字符 eg: searchLike("test*")
?: 1個字符
比如現在需要清除某個用戶所有的無用的cookie的key,,則可以寫成“ *_19092 ”。
- String key_like = "*_19092";
- Set<String> keys = RedisApi.searchLike(key_like);
這樣就可以查出所有這個用戶的keys,調用jedis提供的批量刪除key的方法即可達到目的。
- String key_like = "*_19092";
- Set<String> keys = RedisApi.searchLike(key_like);
到這里從需求到邏輯到編碼一氣呵成,簡單測試沒什么問題后,就發布到線上,由于平時網站的流量不算非常高,所以運行了幾天也沒發生什么異常,直到今天早上,擁有幾十萬粉絲的公眾號發推文,推文的內容直接鏈接到網站,因此說瞬間流量是非常高。

運行了大約十分鐘之后,運營突然發瘋的過來說網站訪問非常慢,甚至出現錯誤碼,心里一慌,趕緊上去看日志,我了個乖乖啊,簡直是嚇人,error日志想流水一樣蹦出來,但五一不例外都是下面圖示的錯誤:從Redis池中獲取不大連接數,馬上上redis服務器查看,發現CPU已經到達了100%以上。

能讓Redis的CPU到100%的,我想出了一下幾個可能:
- 連接數過多,占用連接的時間過長
- 存儲的值過大,存取均很占用CPU和內存
- 慢查詢,事其它操作等待時間超時
- redis阻塞,某個操作把Redis阻塞,導致CPU飆升
由于項目上線時間已經很久,前三個可能基本都在平時查看Redis服務器性能的過程中排除掉,因此很大概率是第四個,突然想起前幾天做的功能,有個模糊查詢,該不是這個問題吧?我到網站輸入“Redis 模糊查詢 性能”,出來非常多關于redis模糊查詢性能急劇下降的的情況,而且建議生產環境下禁用redis的模糊查詢,于是我把模糊查詢這塊業務直接注釋掉,重新上線,運行了半天,再沒出現這個問題,因此可以斷定就是模糊查詢搞的鬼。
【替代方案】
有問題肯定是要解決的,既然模糊查詢行不通的,那就得想別的辦法達到目的,想到Redis有Set這這種存儲結構,因此可以把用戶的所有cookie key都放到一個用戶專屬的Set中,每次用戶登錄成功之后,都把之前Set里的cookie key清除,然后再把***的key放進去,這樣就可以達到同樣的目的了。
- String setKey = "prefix_customer_cookie_list_10920";
- String token="ss2ssssss";
- //取出所有的用戶的cookie key
- Set<String> list = RedisApi.getSet(setKey);
- if (list != null && list.size() > 0) {
- //刪除用戶所有的cookie key
- RedisApi.removeFromSet(setKey, list.toArray(new String[0]));
- }
- //把***的cookie key加入到Set中
- RedisApi.addSet(setKey, token);