GitChat 做者:拿客_三產
原文:Redis 快速提升系統性能的銀彈
關注微信公衆號:GitChat 技術雜談 ,一本正經的講技術html
【不要錯過文末彩蛋】java
說明:閱讀該文章須要必定 Web 開發經驗,最好對 Redis 有一個基本的認知,文章最後的附錄也會爲你們提供一些相關的文章,本文章只是爲了讓那些對 Redis 的應用僅僅侷限於 緩存 的開發人員瞭解到 Redis 更多可能的應用場景,因爲篇幅限制,文中不少場景只是闡述了實現的思想及部分原理,僅僅提供了部分功能的具體實現。git
現代系統隨着功能的複雜化,各類各樣需求層出不窮,面對越發複雜話的業務系統、愈來愈龐大的用戶羣體,以及用戶對體驗的要求愈來愈高,性能就變得更加劇要。程序員
拋開代碼邏輯、服務器性能的相關問題外,提升性能的方式有如下幾種:github
咱們來分析一下負載均衡、分佈式、集羣化涉及的問題:web
另外針對不一樣部分系統中的一些特定問題又有其餘的一些特殊業務需求:redis
誠然,以上各類問題都有花樣繁多的解決方法,例如:算法
配置中心可使用 Zookpeer、Redis 等實現。數據庫
Session 丟失可使用 Session 同步、客戶端 token、Session 共享等解決,其中 Session 共享又能夠細分不一樣實現方式。apache
面對層出不窮的概念,以及各類新興的技術,咱們每每會顯得力不從心,那麼有沒有一個銀彈能夠解決這些問題呢?
我這裏爲你們推薦的就是 Redis ,雖然它離真正意義的銀彈仍是有些距離,可是他是爲數很少的接近銀彈的解決方案:
接下來咱們就來講說怎麼使用 Redis 解決以前提到的問題:
Redis 自己就是內存 K/V 數據庫,支持 哈希、集合、列表等五種數據結構,從而配置信息的存儲、讀取速度都可以獲得知足,Redis 還提供訂閱/發佈功能從而能夠在配置發生改變時通知不一樣服務器來進行更新相關配置。
使用 Redis 的 SETNX 命令或者 SET 命令配合 NX 選項的方式以及過時時間等功能能夠很方便的實現一個性能優越的分佈式鎖。
Redis 支持多種過時淘汰機制,自己性能的優點也使 Redis 在緩存方面獲得普遍使用。
Lua 是一種輕量小巧的腳本語言,用標準C語言編寫並開放源代碼。Redis 支持 Lua 腳本的運行,從而能夠擴展 Redis 中的命令實現不少複雜功能。
Redis 支持使用 Lua 腳原本實現一些組合命令邏輯處理,從而可使用 Redis 作爲限流、分佈式惟一 ID 相關技術的實現。
位圖(bitmap)是一種很是經常使用的結構,在索引,數據壓縮等方面有普遍應用,能同時保證存儲空間和速度最優化(而沒必要空間換時間)。
使用 Redis 的 BitMaps 作爲用戶登陸記錄統計,不只統計速度極快,並且內存佔用極低。
Redis HyperLogLog是一種使用隨機化的算法,以少許內存提供集合中惟一元素數量的近似值。
HyperLogLog 能夠接受多個元素做爲輸入,並給出輸入元素的基數估算值:
HyperLogLog 的優勢是,即便輸入元素的數量或者體積很是很是大,計算基數所需的空間老是固定的、而且是很小的。
在 Redis 裏面,每一個 HyperLogLog 鍵只須要花費 12 KB 內存,就能夠計算接近 2^64 個不一樣元素的基數。這和計算基數時,元素越多耗費內存就越多的集合造成鮮明對比。使用 HyperLogLog 算法,咱們能夠垂手可得的實現 IP 統計等對數據允許些許偏差的統計功能。
咱們可使用基於 Redis 來實現地理位置相關管理,附近的人、兩地理位置間距離計算等功能變得極爲容易實現。
Redis 列表 + 發佈/訂閱功能能夠很方便的實現一個簡單的消息隊列,將消息存入 Redis 列表中,經過 發佈/訂閱功能通知指定成員,成員獲取到通知後能夠根據通知內容進行對應處理。
Redis 官方團隊開發了 RediSearch 模塊,能夠實現使用 Redis 來作全文檢索的功能。
分佈式惟一ID
Redis 的設計使其能夠避免併發的多種問題,使其命令都是原子執行,這些特性都天生匹配分佈式惟一ID生成器的要求。 並且經過與 Lua 腳本的結合使用更是能生成複雜的有某些規律的惟一ID。
下面咱們以 Java代碼做爲演示(編程語言實現方式原理相似只是具體實現方式有些許差異而已)講解幾個功能的實現:
原理:將不一樣 Web 服務器的 Session 信息統一存儲在 Redis 中,而且獲取 Session 也是從 Redis 中獲取
實現方法:
方法一:基於 Tomcat 實現 Sessioin 共享:
Tomcat 配置步驟(相關代碼資源能夠從 https://gitee.com/coderknock/... 獲取):
XML <Context> ...... <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="127.0.0.1" port="6379" database="0" maxInactiveInterval="60" password="admin123" /> ...... </Context>
方法二:基於 Fileter 、 自行實現 HttpServletRequestWrapper 、 HttpSession :
關鍵代碼:
HttpSessionWrapper.java
java import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONException; import com.coderknock.jedis.executor.JedisExecutor; import com.coderknock.pojo.User; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionContext; import java.util.Enumeration; /** * <p></p> * * @author 三產 * @version 1.0 * @date 2017-08-26 * @QQGroup 213732117 * @website http://www.coderknock.com * @copyright Copyright 2017 拿客 coderknock.com All rights reserved. * @since JDK 1.8 */ public class HttpSessionWrapper implements HttpSession { protected final Logger logger = LogManager.getLogger(HttpSessionWrapper.class); private String sid = ""; private HttpServletRequest request; private HttpServletResponse response; private final long creationTime = System.currentTimeMillis(); private final long lastAccessedTime = System.currentTimeMillis(); //過時時間單位秒 private int expire_time = 60; public HttpSessionWrapper() { } public HttpSessionWrapper(String sid, HttpServletRequest request, HttpServletResponse response) { this.sid = sid; this.request = request; this.response = response; } public Object getAttribute(String name) { logger.info(getClass() + "getAttribute(),name:" + name); try { Object obj = JedisExecutor.execute(jedis -> { String jsonStr = jedis.get(sid + ":" + name); if (jsonStr != null || StringUtils.isNotEmpty(jsonStr)) { jedis.expire(sid + ":" + name, expire_time);// 重置過時時間 } return jsonStr; }); return obj; } catch (JSONException je) { logger.error(je); } catch (Exception e) { logger.error(e.getMessage()); } return null; } public void setAttribute(String name, Object value) { logger.info(getClass() + "setAttribute(),name:" + name); try { JedisExecutor.executeNR(jedis -> { if (value instanceof String) { String value_ = (String) value; jedis.set(sid + ":" + name, value_);//普通字符串對象 } else { jedis.set(sid + ":" + name, JSON.toJSONString(value));//序列化對象 } jedis.expire(sid + ":" + name, expire_time);// 重置過時時間 }); } catch (Exception e) { logger.error(e); } } public void removeAttribute(String name) { logger.info(getClass() + "removeAttribute(),name:" + name); if (StringUtils.isNotEmpty(name)) { try { JedisExecutor.executeNR(jedis -> { jedis.del(sid + ":" + name); }); } catch (Exception e) { logger.error(e); } } } //...... 省略部分代碼 }
SessionFilter.java
java import com.coderknock.wrapper.DefinedHttpServletRequestWrapper; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.servlet.*; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; /** * <p></p> * * @author 三產 * @version 1.0 * @date 2017-08-26 * @QQGroup 213732117 * @website http://www.coderknock.com * @copyright Copyright 2017 拿客 coderknock.com All rights reserved. * @since JDK 1.8 */ public class SessionFilter implements Filter { protected final Logger logger = LogManager.getLogger(getClass()); private static final String host = "host"; private static final String port = "port"; private static final String seconds = "seconds"; public void init(FilterConfig filterConfig) throws ServletException { logger.debug("init filterConfig info"); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //從cookie中獲取sessionId,若是這次請求沒有sessionId,重寫爲此次請求設置一個sessionId HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String sid = null; if (httpRequest.getCookies() != null) { for (Cookie cookie : httpRequest.getCookies()) { if (cookie.getName().equals("JSESSIONID")) { sid = cookie.getValue(); break; } } } if (StringUtils.isEmpty(sid)) { try { Cookie cookie = new Cookie("JSESSIONID", httpRequest.getLocalAddr() + ":" + request.getLocalPort() + ":" + UUID.randomUUID().toString().replaceAll("-", "")); httpResponse.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } } logger.info("JSESSIONID:" + sid); chain.doFilter(new DefinedHttpServletRequestWrapper(sid, httpRequest, httpResponse), response); } public void destroy() { } }
原理:經過 Redis 有序集合能夠很便捷的實現該功能
關鍵命令:
ZADD key [NX|XX][CH][INCR] score member [score member ...]
: 初始化排行榜中成員及其分數。
ZINCRBY key increment member
:爲某個成員增長分數,若是該成員不存在則會添加該成員並設定分數爲 increment
。
ZUNIONSTORE destination numkeys key [key ...][WEIGHTS weight [weight ...]][AGGREGATE SUM|MIN|MAX]
: 能夠合併多個排行榜,該操做會將幾個集合的並集存儲到 destination
中,其中各個集合相同成員分數會疊加或者取最大、最小、平均值等(根據 [AGGREGATE SUM|MIN|MAX] 參數決定,默認是疊加),從而能夠實現根據多個分排行榜來計算總榜排行的功能。
ZREVRANGE key start stop [WITHSCORES]
:該命令就是最關鍵的獲取排行信息的命令,能夠獲取從高到低的成員。
Redis 命令演示(「#」以後爲說明):
# 一、存儲幾個排行榜成員數據(這裏能夠理解爲把本身系統已有數據加載到 Redis 中) ZADD testTop 23 member1 25 member2 # 二、增長某我的的分數(這裏的分數就是排行的依據能夠是浮點類型) ZINCRBY testTop 20 member1 # 此時 testTop 中 member1 的分數就編程了 43 ZINCRBY testTop -10 member2 # 此時 testTop 中 member2 的分數就編程了 15 ZINCRBY testTop 20 member3 # 此時向 testTop 中添加了 member3 成員,分數爲 20 # 三、查詢排行榜前兩名,而且查詢出其分數【WITHSCORES 選項用於顯示分數,不帶該參數則只會查出成員名稱】 ZREVRANGE testTop 0 1 WITHSCORES #結果: # 1) "member1" # 2) "43" # 3) "member3" # 4) "20" # 假設此時還有一個 排行榜 ZADD testTop2 100 member2 200 member3 123 member4 # 將 testTop testTop2 合成一個總榜 top ZUNIONSTORE top 2 testTop testTop2 # 查詢總榜全部成員排行狀況 ZREVRANGE top 0 -1 WITHSCORES 1) "member3" 2) "220" 3) "member4" 4) "123" 5) "member2" 6) "115" 7) "member1" 8) "43"
Java 相關實現代碼(模擬了 sf.gg 的名望榜)能夠查看。
https://gitee.com/coderknock/... /src/test/java/TopDemo.java 有具體測試用例
Redis 的 Geo 功能提供了查詢兩個成員距離、某個成員附近範圍成員等功能能夠用其實現一個簡單的附近的人
Java 相關實現代碼能夠查看:
https://gitee.com/coderknock/... /src/test/java/GeoDemo.java 有具體測試用例。
原理:將常常會訪問的數據根據必定規則設置一個 Key 後存入 Redis,每次查詢時先查詢 Redis 中是否包含匹配數據,若是緩存不存在再查詢數據庫。
注意點:對於不存在的數據應該存入一個本身設定的空值並設置過時時間,這樣能夠避免緩存擊穿(因爲數據不存在,因此設置 Key 對應的值爲 null(Java中的表示形式),由於 Redis 會移除值爲 null 的 key 這樣會致使,每次查詢仍是會訪問數據庫)。
Java 相關實現代碼能夠查看:
https://gitee.com/coderknock/...
本文只是問了發散你們的思惟,如對具體功能實現由興趣能夠在以後的交流中共同探討。
因爲我的的侷限性,文中可能存在錯誤表述,你們能夠在評論區中提出共同探討。
在線體驗: http://try.redis.io/
Windows版本: https://github.com/MSOpenTech...
Linux安裝: https://www.coderknock.com/bl...
https://www.coderknock.com/bl...
實錄:《拿客_三產:解析 Redis 如何快速提升系統性能》
重磅 Chat 分享:
《高效學習,快速變現:不走彎路的五大學習策略》
分享人:
一名會在 B 站直播寫代碼,會玩雜耍球、彈 Ukulele、極限健身、跑步、寫段子、畫畫、翻譯、寫做、演講、培訓的程序員。喜歡用編程實現本身的想法,在 Android 市場上賺過錢,有屢次創業經歷。擅長學習,習慣養成,時間管理。身體力行地影響他人作出積極的改變!目前就任於 ThoughtWorks,致力於傳播快樂高效的編程理念。業餘創立軟件匠藝社區 CodingStyle.cn,組織超過30場技術活動。Chat簡介:
說到學習呀,真是頭大喲:碎片化,沒有較長的連續時間來學習難專一,捧起書,手機卻在召喚:來呀,快活呀~ 反正有,大把時光~作不到,看了不少書,生活中卻作不到然並卵,學了方法和工具,找不到使用場景效率低,學習速度跟不上知識產生的速度記不牢,學習速度趕不上遺忘速度在這個知識氾濫、跨界競爭的年代,學習能力纔是核心競爭力。你想一想,過去一週,有沒有哪一件工做是不須要學習就能完成的?儘管如此重要,大部分人卻沒研究過學習這件事,覺得上下班路上打開「獲得」聽本書,就是碎片時間終身學習者了。我是程序員,諮詢師,培訓師,這幾個角色都要求我必須學得又快又好。本場 Chat 將分析學習的「趨勢,原則,策略」,幫你站在更高的視角看待學習,從「內容,動機,交互,收益,資源」五方面制定策略,解決學習痛點,助你成爲高效學習者!
想要免費參與本場 Chat ?很簡單,「GitChat技術雜談」公衆號後臺回覆「高效學習」