Redis
1.介紹
Redis是當(dāng)前比較熱門的NOSQL系統(tǒng)之一,它是一個開源的使用ANSI c語言編寫的key-value存儲系統(tǒng)(區(qū)別于MySQL的二維表格的形式存儲占拍。)。和Memcache類似捎迫,但很大程度補償了Memcache的不足晃酒。和Memcache一樣,Redis數(shù)據(jù)都是緩存在計算機內(nèi)存中窄绒,不同的是贝次,Memcache只能將數(shù)據(jù)緩存到內(nèi)存中,無法自動定期寫入硬盤彰导,這就表示蛔翅,一斷電或重啟,內(nèi)存清空位谋,數(shù)據(jù)丟失山析。所以Memcache的應(yīng)用場景適用于緩存無需持久化的數(shù)據(jù)。而Redis不同的是它會周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件掏父,實現(xiàn)數(shù)據(jù)的持久化笋轨。
1.1 特點
Redis讀取的速度是110000次/s,寫的速度是81000次/s
原子 赊淑。Redis的所有操作都是原子性的爵政,同時Redis還支持對幾個操作全并后的原子性執(zhí)行。
支持多種數(shù)據(jù)結(jié)構(gòu):string(字符串)陶缺;list(列表)钾挟;hash(哈希),set(集合)组哩;zset(有序集合)
持久化等龙,主從復(fù)制(集群)
支持過期時間处渣,支持事務(wù),消息訂閱蛛砰。
官方不支持window,但是有第三方版本罐栈。
1.2 Redis與memcache
/*
1、Redis和Memcache都是將數(shù)據(jù)存放在內(nèi)存中泥畅,都是內(nèi)存數(shù)據(jù)庫荠诬。不過memcache還可用于緩存其他東西,例如圖片位仁、視頻等等柑贞。
2、Redis不僅僅支持簡單的k/v類型的數(shù)據(jù)聂抢,同時還提供list钧嘶,set,hash等數(shù)據(jù)結(jié)構(gòu)的存儲琳疏。
3有决、虛擬內(nèi)存--Redis當(dāng)物理內(nèi)存用完時,可以將一些很久沒用到的value 交換到磁盤
4空盼、過期策略--memcache在set時就指定书幕,例如set key1 0 0 8,即永不過期。Redis可以通過例如expire 設(shè)定揽趾,例如expire name 10
5台汇、分布式--設(shè)定memcache集群,利用magent做一主多從;redis可以做一主多從篱瞎。都可以一主一從
6苟呐、存儲數(shù)據(jù)安全--memcache掛掉后,數(shù)據(jù)沒了奔缠;redis可以定期保存到磁盤(持久化)
7掠抬、災(zāi)難恢復(fù)--memcache掛掉后,數(shù)據(jù)不可恢復(fù); redis數(shù)據(jù)丟失后可以通過aof恢復(fù)
8校哎、Redis支持?jǐn)?shù)據(jù)的備份两波,即master-slave模式的數(shù)據(jù)備份。
*/
2.使用
將一些數(shù)據(jù)在短時間之內(nèi)不會發(fā)生變化闷哆,而且它們還要被頻繁訪問腰奋,為了提高用戶的請求速度和降低網(wǎng)站的負(fù)載,降低數(shù)據(jù)庫的讀寫次數(shù)抱怔,就把這些數(shù)據(jù)放到緩存中劣坊。
頁面緩存:第一次從數(shù)據(jù)庫中讀取,然后生成一個靜態(tài)頁面屈留,以后所有的讀取局冰,只加載這個靜態(tài)頁面就可以了测蘑。
數(shù)據(jù)緩存:由于一個頁面有幾種需要從不同的緩存中讀取數(shù)據(jù)的模塊,所以不適合使用頁面緩存康二。
/*
將redis當(dāng)做緩存使用LRU算法的緩存來使用碳胳;LRU是Redis唯一支持的回收方法。
?
如果你想把Redis當(dāng)做一個緩存來用沫勿,所有的key都有過期時間挨约,那么你可以考慮 使用以下設(shè)置(假設(shè)最大內(nèi)存使用量為2M):
?
maxmemory 2mb #maxmemory配置指令用于配置Redis存儲數(shù)據(jù)時指定限制的內(nèi)存大小。
maxmemory-policy allkeys-lru #設(shè)置maxmemory為0代表沒有內(nèi)存限制产雹。
?
以上設(shè)置并不需要我們的應(yīng)用使用EXPIRE(或相似的命令)命令去設(shè)置每個key的過期時間诫惭,因為 只要內(nèi)存使用量到達(dá)2M,Redis就會使用類LRU算法自動刪除某些key蔓挖。
?
相比使用額外內(nèi)存空間存儲多個鍵的過期時間夕土,使用緩存設(shè)置是一種更加有效利用內(nèi)存的方式。而且相比每個鍵固定的 過期時間瘟判,使用LRU也是一種更加推薦的方式隘弊,因為這樣能使應(yīng)用的熱數(shù)據(jù)(更頻繁使用的鍵) 在內(nèi)存中停留時間更久。
?
基本上這么配置下的Redis可以當(dāng)成memcached使用。
?
當(dāng)我們把Redis當(dāng)成緩存來使用的時候云石,如果應(yīng)用程序同時也需要把Redis當(dāng)成存儲系統(tǒng)來使用坟岔,那么強烈建議 使用兩個Redis實例。一個是緩存滋戳,使用上述方法進行配置,另一個是存儲,根據(jù)應(yīng)用的持久化需求進行配置陕壹,并且 只存儲那些不需要被緩存的數(shù)據(jù)。
3.集群
3.1 特點
多個redis節(jié)點網(wǎng)絡(luò)互聯(lián)树埠,數(shù)據(jù)共享
所有的節(jié)點都是一主一從(可以是多個從)糠馆,其中從不提供服務(wù),僅作為備用
不支持同時處理多個鍵(如mset/mget)怎憋,因為redis需要把鍵均勻分布在各個節(jié)點上又碌,并發(fā)量很高的情況下同時創(chuàng)建鍵值會降低性能并導(dǎo)致不可預(yù)測的行為。
支持在線增加绊袋、刪除節(jié)點
客戶端可以連任何一個主節(jié)點進行讀寫
redis.conf
port 7001 #端口
cluster-enabled yes #啟用集群模式
cluster-config-file nodes.conf
cluster-node-timeout 5000 #超時時間
appendonly yes
daemonize yes #后臺運行
protected-mode no #非保護模式
pidfile /var/run/redis_7001.pid
JedisPoolConfig poolConfig = new JedisPoolConfig();
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
HostAndPort hostAndPort = new HostAndPort("192.168.1.99", 7000);
HostAndPort hostAndPort1 = new HostAndPort("192.168.1.99", 7001);
HostAndPort hostAndPort2 = new HostAndPort("192.168.1.99", 7002);
HostAndPort hostAndPort3 = new HostAndPort("192.168.1.99", 7003);
HostAndPort hostAndPort4 = new HostAndPort("192.168.1.99", 7004);
HostAndPort hostAndPort5 = new HostAndPort("192.168.1.99", 7005);
nodes.add(hostAndPort);
nodes.add(hostAndPort1);
nodes.add(hostAndPort2);
nodes.add(hostAndPort3);
nodes.add(hostAndPort4);
nodes.add(hostAndPort5);
JedisCluster jedisCluster = new JedisCluster(nodes, poolConfig);//JedisCluster中默認(rèn)分裝好了連接池.
// redis內(nèi)部會創(chuàng)建連接池毕匀,從連接池中獲取連接使用,然后再把連接返回給連接池
String string = jedisCluster.get("a");
System.out.println(string);
集群優(yōu)點: 1.主從備份癌别,防止主機宕機皂岔。 2.讀寫分離,分擔(dān)master的任務(wù)展姐。 3.任務(wù)分離躁垛,分擔(dān)工作與計算剖毯。 ------------------------------------------------------------ 方案: 1.master下面有兩個孩子,兩個孩子直接指向master(樹). 2.線性教馆,第一個孩子指向master,第二個孩子指向第一個孩子.
原理: 主從通信:slave啟動之后鏈接master,slave發(fā)出同步命令逊谋,save拿到dump出的rdb 快照,之后再拿緩沖aof隊列中的日志數(shù)據(jù)進行數(shù)據(jù)填充活玲。
---master配置: 1.關(guān)閉rdb快照(備份工作交給slave) 2.可以開啟aof
slave配置: 1.聲明slave-of (slave-of localhsot 6379) 2.如果master有密碼涣狗,slave也要有密碼 3.打開rdb快照 4.配置是否只讀[salve-read-only]
---主; passwordrequire 密碼 --slave masterauth 密碼
=============主從缺點: 每次slave斷開后舒憾,無論是主動斷開還是網(wǎng)絡(luò)故障,再鏈接master, 都要master全部dump導(dǎo)出rdb再aof. 所以,啟動slave要一個一個啟動,且有時間間隔。要不然 master主IO劇增。
4.注冊中心
使用 Redis 的 Key/Map 結(jié)構(gòu)存儲數(shù)據(jù)結(jié)構(gòu):
主 Key 為服務(wù)名和類型
Map 中的 Key 為 URL 地址
Map 中的 Value 為過期時間,用于判斷臟數(shù)據(jù),臟數(shù)據(jù)由監(jiān)控中心刪除 3
使用 Redis 的 Publish/Subscribe 事件通知數(shù)據(jù)變更:
通過事件的值區(qū)分事件類型:register, unregister, subscribe, unsubscribe
普通消費者直接訂閱指定服務(wù)提供者的 Key笼踩,只會收到指定服務(wù)的 register, unregister 事件
監(jiān)控中心通過 psubscribe 功能訂閱 /dubbo/挟冠,會收到所有服務(wù)的所有變更事件*
調(diào)用過程:
1.服務(wù)提供方啟動時控淡,向 Key:/dubbo/com.foo.BarService/providers 下凭戴,添加當(dāng)前提供者的地址
2.并向 Channel:/dubbo/com.foo.BarService/providers 發(fā)送 register 事件
3.服務(wù)消費方啟動時者冤,從 Channel:/dubbo/com.foo.BarService/providers 訂閱 register 和 unregister 事件
4.并向 Key:/dubbo/com.foo.BarService/providers 下拜银,添加當(dāng)前消費者的地址
5.服務(wù)消費方收到 register 和 unregister 事件后操灿,從 Key:/dubbo/com.foo.BarService/providers 下獲取提供者地址列表
6.服務(wù)監(jiān)控中心啟動時锯仪,從 Channel:/dubbo/* 訂閱 register 和 unregister,以及 subscribe和unsubsribe事件
7.服務(wù)監(jiān)控中心收到 register 和 unregister 事件后趾盐,從 Key:/dubbo/com.foo.BarService/providers下獲取提供者地址列表
8.服務(wù)監(jiān)控中心收到 subscribe 和 unsubsribe 事件后庶喜,從 Key:/dubbo/com.foo.BarService/consumers 下獲取消費者地址列
*/
5.redis消息隊列
6.分布式鎖
分布式鎖一般有三種實現(xiàn)方式:1. 數(shù)據(jù)庫樂觀鎖;2. 基于Redis的分布式鎖救鲤;3. 基于ZooKeeper的分布式鎖久窟。本篇博客將介紹第二種方式,基于Redis實現(xiàn)分布式鎖本缠。雖然網(wǎng)上已經(jīng)有各種介紹Redis分布式鎖實現(xiàn)的博客斥扛,然而他們的實現(xiàn)卻有著各種各樣的問題,為了避免誤人子弟丹锹,本篇博客將詳細(xì)介紹如何正確地實現(xiàn)Redis分布式鎖稀颁。
6.1 可靠性
首先芬失,為了確保分布式鎖可用,我們至少要確保鎖的實現(xiàn)同時滿足以下四個條件:
互斥性匾灶。在任意時刻棱烂,只有一個客戶端能持有鎖。
不會發(fā)生死鎖阶女。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖颊糜,也能保證后續(xù)其他客戶端能加鎖。
具有容錯性秃踩。只要大部分的Redis節(jié)點正常運行衬鱼,客戶端就可以加鎖和解鎖。
解鈴還須系鈴人吞瞪。加鎖和解鎖必須是同一個客戶端馁启,客戶端自己不能把別人加的鎖給解了。
加鎖
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 嘗試獲取分布式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標(biāo)識
* @param expireTime 超期時間
* @return 是否獲取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到芍秆,我們加鎖就一行代碼:jedis.set(String key, String value, String nxxx, String expx, int time)
惯疙,這個set()方法一共有五個形參:
第一個為key,我們使用key來當(dāng)鎖妖啥,因為key是唯一的霉颠。
第二個為value,我們傳的是requestId荆虱,很多童鞋可能不明白蒿偎,有key作為鎖不就夠了嗎,為什么還要用到value怀读?原因就是我們在上面講到可靠性時诉位,分布式鎖要滿足第四個條件解鈴還須系鈴人,通過給value賦值為requestId菜枷,我們就知道這把鎖是哪個請求加的了苍糠,在解鎖的時候就可以有依據(jù)。requestId可以使用
UUID.randomUUID().toString()
方法生成啤誊。第三個為nxxx岳瞭,這個參數(shù)我們填的是NX,意思是SET IF NOT EXIST蚊锹,即當(dāng)key不存在時瞳筏,我們進行set操作;若key已經(jīng)存在牡昆,則不做任何操作姚炕;
第四個為expx,這個參數(shù)我們傳的是PX,意思是我們要給這個key加一個過期的設(shè)置钻心,具體時間由第五個參數(shù)決定凄硼。
第五個為time,與第四個參數(shù)相呼應(yīng)捷沸,代表key的過期時間摊沉。
總的來說,執(zhí)行上面的set()方法就只會導(dǎo)致兩種結(jié)果:
當(dāng)前沒有鎖(key不存在)痒给,那么就進行加鎖操作说墨,并對鎖設(shè)置個有效期,同時value表示加鎖的客戶端苍柏。
已有鎖存在尼斧,不做任何操作。
心細(xì)的童鞋就會發(fā)現(xiàn)了试吁,我們的加鎖代碼滿足我們可靠性里描述的三個條件棺棵。首先,set()加入了NX參數(shù)熄捍,可以保證如果已有key存在烛恤,則函數(shù)不會調(diào)用成功,也就是只有一個客戶端能持有鎖余耽,滿足互斥性缚柏。其次,由于我們對鎖設(shè)置了過期時間碟贾,即使鎖的持有者后續(xù)發(fā)生崩潰而沒有解鎖币喧,鎖也會因為到了過期時間而自動解鎖(即key被刪除),不會發(fā)生死鎖袱耽。最后杀餐,因為我們將value賦值為requestId,代表加鎖的客戶端請求標(biāo)識朱巨,那么在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端怜浅。由于我們只考慮Redis單機部署的場景,所以容錯性我們暫不考慮蔬崩。
解鎖
private static final Long RELEASE_SUCCESS = 1L;
/**
* 釋放分布式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標(biāo)識
* @return 是否釋放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我們解鎖只需要兩行代碼就搞定了搀暑!第一行代碼沥阳,我們寫了一個簡單的Lua腳本代碼,上一次見到這個編程語言還是在《黑客與畫家》里自点,沒想到這次居然用上了桐罕。第二行代碼,我們將Lua代碼傳到jedis.eval()
方法里,并使參數(shù)KEYS[1]賦值為lockKey功炮,ARGV[1]賦值為requestId溅潜。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。
就是在eval命令執(zhí)行Lua代碼的時候薪伏,Lua代碼將被當(dāng)成一個命令去執(zhí)行滚澜,并且直到eval命令執(zhí)行完成,Redis才會執(zhí)行其他命令嫁怀。
Redission
概述
https://blog.csdn.net/u014042066/article/details/72778440
分布式系統(tǒng)有一個著名的理論CAP设捐,指在一個分布式系統(tǒng)中,最多只能同時滿足一致性(Consistency)塘淑、可用性(Availability)和分區(qū)容錯性(Partition tolerance)這三項中的兩項萝招。所以在設(shè)計系統(tǒng)時,往往需要權(quán)衡存捺,在CAP中作選擇槐沼。當(dāng)然,這個理論也并不一定完美捌治,不同系統(tǒng)對CAP的要求級別不一樣岗钩,選擇需要考慮方方面面。
在微服務(wù)系統(tǒng)中具滴,一個請求存在多級跨服務(wù)調(diào)用凹嘲,往往需要犧牲強一致性老保證系統(tǒng)高可用,比如通過分布式事務(wù)构韵,異步消息等手段完成周蹭。但還是有的場景,需要阻塞所有節(jié)點的所有線程疲恢,對共享資源的訪問凶朗。比如并發(fā)時“超賣”和“余額減為負(fù)數(shù)”等情況。
本地鎖可以通過語言本身支持显拳,要實現(xiàn)分布式鎖棚愤,就必須依賴中間件,數(shù)據(jù)庫杂数、redis宛畦、zookeeper等。
互斥:互斥好像是必須的揍移,否則怎么叫鎖次和。
死鎖: 如果一個線程獲得鎖,然后掛了那伐,并沒有釋放鎖踏施,致使其他節(jié)點(線程)永遠(yuǎn)無法獲取鎖石蔗,這就是死鎖。分布式鎖必須做到避免死鎖畅形。
性能: 高并發(fā)分布式系統(tǒng)中养距,線程互斥等待會成為性能瓶頸,需要好的中間件和實現(xiàn)來保證性能日熬。
鎖特性:考慮到復(fù)雜的場景棍厌,分布式鎖不能只是加鎖,然后一直等待碍遍。最好實現(xiàn)如Java Lock的一些功能如:鎖判斷定铜,超時設(shè)置,可重入性等怕敬。
Redission鎖總結(jié)
1揣炕,加鎖機制
為了實現(xiàn)原子操作,通過執(zhí)行一段Lua腳本進行加鎖东跪。首先鎖定key, 先判斷key是否已經(jīng)鎖定畸陡,未鎖定則通過hset語句進行鎖定
第一步 使用exist判斷key是否存在
第二步 hset 第一個參數(shù)是key,第二個參數(shù)是value虽填,第三個參數(shù)是1 丁恭。表示創(chuàng)建一個散列key, field域的值是 1
第三步 設(shè)置key過期時間
第四步 使用hexists判斷散列,field域的值是否為客戶端id
第五步 如果是斋日,則filed域的值加一
第六步 重新設(shè)置key過期時間
2牲览,互斥機制
客戶端2去加鎖,通過第二條語句判斷恶守,如果散列field域不是客戶端2第献,則返回pttl剩余的時間,客戶端2會進入一個while循環(huán)
3, watch dog自動延期
一個后臺線程兔港,發(fā)現(xiàn)客戶端1還持有鎖庸毫,會不斷延長key的時間
4, 可重入加鎖機制
第四步判斷成功,則會通過hincrby增加域的值