開發大系統必備技術之Redis技術學習與研究
1、【引子】
前段時間有朋友問了一個問題,是關于如何開發一套大系統的問題。這類大系統可以像Twitter, Youtube這樣的規模。其中有一個技術點,就是Redis系統。
接下來我們就通過這篇文章來學習和探索一下Redis系統。
2、【Redis】
Redis(遠程字典服務器)是一個在內存上構建的數據結構系統,它實現了一個分布式的內存中的鍵值數據庫,它具有一定程度的持久性。Redis支持不同類型的抽象數據結構,如字符串、列表、映射、集、排序集、HyperLogLogs、位圖、流和空間索引等。
該系統主要由Salvatore Sanfilippo開發,截止到2019年,由Redis實驗室贊助,它是在BSD 3-clause許可證下發布的開源軟件。
3、【歷史】
Redis這個名字的意思是RemoteDictionary Server。Redis項目開始時,Redis的原始開發者Salvatore Sanfilippo,綽號antirez,他的意大利初創公司試圖提高自己初創公司的可擴展性,于是開發了一個實時網絡日志分析器。
但在使用傳統的數據庫系統擴展某些類型的工作負載時遇到了很大問題,之后,Sanfilippo開始用Tcl語言做了Redis的第一個概念驗證版本,后來Sanfilippo把這個原型翻譯成了C語言,并實現了第一個數據類型--list。
Sanfilippo在內部使用了幾個星期并獲得成功后,決定將其開源,在HackerNews上宣布了這個項目。這個項目開始得到了廣泛的關注,在Ruby社區中更是如此,GitHub和Instagram是首批采用該項目的公司之一。
2010年3月,Sanfilippo被VMware錄用。
2013年5月,Redis由Pivotal Software(VMware分拆出來的公司)贊助。
2015年6月,發展成為由Redis實驗室贊助。
2018年10月,Redis 5.0版本發布,引入了Redis Stream---一種新的數據結構,可以在一個鍵上自動存儲多個字段和字符串值,并以時間為基準的順序自動存儲。
4、【流行度】
根據DB-Engines的月度排名,Redis是最受歡迎的關鍵值數據庫,根據用戶評價,Redis也被評為用戶滿意度和市場占有率排名第4的NoSQL數據庫,在容器中最受歡迎的NoSQL數據庫。
在排名網站stackshare.io中,Redis被評為2015年NoSQL數據庫第一名。
在2017年、2018年和2019年的Stack Overflow開發者調查中,Redis被評為最受喜愛的數據庫。
5、【支持的語言】
從2.6版本開始,Redis采用Lua語言進行服務器端腳本編寫。
許多編程語言在客戶端都有Redis語言綁定,包括:ActionScript、C、C++、C#、Chicken、Clojure、CommonLisp、Crystal、D、Dart、Elixir、Erlang、Go、Haskell、Haxe、Io、Java、JavaScript(Node.js)、Julia、Lua、Objective-C、OCaml、Perl、PHP、Pure Data、Python、R、Racket、Ruby、Rust、Scala、Smalltalk、Swift和Tcl。
6、【設計理念】
Redis被認為是一個存儲和緩存系統的集合體,它采用的設計思想是,數據總是從計算機主內存中進行修改和讀取,同時也以不適合隨機存取數據的格式存儲在磁盤上,這些數據只能在系統重啟后將數據重新組裝回內存中。同時,與關系型數據庫管理系統(RDBMS)相比,Redis提供的數據模型不同。用戶命令不是描述數據庫引擎要執行的查詢,而是對給定的抽象數據類型執行的具體操作。因此,數據必須以一種適合于以后快速檢索的方式進行存儲,而不需要像數據庫系統那樣以二級索引、聚合或其他傳統RDBMS的常見特性進行處理。Redis的實現中大量使用了fork系統調用,用來復制持有數據的進程,在這樣的機制下,父進程持續為客戶機提供服務,而子進程則負責在磁盤上創建一個數據副本。
6.1【數據類型】
Redis是個鍵值映射系統。Redis與其他結構化存儲系統的一個重要區別是,Redis不僅支持字符串,還支持抽象數據類型:
字符串的列表
字符串的集合(無重復無序元素的集合)。
排序的字符串集(按浮點分數排序的非重復元素的集合)。
散列表,其中的鍵和值是字符串。
自2014年4月Redis 2.8.9版本以來,HyperLogLogs用于近似集的賁度大小估計。
帶消費組的分錄流,允許你在一個單鍵上自動存儲以時間為基礎的序列,這個序列來自于多個字段和字符串值,自2018年10月Redis5.0開始,這項功能就可以使用了。
自Redis 3.2以來,通過實施geo-hash技術實現了地理空間數據的支持。
一個值的類型決定了該值可以進行哪些操作。Redis支持高級的、原子式的、服務器端的操作,如交集、聯合、集與集之間的差分以及列表和集的排序等。
基于Redis模塊API支持更多的數據類型,Redis模塊RedisJSON把ECMA-404(JSON數據交換標準)作為一種原生數據類型。
6.2、【持久性】
Redis通常將整個數據集保存在內存中。2.4以下的版本可以被配置為使用他們所說的虛擬內存,其中部分數據集存儲在磁盤上,但這個功能已經被廢棄了。Redis中的持久化可以通過兩種不同的方法來實現。首先是通過快照,即使用Redis RDB Dump文件格式,將數據集以二進制轉儲的形式從內存中異步傳輸到磁盤。另一種方法是通過日志化,即在后臺進程中,將修改數據集的每個操作的記錄添加到append-only文件(AOF)中。Redis可以在后臺重寫append-only文件,以避免日志的無限增長。日志是在1.1版本中引入的,通常被認為是比較安全的方法。
默認情況下,Redis至少每隔2秒向文件系統寫入數據一次,如果需要,可以選擇更多或更少的健壯選項。在默認設置的情況下,如果系統完全失效,只有指定的那幾秒鐘的數據會丟失。
6.3、【復制】
Redis支持主副本復制。任何Redis服務器的數據都可以復制出任意數量的副本。這使得Redis可以實現單根復制樹。Redis副本被配置為可寫入的,允許在實例之間有意和無意的不一致性。發布/訂閱功能可以在復制體的客戶端訂閱一個通道,并在復制樹上的任何地方接收發布到主服務器的消息。復制機制對于讀取的可擴展性或數據備份是有用的。
6.4、【性能】
當不需要數據的持久性時,Redis的內存內特性使其與數據庫系統相比,在性能處理上有很多優勢。因為傳統的關系型數據庫系統在考慮事務提交之前,會將每一個變化寫到磁盤上。
Redis作為一個單進程運行,在重寫AOF(append-only文件)時是單線程或雙線程的。因此,單個Redis實例不能進行并行任務,比如存儲過程等。
6.5、【集群】
Redis在2015年4月發布了3.0版本,引入了集群,集群規范實現了Redis命令子集:所有的單鍵操作都是可用的,多鍵操作(與聯合和交叉相關的命令)僅限于屬于同一節點上的鍵。
與數據庫選擇相關的操作是不可用的。
一個Redis集群可以擴展到1000個節點,可以實現?"可接受的?"寫安全,并能夠在某些節點失效時繼續工作。
7、【代碼案例】
下面我們來看些例子。
7.1、【Jedis】
Jedis是一個非常小巧靈活的Redisjava客戶端。
Jedis的設計是為了方便使用。
Jedis完全兼容Redis2.8.x、3.x.x及以上版本*。
7.1.1、【配置】
7.1.2、【安裝】
你需要安裝并啟動最新版本的Redis。
Jedis?jedis?=?new?Jedis();
默認的構造函數就可以正常工作,除非你用的是非默認端口或在遠程機器上啟動了服務,在這種情況下,你可以通過在構造函數中傳遞正確的參數值來正確配置。
7.1.3、【數據類型案例】
7.1.3.1、【字符串】
字符串是最基本的Redis值的一種,當你需要持久化簡單的key-value數據類型時,字符串很有用。
jedis.set("events/city/rome", "32,15,223,828"); String cachedResponse = jedis.get("events/city/rome");
7.1.3.2、【列表】
Redis列表是簡單的字符串列表,按插入順序排序,使其成為實現消息隊列等的理想工具。
jedis.lpush("queue#tasks", "firstTask"); jedis.lpush("queue#tasks", "secondTask"); String task = jedis.rpop("queue#tasks");
7.1.3.3、【集合】
Redis集是一個無序的字符串集合,可以用來排除重復的成員。
jedis.sadd("nicknames", "nickname#1"); jedis.sadd("nicknames", "nickname#2"); jedis.sadd("nicknames", "nickname#1"); Set
7.1.3.4、【哈希】
Redis Hashes是字符串字段和字符串值之間的映射。
jedis.hset("user#1", "name", "Peter"); jedis.hset("user#1", "job", "politician"); String name = jedis.hget("user#1", "name"); Map
7.1.3.5、【有序的集合】
Map
7.1.4、【事務】
事務保證了原子性和線程安全操作。
String friendsPrefix = "friends#"; String userOneId = "4352523"; String userTwoId = "5552321"; Transaction t = jedis.multi(); t.sadd(friendsPrefix + userOneId, userTwoId); t.sadd(friendsPrefix + userTwoId, userOneId); t.exec();
7.1.5、【管線】
當我們需要發送多條命令時,我們可以將多條命令打包成一條請求,通過使用流水線來節省連接開銷,這本質上是一種網絡優化。只要這些操作是相互獨立的,我們就可以使用這種技術。
Pipeline p = jedis.pipelined(); p.sadd("searched#" + userOneId, "paris"); p.zadd("ranking", 126, userOneId); p.zadd("ranking", 325, userTwoId); Response
7.1.6、【發布/訂閱】
我們可以使用Redis消息代理功能,在系統的不同組件之間發送消息。要確保訂閱者和發布者線程不共享同一個Jedis連接。
7.1.6.1、【訂閱者】
Jedis jSubscriber = new Jedis(); jSubscriber.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { // 處理消息 } }, "channel");
7.1.6.2、【發布者】
Jedis jPublisher = new Jedis(); jPublisher.publish("channel", "test message");
7.1.7、【連接池】
在實際的處理場景中,我們需要使用連接池來連接Redis。連接池是線程安全的。
final JedisPoolConfig poolConfig = buildPoolConfig(); JedisPool jedisPool = new JedisPool(poolConfig, "localhost"); private JedisPoolConfig buildPoolConfig() { final JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(128); poolConfig.setMaxIdle(128); poolConfig.setMinIdle(16); poolConfig.setTestOnBorrow(true); poolConfig.setTestOnReturn(true); poolConfig.setTestWhileIdle(true); poolConfig.setMinEvictableIdleTimeMillis(Duration.ofSeconds(60).toMillis()); poolConfig.setTimeBetweenEvictionRunsMillis(Duration.ofSeconds(30).toMillis()); poolConfig.setNumTestsPerEvictionRun(3); poolConfig.setBlockWhenExhausted(true); return poolConfig; }
由于連接池是線程安全的,所以可以將其靜態存儲在某個地方,但應該避免應用程序關閉時發生泄漏,所以要記著銷毀連接池。
我們可以在需要的時候,從應用程序的任何地方使用它。
try (Jedis jedis = jedisPool.getResource()) { // 用jedis資源進行操作 }
7.1.8、【Redis集群】
關于Redis集群的配置可以參考官方文檔。
https://redis.io/topics/cluster-tutorial
我們只需要提供其中一個主實例的主機和端口信息,它就會自動發現集群中的其他實例。
try (JedisCluster jedisCluster = new JedisCluster(new HostAndPort("localhost", 6379))) { // 使用jedisCluster資源,跟一個普通的Jedis資源一樣。 } catch (IOException e) {}
7.1.9、【Jedis官方網站】
https://github.com/xetorthio/jedis
7.2、【Redisson】
Redisson?是一個RedisJava?客戶端,具有內存中數據網格的特點。
它是基于高性能異步處理的Java Redis客戶端和Netty框架。
Java JDK兼容性是:1.8- 14, Android
7.2.1、【安裝/配置】
7.2.2、【列表】
// 缺省連接到 127.0.0.1:6379 RedissonClient redisson = Redisson.create(); // 獲取 java.util.List RList
7.2.3、【映射】
// 缺省連接到 127.0.0.1:6379 RedissonClient redisson = Redisson.create(); // 獲取 java.util.concurrent.ConcurrentMap RMap
7.2.4、【鎖】
// 缺省連接到 127.0.0.1:6379 RedissonClient redisson = Redisson.create(); // 獲取java.util.concurrent.locks.Lock RLock lock = redisson.getLock("lock"); lock.lock(); System.out.println("lock aquired"); Thread t = new Thread() { public void run() { RLock lock1 = redisson.getLock("lock"); lock1.lock(); System.out.println("lock aquired by thread"); lock1.unlock(); System.out.println("lock released by thread"); }; }; t.start(); t.join(1000); lock.unlock(); System.out.println("lock released"); t.join(); redisson.shutdown();
7.2.5、【原子操作】
// 缺省連接到 127.0.0.1:6379 RedissonClient redisson = Redisson.create(); RAtomicLong atomicLong = redisson.getAtomicLong("myLong"); System.out.println("Init value: " + atomicLong.get()); atomicLong.incrementAndGet(); System.out.println("Current value: " + atomicLong.get()); atomicLong.addAndGet(10L); System.out.println("Final value: " + atomicLong.get()); redisson.shutdown();
7.2.6、【Redisson官方網站】
https://github.com/redisson/redisson
8、【場景案例】
由于Redis數據庫設計的特點,比較典型的用例有會話緩存、全頁緩存、消息隊列應用、排行榜和計數等,
Twitter等大公司都在使用Redis,
Amazon Web Services在其產品組合中提供了Redis服務,
微軟在Azure中提供了Redis服務,
阿里巴巴在阿里云中提供了基于Redis的ApsaraDB。
9、【小結】
我們在本文中通過對Redis系統的設計理念,歷史,與其他數據庫的區別等方面分析和學習了Redis,希望對各位朋友有幫助。
歡迎討論。
【參考】
https://en.wikipedia.org/wiki/Redis
Redis
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。