Redis 快速提升系統性能的銀彈

GitChat 做者:拿客_三產
原文:Redis 快速提升系統性能的銀彈
關注微信公衆號:GitChat 技術雜談 ,一本正經的講技術html

【不要錯過文末彩蛋】java

前言

說明:閱讀該文章須要必定 Web 開發經驗,最好對 Redis 有一個基本的認知,文章最後的附錄也會爲你們提供一些相關的文章,本文章只是爲了讓那些對 Redis 的應用僅僅侷限於 緩存 的開發人員瞭解到 Redis 更多可能的應用場景,因爲篇幅限制,文中不少場景只是闡述了實現的思想及部分原理,僅僅提供了部分功能的具體實現。git

現代高併發複雜系統面臨的挑戰

現代系統隨着功能的複雜化,各類各樣需求層出不窮,面對越發複雜話的業務系統、愈來愈龐大的用戶羣體,以及用戶對體驗的要求愈來愈高,性能就變得更加劇要。程序員

拋開代碼邏輯、服務器性能的相關問題外,提升性能的方式有如下幾種:github

  1. 動靜分離
  2. 負載均衡
  3. 分佈式
  4. 集羣化
  5. 緩存
  6. 限流處理
  7. 數據壓縮
  8. 其餘

咱們來分析一下負載均衡、分佈式、集羣化涉及的問題:web

  1. 配置管理變得複雜,所以須要設置配置中心來解決該問題。
  2. 同一個用戶的請求會轉發至不一樣的 Web 服務器,從而致使 Session 丟失等問題。
  3. 同一個請求在分佈式環境中須要不一樣服務來提供不一樣處理,從而須要分佈式事務來確保數據的一致性。
  4. 分佈式惟一 ID 問題。

另外針對不一樣部分系統中的一些特定問題又有其餘的一些特殊業務需求:redis

  1. IP統計
  2. 用戶登陸記錄統計
  3. 實時的排行榜
  4. 原子計數
  5. 最新評論

誠然,以上各類問題都有花樣繁多的解決方法,例如:算法

配置中心可使用 Zookpeer、Redis 等實現。數據庫

Session 丟失可使用 Session 同步、客戶端 token、Session 共享等解決,其中 Session 共享又能夠細分不一樣實現方式。apache

面對層出不窮的概念,以及各類新興的技術,咱們每每會顯得力不從心,那麼有沒有一個銀彈能夠解決這些問題呢?

Redis 非銀彈卻無比接近

我這裏爲你們推薦的就是 Redis ,雖然它離真正意義的銀彈仍是有些距離,可是他是爲數很少的接近銀彈的解決方案:

  1. Redis 使用 C 開發,是一款內存 K/V 數據庫,架構設計極簡,性能卓著。
  2. Redis 採用 單線程 多路複用的設計,避免了併發帶來的鎖性能損耗等問題。
  3. Redis 安裝、測試、配置、運維較其餘產品更爲容易。
  4. Redis 是目前爲止最受歡迎的 K/V 數據庫,支持持久化,value 支持多種數據結構。
  5. Redis 命令語法簡單,極易掌握。
  6. Redis 提供了一種通用的協議,使得各類編程語言都能很方便的開發出與其交互的客戶端。
  7. Redis 開放源碼,咱們能夠對其進行二次開發來定製優化。
  8. Redis 目前有較好的社區維護,版本迭代有所保障,新的功能也在有條不紊的添加完善。
  9. Redis 有較好的主從複製、集羣相關支持。
  10. 最新版本提供模塊化功能,能夠方便的擴展功能。

接下來咱們就來講說怎麼使用 Redis 解決以前提到的問題:

  1. 配置中心

​Redis 自己就是內存 K/V 數據庫,支持 哈希、集合、列表等五種數據結構,從而配置信息的存儲、讀取速度都可以獲得知足,Redis 還提供訂閱/發佈功能從而能夠在配置發生改變時通知不一樣服務器來進行更新相關配置。

  1. 分佈式鎖

​使用 Redis 的 SETNX 命令或者 SET 命令配合 NX 選項的方式以及過時時間等功能能夠很方便的實現一個性能優越的分佈式鎖。

  1. 緩存

​Redis 支持多種過時淘汰機制,自己性能的優點也使 Redis 在緩存方面獲得普遍使用。

  1. Lua 腳本

Lua 是一種輕量小巧的腳本語言,用標準C語言編寫並開放源代碼。Redis 支持 Lua 腳本的運行,從而能夠擴展 Redis 中的命令實現不少複雜功能。

Redis 支持使用 Lua 腳原本實現一些組合命令邏輯處理,從而可使用 Redis 作爲限流、分佈式惟一 ID 相關技術的實現。

  1. Redis 支持 BitMaps

位圖(bitmap)是一種很是經常使用的結構,在索引,數據壓縮等方面有普遍應用,能同時保證存儲空間和速度最優化(而沒必要空間換時間)。

使用 Redis 的 BitMaps 作爲用戶登陸記錄統計,不只統計速度極快,並且內存佔用極低。

  1. Redis 支持 HyperLogLog 算法

Redis HyperLogLog是一種使用隨機化的算法,以少許內存提供集合中惟一元素數量的近似值。

HyperLogLog 能夠接受多個元素做爲輸入,並給出輸入元素的基數估算值:

  • 基數:集合中不一樣元素的數量。好比 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基數就是3。
  • 估算值:算法給出的基數並非精確的,可能會比實際稍微多一些或者稍微少一些,但會控制在合理的範圍以內。

HyperLogLog 的優勢是,即便輸入元素的數量或者體積很是很是大,計算基數所需的空間老是固定的、而且是很小的。

在 Redis 裏面,每一個 HyperLogLog 鍵只須要花費 12 KB 內存,就能夠計算接近 2^64 個不一樣元素的基數。這和計算基數時,元素越多耗費內存就越多的集合造成鮮明對比。使用 HyperLogLog 算法,咱們能夠垂手可得的實現 IP 統計等對數據允許些許偏差的統計功能。

  1. Redis 支持 Geo 功能

​咱們可使用基於 Redis 來實現地理位置相關管理,附近的人、兩地理位置間距離計算等功能變得極爲容易實現。

  1. 簡單消息隊列

Redis 列表 + 發佈/訂閱功能能夠很方便的實現一個簡單的消息隊列,將消息存入 Redis 列表中,經過 發佈/訂閱功能通知指定成員,成員獲取到通知後能夠根據通知內容進行對應處理。

  1. 全文檢索

Redis 官方團隊開發了 RediSearch 模塊,能夠實現使用 Redis 來作全文檢索的功能。

  1. 分佈式惟一ID

    Redis 的設計使其能夠避免併發的多種問題,使其命令都是原子執行,這些特性都天生匹配分佈式惟一ID生成器的要求。
    
    並且經過與 Lua 腳本的結合使用更是能生成複雜的有某些規律的惟一ID。

部分代碼實現

下面咱們以 Java代碼做爲演示(編程語言實現方式原理相似只是具體實現方式有些許差異而已)講解幾個功能的實現:

Session 共享

原理:將不一樣 Web 服務器的 Session 信息統一存儲在 Redis 中,而且獲取 Session 也是從 Redis 中獲取

實現方法:

方法一:基於 Tomcat 實現 Sessioin 共享:

Tomcat 配置步驟(相關代碼資源能夠從 https://gitee.com/coderknock/... 獲取):

  1. 將 commons-pool2-2.4.2.jar、jedis-2.9.0.jar、commons-pool2-2.4.2.jar 三個 jar 包放到 Tomcat 下的 lib 目錄下(注意:不是項目的 lib 目錄)。
  2. 修改 Tomcat conf 下 context.xml:
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 有具體測試用例

Geo 相關功能

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/...

結束語

本文只是問了發散你們的思惟,如對具體功能實現由興趣能夠在以後的交流中共同探討。

因爲我的的侷限性,文中可能存在錯誤表述,你們能夠在評論區中提出共同探討。

附錄

Redis環境搭建

在線體驗: http://try.redis.io/

Windows版本: https://github.com/MSOpenTech...

Linux安裝: https://www.coderknock.com/bl...

Redis 配置

https://www.coderknock.com/bl...

Redis 支持的五大數據結構

enter image description here

Redis 基礎知識擴展閱讀

Redis 基礎知識擴展閱讀

Redis 發佈訂閱圖解

 Redis 發佈訂閱圖解


實錄:《拿客_三產:解析 Redis 如何快速提升系統性能》


彩蛋

重磅 Chat 分享:

《高效學習,快速變現:不走彎路的五大學習策略》

分享人:
一名會在 B 站直播寫代碼,會玩雜耍球、彈 Ukulele、極限健身、跑步、寫段子、畫畫、翻譯、寫做、演講、培訓的程序員。喜歡用編程實現本身的想法,在 Android 市場上賺過錢,有屢次創業經歷。擅長學習,習慣養成,時間管理。身體力行地影響他人作出積極的改變!目前就任於 ThoughtWorks,致力於傳播快樂高效的編程理念。業餘創立軟件匠藝社區 CodingStyle.cn,組織超過30場技術活動。

Chat簡介:
說到學習呀,真是頭大喲:碎片化,沒有較長的連續時間來學習難專一,捧起書,手機卻在召喚:來呀,快活呀~ 反正有,大把時光~作不到,看了不少書,生活中卻作不到然並卵,學了方法和工具,找不到使用場景效率低,學習速度跟不上知識產生的速度記不牢,學習速度趕不上遺忘速度在這個知識氾濫、跨界競爭的年代,學習能力纔是核心競爭力。你想一想,過去一週,有沒有哪一件工做是不須要學習就能完成的?儘管如此重要,大部分人卻沒研究過學習這件事,覺得上下班路上打開「獲得」聽本書,就是碎片時間終身學習者了。

我是程序員,諮詢師,培訓師,這幾個角色都要求我必須學得又快又好。本場 Chat 將分析學習的「趨勢,原則,策略」,幫你站在更高的視角看待學習,從「內容,動機,交互,收益,資源」五方面制定策略,解決學習痛點,助你成爲高效學習者!

想要免費參與本場 Chat ?很簡單,「GitChat技術雜談」公衆號後臺回覆「高效學習」

這裏寫圖片描述

相關文章
相關標籤/搜索