馮紹峰趙麗穎結婚微博又掛了!開發者該如何提高系統性能?
明星一結婚,微博就開掛!
那麼,作為開發者是否有好的方法,來快速提高系統性能呢?
接下來,來自 CSDN 博客專家的「拿客_三產」,拿客站長,與我們分享目前最為炙手可熱的 Key-Value 資料庫、常用做緩存、Session 共享中間件、分散式鎖等的——Redis 技術。
說明:閱讀本文章需要一定 Web 開發經驗,最好對 Redis 有一個基本的認知,文章最後的附錄也會為大家提供一些相關的文章,本文章只是為了讓那些對 Redis 的應用僅僅局限於 緩存 的開發人員了解到 Redis 更多可能的應用場景,由於篇幅限制,文中很多場景只是闡述了實現的思想及部分原理,僅僅提供了部分功能的具體實現。
現代高並發複雜系統面臨的挑戰
現代系統隨著功能的複雜化,各種各樣需求層出不窮,面對愈加複雜話的業務系統、越來越龐大的用戶群體,以及用戶對體驗的要求越來越高,性能就變得更加重要。
拋開代碼邏輯、伺服器性能的相關問題外,提高性能的方式有以下幾種:
- 動靜分離負載均衡分散式集群化緩存限流處理數據壓縮其他
我們來分析一下負載均衡、分散式、集群化涉及的問題:
- 配置管理變得複雜,因此需要設置配置中心來解決該問題。同一個用戶的請求會轉發至不同的 Web 伺服器,從而導致 Session 丟失等問題。同一個請求在分散式環境中需要不同服務來提供不同處理,從而需要分散式事務來確保數據的一致性。分散式唯一 ID 問題。
另外針對不同部分系統中的一些特定問題又有其他的一些特殊業務需求:
- IP統計用戶登錄記錄統計實時的排行榜原子計數最新評論
誠然,以上各種問題都有花樣繁多的解決方法,例如:
配置中心可以使用 Zookpeer、Redis 等實現。
Session 丟失可以使用 Session 同步、客戶端 token、Session 共享等解決,其中 Session 共享又可以細分不同實現方式。
面對層出不窮的概念,以及各種新興的技術,我們往往會顯得力不從心,那麼有沒有一個銀彈可以解決這些問題呢?
Redis 非銀彈卻無比接近
我這裡為大家推薦的就是 Redis ,雖然它離真正意義的銀彈還是有些距離,但是他是為數不多的接近銀彈的解決方案:
- Redis 使用 C 開發,是一款內存 K/V 資料庫,架構設計極簡,性能卓著。Redis 採用 單線程 多路復用的設計,避免了並髮帶來的鎖性能損耗等問題。Redis 安裝、測試、配置、運維較其他產品更為容易。Redis 是目前為止最受歡迎的 K/V 資料庫,支持持久化,value 支持多種數據結構。Redis 命令語法簡單,極易掌握。Redis 提供了一種通用的協議,使得各種編程語言都能很方便的開發出與其交互的客戶端。Redis 開放源碼,我們可以對其進行二次開發來定製優化。Redis 目前有較好的社區維護,版本迭代有所保障,新的功能也在有條不紊的添加完善。Redis 有較好的主從複製、集群相關支持。最新版本提供模塊化功能,可以方便的擴展功能。
接下來我們就來說說怎麼使用 Redis 解決之前提到的問題:
1. 配置中心
Redis 本身就是內存 K/V 資料庫,支持 哈希、集合、列表等五種數據結構,從而配置信息的存儲、讀取速度都能夠得到滿足,Redis 還提供訂閱/發布功能從而可以在配置發生改變時通知不同伺服器來進行更新相關配置。
2. 分散式鎖
使用 Redis 的 SETNX 命令或者 SET 命令配合 NX 選項的方式以及過期時間等功能可以很方便的實現一個性能優越的分散式鎖。
3. 緩存
Redis 支持多種過期淘汰機制,本身性能的優勢也使 Redis 在緩存方面得到廣泛使用。
4. Lua 腳本
Lua 是一種輕量小巧的腳本語言,用標準C語言編寫並開放源代碼。Redis 支持 Lua 腳本的運行,從而可以擴展 Redis 中的命令實現很多複雜功能。
Redis 支持使用 Lua 腳本來實現一些組合命令邏輯處理,從而可以使用 Redis 做為限流、分散式唯一 ID 相關技術的實現。
5. Redis 支持 BitMaps
點陣圖(bitmap)是一種非常常用的結構,在索引,數據壓縮等方面有廣泛應用,能同時保證存儲空間和速度最優化(而不必空間換時間)。
使用 Redis 的 BitMaps 做為用戶登錄記錄統計,不僅統計速度極快,而且內存佔用極低。
6. Redis 支持 HyperLogLog 演算法
Redis HyperLogLog是一種使用隨機化的演算法,以少量內存提供集合中唯一元素數量的近似值。
HyperLogLog 可以接受多個元素作為輸入,並給出輸入元素的基數估算值:
HyperLogLog 的優點是,即使輸入元素的數量或者體積非常非常大,計算基數所需的空間總是固定的、並且是很小的。
在 Redis 裡面,每個 HyperLogLog 鍵只需要花費 12 KB 內存,就可以計算接近 2^64 個不同元素的基數。這和計算基數時,元素越多耗費內存就越多的集合形成鮮明對比。使用 HyperLogLog 演算法,我們可以輕而易舉的實現 IP 統計等對數據容許些許誤差的統計功能。
- 基數:集合中不同元素的數量。比如 {『apple』, 『banana』, 『cherry』, 『banana』, 『apple』} 的基數就是3。估算值:演算法給出的基數並不是精確的,可能會比實際稍微多一些或者稍微少一些,但會控制在合理的範圍之內。
7. Redis 支持 Geo 功能
我們可以使用基於 Redis 來實現地理位置相關管理,附近的人、兩地理位置間距離計算等功能變得極為容易實現。
8. 簡單消息隊列
Redis 列表 + 發布/訂閱功能可以很方便的實現一個簡單的消息隊列,將消息存入 Redis 列表中,通過 發布/訂閱功能通知指定成員,成員獲取到通知後可以根據通知內容進行對應處理。
9. 全文檢索
Redis 官方團隊開發了 RediSearch 模塊,可以實現使用 Redis 來做全文檢索的功能。
10. 分散式唯一ID
Redis 的設計使其可以避免並發的多種問題,使其命令都是原子執行,這些特性都天生匹配分散式唯一ID生成器的要求。
而且通過與 Lua 腳本的結合使用更是能生成複雜的有某些規律的唯一ID。
部分代碼實現
下面我們以 Java代碼作為演示(編程語言實現方式原理類似只是具體實現方式有些許差別而已)講解幾個功能的實現:
Session 共享:
原理:將不同 Web 伺服器的 Session 信息統一存儲在 Redis 中,並且獲取 Session 也是從 Redis 中獲取
實現方法:
方法一:基於 Tomcat 實現 Sessioin 共享:
Tomcat 配置步驟(相關代碼資源可以從https://gitee.com/coderknock/Tomcat-Redis-Session-Manager-Demo 獲取):
- 將 commons-pool2-2.4.2.jar、jedis-2.9.0.jar、commons-pool2-2.4.2.jar 三個 jar 包放到 Tomcat 下的 lib 目錄下(注意:不是項目的 lib 目錄)。修改 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 拿客 http://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) {
http://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) {
http://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) {
http://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 拿客 http://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();
}
}
http://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 命令演示(「#」之後為說明):
# 1、存儲幾個排行榜成員數據(這裡可以理解為把自己系統已有數據載入到 Redis 中)
ZADD testTop 23 member1 25 member2
# 2、增加某個人的分數(這裡的分數就是排行的依據可以是浮點類型)
ZINCRBY testTop 20 member1 # 此時 testTop 中 member1 的分數就編程了 43
ZINCRBY testTop -10 member2 # 此時 testTop 中 member2 的分數就編程了 15
ZINCRBY testTop 20 member3 # 此時向 testTop 中添加了 member3 成員,分數為 20
# 3、查詢排行榜前兩名,並且查詢出其分數【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/Redis-Top-And-Around
/src/test/java/TopDemo.java 有具體測試用例
Geo 相關功能
Redis 的 Geo 功能提供了查詢兩個成員距離、某個成員附近範圍成員等功能可以用其實現一個簡單的附近的人
Java 相關實現代碼可以查看:
https://gitee.com/coderknock/Redis-Top-And-Around
/src/test/java/GeoDemo.java 有具體測試用例。
緩存
原理:將經常會訪問的數據根據一定規則設置一個 Key 後存入 Redis,每次查詢時先查詢 Redis 中是否包含匹配數據,如果緩存不存在再查詢資料庫。
注意點:對於不存在的數據應該存入一個自己設定的空值並設置過期時間,這樣可以避免緩存擊穿(由於數據不存在,所以設置 Key 對應的值為 null(Java中的表示形式),因為 Redis 會移除值為 null 的 key 這樣會導致,每次查詢還是會訪問資料庫)。
Java 相關實現代碼可以查看:
https://gitee.com/coderknock/Redis-Cache
結束語
本文只是問了發散大家的思維,如對具體功能實現由興趣可以在之後的交流中共同探討。
由於個人的局限性,文中可能存在錯誤表述,大家可以在評論區中提出共同探討。
附錄:Redis環境搭建
在線體驗:
http://try.redis.io/
Windows版本:
https://github.com/MSOpenTech/redis
Linux安裝:
https://www.coderknock.com/blog/2016/05/28/LinuxRedis.html
Redis 配置
https://www.coderknock.com/blog/2017/06/14/Redis%20%E9%85%8D%E7%BD%AE.html
Redis 支持的五大數據結構
Redis 基礎知識擴展閱讀
Redis 基礎知識擴展閱讀:https://segmentfault.com/bookmark/1230000010694933
Redis 發布訂閱圖解
「徵稿啦」
CSDN 公眾號秉持著「與千萬技術人共成長」理念,不僅以「極客頭條」、「暢言」欄目在第一時間以技術人的獨特視角描述技術人關心的行業焦點事件,更有「技術頭條」專欄,深度解讀行業內的熱門技術與場景應用,讓所有的開發者緊跟技術潮流,保持警醒的技術嗅覺,對行業趨勢、技術有更為全面的認知。
如果你有優質的文章,或是行業熱點事件、技術趨勢的真知灼見,或是深度的應用實踐、場景方案等的新見解,歡迎聯繫 CSDN 投稿,聯繫方式:微信(guorui_1118,請備註投稿+姓名+公司職位),郵箱(guorui@csdn.net)。
※給理工男女的一個神奇網站!
※華為雲總裁鄭葉來:易獲取、用得起、方便用的算力是人工智慧發展的關鍵
TAG:CSDN |