學(xué)習(xí)一門技術(shù),我們首先得明白以下幾點:
它是什么?
它為什么會出現(xiàn)?
它的出現(xiàn)解決了什么問題?
如何使用?
帶著這幾個問題去學(xué),我們才能將它的衣服一件件的扒光,最后看到它的本質(zhì)。不然面試的時候面試官稍微問得深入一點就涼涼了。接下來聊聊NoSql。
一、什么是NoSql? NoSql意思是Not Only Sql,不僅僅是SQL。NoSql也被稱作非關(guān)系型數(shù)據(jù)庫,那么與之相對的就是關(guān)系型數(shù)據(jù)庫。你去百度“什么叫關(guān)系型數(shù)據(jù)庫”,搜到的是非常官方的答案,說是依據(jù)關(guān)系模型來創(chuàng)建的數(shù)據(jù)庫??戳税胩?,所有字都認識,但是連在一起就不知道是什么意思了。這里不搞那些花里胡哨的,用最簡單的話說明白:
關(guān)系型數(shù)據(jù)庫:以數(shù)據(jù)表來存儲數(shù)據(jù),一個pojo對應(yīng)一張表,表中的一行就是pojo的一個對象,一列就是對象的一個屬性,表與表之間的關(guān)聯(lián)代表對象之間的一對一、一對多和多對多的關(guān)系。通過Sql對數(shù)據(jù)表中的數(shù)據(jù)進行操作。
非關(guān)系型數(shù)據(jù)庫:上面說明白了關(guān)系型數(shù)據(jù)庫,那么非關(guān)系型數(shù)據(jù)庫就好理解了。沒有那些對應(yīng)關(guān)系,一般是以鍵值對的形式存儲,不需要通過Sql來進行操作。其實NoSql數(shù)據(jù)庫也不是全都用鍵值對的形式存儲數(shù)據(jù),主要有以下四類:
鍵值存儲型(Redis)
文檔型(MongoDB)
列儲存(Hbase)
圖關(guān)系數(shù)據(jù)庫(Neo4J)
二、NoSql為何會興起? 在此之間,先來說說互聯(lián)網(wǎng)架構(gòu)的演變(數(shù)據(jù)庫層面)。
單機MySql:互聯(lián)網(wǎng)剛興起的時候,由于使用的人數(shù)不多,一個網(wǎng)站就是我們IDE中的一個工程,然后發(fā)布到服務(wù)器上,數(shù)據(jù)庫也就一個MySql就夠了。
到后來,用的人多了,數(shù)據(jù)的讀取越來越慢,就在數(shù)據(jù)庫的前面擋了一層緩存,dao層訪問的是緩存,而不是直接捅到數(shù)據(jù)庫中取。當(dāng)時緩存用得多的是Memcached。
再后來,數(shù)據(jù)庫的寫入操作越來越多了,由于緩存只能解決讀取壓力,所以它又行不通了。所以就出現(xiàn)了讀寫分離。dao層訪問緩存,緩存連接著主庫和從庫,寫操作在主庫進行,主庫再同步到從庫,讀操作在從庫進行,這樣來達到讀寫分離的目的。
隨著訪問量越來越大,讀寫分離也搞不定了,就出現(xiàn)了分庫分表和數(shù)據(jù)庫集群等技術(shù)。
到今天,互聯(lián)網(wǎng)用戶人數(shù)已經(jīng)不是一般的多了,有海量的數(shù)據(jù),如果要對海量的數(shù)據(jù)進行挖掘,關(guān)系型數(shù)據(jù)庫就不適用了。所以NoSql就閃亮登場了。本文將介紹用得比較多的NoSQL數(shù)據(jù)庫 --- Redis。
三、NoSQL之Redis 關(guān)于redis的安裝、五種數(shù)據(jù)類型以及jedis我在之前的文章中有說過,這里來聊聊剩下的內(nèi)容。1、發(fā)布與訂閱:
什么是發(fā)布與訂閱: 發(fā)布與訂閱,就類似于微信公眾號。微信公眾號發(fā)布了一條消息,所有關(guān)注了這個公眾號的人都能夠收到消息。了解過MQ的人也許會問,MQ也有類似的功能,那么它們之間有什么區(qū)別呢?形象地說就是,Redis是兼職,MQ是專職的。MQ它是實現(xiàn)了JMS規(guī)范的專門處理消息的消息中間件,其功能遠比Redis提供的發(fā)布與訂閱強大且復(fù)雜得多,后續(xù)我也會專門說一說MQ,歡迎大家關(guān)注。
怎么用? 其實特別簡單,新建兩個類,一個訂閱者一個發(fā)布者。發(fā)布者調(diào)用publish方法,指定發(fā)布的channel和消息內(nèi)容,而發(fā)布者只需繼承JedisPubSub類,重寫一些方法(主要是為了能夠打印訂閱情況,為了避免每個訂閱者都重寫這些方法,自己寫一個類去重寫這些方法,然后訂閱者繼承自己這個類就行了),然后調(diào)用subscribe方法,指定訂閱者和channel即可訂閱。代碼如下:工具類:
1 public class SubUtil extends JedisPubSub { 2 @Override 3 public void onMessage (String channel, String message) { 4 System.out.println(String.format("receive redis published message, channel %s, message %s" , channel, message)); 5 } 6 7 @Override 8 public void onSubscribe (String channel, int subscribedChannels) { 9 System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d" ,10 channel, subscribedChannels));11 }12 13 @Override 14 public void onUnsubscribe (String channel, int subscribedChannels) {15 System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d" ,16 channel, subscribedChannels));17 }18 }
訂閱者:
1 /** 2 * 訂閱者(先啟動訂閱者,再啟動發(fā)布者) 3 */ 4 public class SubOne extends SubUtil { 5 public static void main (String[] args) throws Exception { 6 Jedis jedis = new Jedis("192.168.x.xx" , 6379 ); 7 SubOne subOne = new SubOne(); 8 jedis.subscribe(subOne, "test" ); 9 System.in.read();10 }11 }
發(fā)布者:
1 public class Pub {2 public static void main (String[] args) throws Exception {3 Jedis jedis = new Jedis("192.168.2.43" ,6379 );4 long result = jedis.publish("test" ,"hello" );5 }6 }
這樣就搞定了發(fā)布與訂閱,注意,先啟動訂閱者,再啟動發(fā)布者 。按順序啟動后會看到如下運行結(jié)果:
運行結(jié)果 2、管道技術(shù): 在說管道技術(shù)之前先說說TCP請求響應(yīng)模型。
女朋友:你能給我解釋一下什么是TCP么?
程序員:我開始解釋了:你想聽我解釋一下TCP 么?
女朋友:嗯,我想聽你解釋一下TCP 。
程序員:好的,我會給你解釋一下什么是TCP 。
女朋友:好的,我會聽你解釋一下什么是TCP 。
程序員:你準(zhǔn)備好聽我解釋一下什么是TCP了嗎 ?
女朋友:嗯,我準(zhǔn)備好聽你解釋一下什么是TCP了。
程序員:Ok,那我要開始解釋什么是 TCP 了。大概要 10 秒,20 個字。
女朋友:Ok,我準(zhǔn)備收你那個 10 秒時長,20 個字的解釋了。
系統(tǒng)提示:抱歉,連接超時了……
以上就是TCP請求響應(yīng)模型(本故事純屬虛構(gòu),程序員哪來的女朋友??)。
redis其實也是這種請求響應(yīng)模型的服務(wù),客戶端向服務(wù)端發(fā)送一個命令,等待服務(wù)端的返回;服務(wù)端接收到命令進行執(zhí)行,然后將結(jié)果返回給客戶端。在這個回合中,服務(wù)端是接收不了其他命令的。假如一個回合需要250毫秒,即使服務(wù)端一次性能接收100k的請求數(shù),那么1秒鐘也只能處理4個請求。使用管道技術(shù)就可以解決這種問題。使用管道就相當(dāng)于可以并發(fā)處理,客戶端不用等待服務(wù)端的響應(yīng),繼續(xù)發(fā)起下一個請求。
那么jedis如何使用管道技術(shù)呢?請看下面的代碼:
1 public static void main (String[] args ) { 2 Jedis jedis = new Jedis("192.168.X.XX" ,6379 ); 3 // 未使用管道技術(shù) 4 long start1 = System.currentTimeMillis(); 5 for (int i = 1 ; i <= 10000 ; i++) { 6 jedis.set (UUID.randomUUID().toString(), String.valueOf(i)); 7 } 8 long end1 = System.currentTimeMillis(); 9 System.out .println("未使用管道技術(shù)插入10000條數(shù)據(jù)耗時:" + (end1 - start1) + "毫秒" );10 11 // 使用管道技術(shù) 12 Pipeline pipeline = jedis.pipelined();13 long start2 = System.currentTimeMillis();14 for (int i = 1 ; i <= 10000 ; i++) {15 pipeline.set (UUID.randomUUID().toString(), String.valueOf(i));16 }17 long end2 = System.currentTimeMillis();18 System.out .println("使用管道技術(shù)插入10000條數(shù)據(jù)耗時:" + (end2 - start2) + "毫秒" );19 }
看運行結(jié)果:
運行結(jié)果 不得不說這管道技術(shù)還是有兩把刷子的,這耗時相差將近40倍。3、為key設(shè)置過期時間: 我們寫入redis中的內(nèi)容,可以對key設(shè)置過期時間。設(shè)置key的過期時間,超過時間后,將會自動刪除該key(只有執(zhí)行對key值有影響的操作時才會清除)。設(shè)置過期時間的方法是expire,如下:
1 jedis .expire("hash" ,30);
這就是將"hash"這個key設(shè)置為30秒后過期。 通過persist方法可以清除過期。
1 jedis .persist("hash" );
這就是將"hash"這個key過期清除掉,也就是相當(dāng)于上面設(shè)置的過期無效了。 如果對一個設(shè)置了過期時間的key再次設(shè)置過期時間,那么將刷新這個過期時間。比如“hash”這個key我設(shè)置了30秒過期,當(dāng)過了24秒的時候我再次使用expire方法設(shè)置它的過期時間為60秒,那么這個key的離過期又還有60秒的時間。
4、redis的事務(wù): redis的事務(wù),本質(zhì)是一組命令的集合。所有的命令都會序列化,然后按順序串行地執(zhí)行。事務(wù)開啟后,所有的命令先入隊,提交事務(wù)的時候,要么全執(zhí)行,要么全不執(zhí)行。常用命令如下: 命令|作用 :-:|:-: multi|開啟事務(wù) exec|提交事務(wù) discard|取消事務(wù) watch key…|監(jiān)視一個或多個key,如果事務(wù)提交前這些key被改動,事務(wù)將被打斷 unwatch|取消對所有key的監(jiān)視 下面來使用一下事務(wù):
正常情況 這種是正常執(zhí)行,如果最后不是exec,而是discard,那就是放棄事務(wù)了。接下來看看下面這種情況:事務(wù)中有語法錯誤 set k3的時候,小手一抖,寫成了sete,最后發(fā)現(xiàn)事務(wù)提交不了,所有的命令都未執(zhí)行。這種就相當(dāng)于java中的編譯時期就報錯了,所以肯定是提交不了的。接下來再看另外一種情況:
運行時異常 開啟事務(wù)后set了一個String類型的k1,然后讓其自增,再set其他的key,最后提交。結(jié)果是只有自增的那條命令執(zhí)行報錯,其他的正常執(zhí)行。最后來看看事務(wù)中的watch和unwatch命令。watch相當(dāng)于樂觀鎖,如果watch的key在事務(wù)提交前被修改了,那么事務(wù)就會提交失敗,得重新watch,獲取到最新值(watch應(yīng)該在事務(wù)開啟之前)。
watch 我開了兩個窗口,watch的k1初始值是100,然后在事務(wù)中改成200,在事務(wù)提交之前,另外一邊將其改成了50,現(xiàn)在提交事務(wù)就會出現(xiàn)如下結(jié)果:
結(jié)果 這時候再重新開啟事務(wù)去修改,如果這個過程沒有再被修改,事務(wù)才能提交成功,這就類似于CAS。5、redis執(zhí)行txt文件中的命令: 如果我們需要執(zhí)行的redis命令很多,可以寫到一個txt文件中,然后讀取這個txt文件即可執(zhí)行里面的命令。具體步驟如下:
1 set name tom2 set age 20 3 set sex man4 set phone 8008208820 5 set country china
1 unix2dos redispipedata .txt
如果找不到命令,yum install 一下即可。
1 cat /srv/redispipedata.txt | ./redis-cli
` 即可批量執(zhí)行txt文件中的命令了,執(zhí)行后可以看到如下結(jié)果:
執(zhí)行結(jié)果 上面介紹了管道技術(shù),其實執(zhí)行txt文件中的命令也可以使用管道技術(shù),前面的步驟都一樣,只不過是執(zhí)行命令改成為:
1 cat /srv/redispipedata.txt | ./redis-cli --pipe
執(zhí)行成功的話會看到如下結(jié)果:
執(zhí)行結(jié)果 6、redis的持久化: 接觸過redis的童鞋一定聽過RDB和AOF這兩個詞,這是redis持久化的兩種方式,下面就介紹一下RDB和AOF。
RDB(redis database)介紹: RDB就是在指定時間間隔內(nèi)將redis中的數(shù)據(jù)進行快照存儲,默認情況下會將數(shù)據(jù)寫到dump.rdb文件中。保存RDB文件時redis會fork出一個子進程來操作,所以對redis的性能影響很小。但是,由于是隔一段時間保存一次,所以可能會造成一段時間內(nèi)的數(shù)據(jù)丟失。假如配置的是每個5分鐘保存一次,10點鐘的時候保存了一次,10點零4分的時候redis掛掉了,那么10點到10點零4分這段時間的數(shù)據(jù)就丟失了。
AOF(append only file)介紹: 為什么有了RDB還會出現(xiàn)AOF?就是因為上面說的,10點到10點零4分這段時間的數(shù)據(jù)會丟失,所以AOF就出現(xiàn)了。AOF會記錄redis每次寫操作,追加到aof文件中。當(dāng)redis重啟的時候就會重新執(zhí)行記錄的這些操作來恢復(fù)數(shù)據(jù)。由于每次記錄寫操作都是追加到aof文件中,為了避免AOF文件體積過大,redis還可以對AOF文件進行重寫,也就是去除重復(fù)的操作,重寫完成后,就會立即切換到新的AOF文件,此后的操作都會記錄到新的AOF文件中。用戶可以自己選擇AOF持久化的策略,策略有不同步、每秒鐘同步一次和每次進行寫操作的時候同步。默認是每秒鐘同步一次。所以,采用默認策略的話,最多也就丟失這1秒鐘內(nèi)的寫操作。
這兩種持久化方式原理其實都是利用了寫時復(fù)制,redis搞出兩個進程,父進程與子進程,RDB方式的時候,子進程以快照形式保存數(shù)據(jù),AOF方式的時候,子進程記錄寫操作。
1 config get dir
命令可以查看dump.rdb文件所在的位置。
被動保存: 在redis.conf中,有一項配置叫SNAPSHOTTING ,這里就是關(guān)于持久化的一些配置。
rdb的配置 這三行配置分別表示:15分鐘內(nèi)改了1次;5分鐘內(nèi)改了10次;1分鐘內(nèi)改了1萬次。只要滿足這三條中的任意一條,就會將數(shù)據(jù)保存到dump.rdb中。(測試的時候可以先刪除掉dump.rdb文件,然后5分鐘內(nèi)讓10個key發(fā)生改變,看看是否重新生成了dump.rdb,然后直接讓redis shutdown,然后重新啟動,keys * 看看之前設(shè)置的key是否還在)。主動保存: 假如你設(shè)置的某個key十分重要,你想設(shè)置完就寫到rdb文件中,那么可以在redis-cli中設(shè)置完后用save命令。
主動保存 可以看到,AOF默認是關(guān)閉的,默認文件名為appendonly.aof。上面說到過AOF有三種同步策略,請看下圖,確實是3種,我不是吹的。
AOF同步策略 所以,要使用AOF只需要把上面的no改成yes,然后選擇策略就可以了?,F(xiàn)在的問題是,如果同時存在RDB文件和AOF文件,那么redis啟動的時候根據(jù)誰來恢復(fù)?測試的辦法:vim打開appendonly.aof文件,在末尾加上一些亂七八糟的不正確的指令,保存退出。然后再啟動redis,如果redis能夠正常啟動,說明啟動時是優(yōu)先根據(jù)RDB來恢復(fù)的,如果不能正常啟動,說明優(yōu)先根據(jù)AOF來恢復(fù)。接下來就開打:
同時存在RDB和AOF 將appendonly.aof亂改一通:
aof文件 然后啟動redis:
啟動redis 結(jié)果報錯了,說明啟動時優(yōu)先根據(jù)AOF來恢復(fù)數(shù)據(jù)的。AOF文件中有不可執(zhí)行的命令,redis啟動就會報錯,那么怎么修復(fù)呢?不要告訴我你打算手動的去將AOF文件中那些不可執(zhí)行的命令刪掉,萬一你手一抖多刪了怎么辦。我們看看redis的src目錄:
src目錄 沒錯,就是這兩個文件,一個是修復(fù)RDB文件的,一個是修復(fù)AOF文件的。在src目錄下執(zhí)行
1 ./redis-check -aof --fix appendonly.aof
就可以修復(fù)了。
在上面AOF的介紹中提到了會對aof文件進行壓縮重寫,那么什么時候會觸發(fā)重寫呢?看看配置文件中的相關(guān)配置:
重寫策略 這兩行配置的意思是:當(dāng)文件大小大于上次rewrite時文件大小的100%且大于64MB時進行重寫。其實如果一個公司如果大規(guī)模使用redis的話,rewrite-min-size 至少3GB起步。
redis的持久化小總結(jié): RDB方式性能更好,但是數(shù)據(jù)完整性不如AOF,所以如果對數(shù)據(jù)完整性要求不高,使用RDB即可;如果對數(shù)據(jù)完整性要求高,那么請同時使用RDB和AOF;如果數(shù)據(jù)量大,使用AOF會進行大量的IO操作,對性能的影響十分明顯,那么就使用接下來要介紹的主從復(fù)制。
7、redis主從: 所謂主從,就是主從復(fù)制,主機數(shù)據(jù)更新后自動同步到從機的機制,Master以寫為主,Slave以讀為主。主從可以實現(xiàn)讀寫分離以及容災(zāi)恢復(fù)。主從可以分為三種模式,一主二從、薪火相傳和反客為主(這里的二是泛指,其實是一主n從)。
(1)、主從之一主二從
首先拷貝三份redis.conf,分別重命名為redismaster.conf、redisslave1.conf和redisslave2.conf。然后要做如下的修改: ---- 開啟daemonize yes; ---- 設(shè)置pid文件名字,pidfile /var/run/redisxxxx.pid; ---- 指定端口,port xxxx; ---- 設(shè)置log文件名字,logfile "xxxx.log"; ---- 設(shè)置dump.rdb文件名字,dbfilename dumpxxxx.rdb。 (xxxx是端口號,三份配置文件端口可以分別為6379、6380、6381)
修改好三份配置文件,接下來就配置一主二仆。 首先啟動這三個redis,然后在redis-cli輸入info replication
命令,可以看到下圖信息:
info 可以看到,目前啟動的三個redis的角色都是master,slave都是0。接下來,在6379的redis中set幾個值,然后在6380和6381的redis中輸入
1 slaveof 127.0 .0 .1 6379
接下來你就會發(fā)現(xiàn),6379的redis中set的值在另外兩臺機器中也有。
一主二仆 這里我先set 了k1,然后再將6380和6381slaveof 6379的,最后發(fā)現(xiàn)get k1也是可以取到值。說明從機會把主機所有數(shù)據(jù)到拉到從機上,包括監(jiān)視主機之前主機上的數(shù)據(jù)。另外,從機是不能set 值的,讀寫分離,主機負責(zé)寫,從機是不能寫的,在從機set 值會報如下錯誤:
從機不能寫數(shù)據(jù) 完成了一主二從的配置,看看下面幾個問題:
主機掛了,從機上位變主機還是原地待命?
主機掛了 可以看到,主機掛了,從機還是從機,沒有造反。
可以看到,原先的主從體系并沒有土崩瓦解,還可以用。
從機掛了再啟動,從機就不再是從機了,而變成了另外一個master。所以用slaveof命令配置的從機重啟后需要重新配置。
(2)、主從之薪火相傳: 上面的一主N從,只有一個boss,所有的小弟都直接跟boss打交道,小弟一多boss也會很累的。所以就有了薪火相傳這種模式。6379是主機,6380認6379做大哥,6381認6380做大哥,就這樣一脈相承。
薪火相傳 將6381這臺從機掛在6380下,這就是薪火相傳。
(3)、主從之反客為主: 一主二從中說到過,主機掛了,從機原地待命,而反客為主就是主機掛了,從機上位了。要上位的從機執(zhí)行如下命令即可:
1 slaveof no one
然后將另外一臺從機掛在反客為主的這臺機器下,那么原先的這兩臺從機就變成了一個新的一主一從體系。即使原先的主機回來了,也跟這個一主一從沒關(guān)系了。
8、哨兵模式: 上面講了主從的三種模式,那么哨兵又是什么鬼?說白了,哨兵模式就是上面“反客為主”的自動版。上面說的“反客為主”,主機掛了需要手動地將從機設(shè)置為主機,哨兵模式就不需要手動設(shè)置了。哨兵模式相當(dāng)于有一個哨兵在巡邏,一旦發(fā)現(xiàn)主機掛了,就會以投票的方式立即選出新的老大。
1 #監(jiān)控主機,主機名,主機ip,主機端口,1表示投票,誰的票數(shù)多就晉升為主機 2 sentinel monitor host6379 127.0.0.1 6379 1 3 #后臺啟動 4 daemonize yes
然后啟動哨兵,在redis的src目錄下執(zhí)行如下命令:
1 ./redis-sentinel /etc/sentinel.conf
啟動成功后,再讓主機掛掉,等幾秒鐘,就會發(fā)現(xiàn)已經(jīng)自動選出新的主機了。
哨兵 可以看到,主機掛了之后,哨兵自動選出了6381為新的主機,6380也跟著6381混了。如果6379又復(fù)活了,它會繼續(xù)擔(dān)任6380和6381的老大呢還是與6380和6381沒關(guān)系了?還是成了6381的小弟?看圖說話:哨兵 6379不再是主,成為了6381的小弟,6381反客為主成功。