Redis
什么是redis
Redis 是一個(gè)基于內(nèi)存的高性能key-value數(shù)據(jù)庫(kù)。
Reids的特點(diǎn)
Redis本質(zhì)上是一個(gè)Key-Value類型的內(nèi)存數(shù)據(jù)庫(kù),因?yàn)槭羌儍?nèi)存操作,Redis的性能非常出色,每秒可以處理超過(guò) 10萬(wàn)次讀寫操作,是已知性能最快的Key-Value DB。 Redis最大的魅力是支持保存多種數(shù)據(jù)結(jié)構(gòu),此外單個(gè)value的最大限制是1GB 。
使用redis有哪些好處?
(1) 速度快,因?yàn)閿?shù)據(jù)存在內(nèi)存中
(2) 支持多種數(shù)據(jù)結(jié)構(gòu),支持string,list,set,sorted set,hash
(3) 支持事務(wù),操作都是原子性,所謂的原子性就是對(duì)數(shù)據(jù)的更改要么全部執(zhí)行,要么全部不執(zhí)行
(4) 豐富的特性:可用于緩存,消息,按key設(shè)置過(guò)期時(shí)間,過(guò)期后將會(huì)自動(dòng)刪除
Redis是單進(jìn)程單線程的
redis利用隊(duì)列技術(shù)將并發(fā)訪問(wèn)變?yōu)榇性L問(wèn),消除了傳統(tǒng)數(shù)據(jù)庫(kù)串行控制的開(kāi)銷
Redis的并發(fā)競(jìng)爭(zhēng)問(wèn)題如何解決?
Redis為單進(jìn)程單線程模式,采用隊(duì)列模式將并發(fā)訪問(wèn)變?yōu)榇性L問(wèn)。Redis本身沒(méi)有鎖的概念,Redis對(duì)于多個(gè)客戶端連接并不存在競(jìng)爭(zhēng),但是在Jedis客戶端對(duì)Redis進(jìn)行并發(fā)訪問(wèn)時(shí)會(huì)發(fā)生連接超時(shí)、數(shù)據(jù)轉(zhuǎn)換錯(cuò)誤、阻塞、客戶端關(guān)閉連接等問(wèn)題,這些問(wèn)題均是由于客戶端連接混亂造成。對(duì)此有2種解決方法:
1.客戶端角度,為保證每個(gè)客戶端間正常有序與Redis進(jìn)行通信,對(duì)連接進(jìn)行池化,同時(shí)對(duì)客戶端讀寫Redis操作采用內(nèi)部鎖synchronized。
2.服務(wù)器角度,利用setnx實(shí)現(xiàn)鎖。
是否使用過(guò)Redis集群,集群的原理是什么?
Redis Sentinel['s?nt?nl]著眼于高可用,在master宕機(jī)時(shí)會(huì)自動(dòng)將slave提升為master,繼續(xù)提供服務(wù)。
Redis Cluster['kl?st?]著眼于擴(kuò)展性,在單個(gè)redis內(nèi)存不足時(shí),使用Cluster進(jìn)行分片存儲(chǔ)。
Redis可以做什么(使用場(chǎng)景)
- 利用發(fā)布、訂閱構(gòu)建實(shí)時(shí)消息推送系統(tǒng)
- 配合關(guān)系型數(shù)據(jù)庫(kù)做高速緩存
- 分布式架構(gòu),會(huì)話緩存
什么是分布式鎖
分布式鎖是相對(duì)線程鎖和進(jìn)程鎖而言的。
| 名稱 | 描述 |
|---|---|
| 線程鎖 | 主要用來(lái)給方法、代碼塊加鎖。當(dāng)某個(gè)方法或代碼使用鎖,在同一時(shí)刻僅有一個(gè)線程執(zhí)行該方法或該代碼段。 |
| 進(jìn)程鎖 | 為了控制同一操作系統(tǒng)中多個(gè)進(jìn)程訪問(wèn)某個(gè)共享資源,因?yàn)檫M(jìn)程具有獨(dú)立性,各個(gè)進(jìn)程無(wú)法訪問(wèn)其他進(jìn)程的資源,因此無(wú)法通過(guò)synchronized等線程鎖實(shí)現(xiàn)進(jìn)程鎖。 |
| 分布式鎖 | 當(dāng)多個(gè)進(jìn)程不在同一個(gè)系統(tǒng)中,用分布式鎖控制多個(gè)進(jìn)程對(duì)資源的訪問(wèn)。 |
分布式鎖的使用場(chǎng)景
有這樣一個(gè)情境,線程A和線程B都共享某個(gè)變量X。
單機(jī)情況下,線程之間共享內(nèi)存,只需要使用線程鎖就可以解決并發(fā)問(wèn)題。
但是分布式情況下,線程A和線程B不再在同一個(gè)JVM中,線程鎖不起作用,這個(gè)時(shí)候就必須使用分布式鎖。
確保分布式鎖可以用,需滿足什么條件
- 互斥性。在任意時(shí)刻,只有一個(gè)客戶端能持有鎖。
- 不會(huì)發(fā)生死鎖。即使有一個(gè)客戶端在持有鎖的期間崩潰而沒(méi)有主動(dòng)解鎖,也能保證后續(xù)其他客戶端能加鎖。
- 具有容錯(cuò)性。只要大部分的Redis節(jié)點(diǎn)正常運(yùn)行,客戶端就可以加鎖和解鎖。
- 解鈴還須系鈴人。加鎖和解鎖必須是同一個(gè)客戶端,客戶端自己不能把別人加的鎖給解了。
分布式鎖的實(shí)現(xiàn)方式
1.引入jedis依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.獲取鎖的代碼實(shí)現(xiàn)
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 請(qǐng)求標(biāo)識(shí)
* @param expireTime 超期時(shí)間
* @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),這個(gè)set()方法一共有五個(gè)形參:
- 第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。
- 第二個(gè)為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?原因就是我們?cè)谏厦嬷v到可靠性時(shí),分布式鎖要滿足第四個(gè)條件解鈴還須系鈴人,通過(guò)給value賦值為requestId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)。requestId可以使用
UUID.randomUUID().toString()方法生成。 - 第三個(gè)為nxxx,這個(gè)參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時(shí),我們進(jìn)行set操作;若key已經(jīng)存在,則不做任何操作;
- 第四個(gè)為expx,這個(gè)參數(shù)我們傳的是PX,意思是我們要給這個(gè)key加一個(gè)過(guò)期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定。
- 第五個(gè)為time,與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間。
總的來(lái)說(shuō),執(zhí)行上面的set()方法就只會(huì)導(dǎo)致兩種結(jié)果:1. 當(dāng)前沒(méi)有鎖(key不存在),那么就進(jìn)行加鎖操作,并對(duì)鎖設(shè)置個(gè)有效期,同時(shí)value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。
3.釋放鎖代碼實(shí)現(xiàn)
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 釋放分布式鎖
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請(qǐng)求標(biāo)識(shí)
* @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;
}
}
可以看到,我們解鎖只需要兩行代碼就搞定了!第一行代碼,我們寫了一個(gè)簡(jiǎn)單的Lua腳本代碼,上一次見(jiàn)到這個(gè)編程語(yǔ)言還是在《黑客與畫家》里,沒(méi)想到這次居然用上了。第二行代碼,我們將Lua代碼傳到jedis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。
那么這段Lua代碼的功能是什么呢?其實(shí)很簡(jiǎn)單,首先獲取鎖對(duì)應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那么為什么要使用Lua語(yǔ)言來(lái)實(shí)現(xiàn)呢?因?yàn)橐_保上述操作是原子性的。
redis部署方式
| 部署方式 | 運(yùn)作模式 | 分片 | 可用性 |
|---|---|---|---|
| 單實(shí)例模式 | 單臺(tái)redis完成所有請(qǐng)求任務(wù),宕機(jī)則無(wú)法工作 | 由一臺(tái)實(shí)例獨(dú)立完成 | 無(wú)法復(fù)用和不具備容錯(cuò)性,只能進(jìn)行服務(wù)器物理擴(kuò)展。 |
| ['s?nt?nl]sentinel主從配置 | 由sentinel哨兵處理故障和完成故障轉(zhuǎn)移,也是性能的瓶頸所在 | 每個(gè)主實(shí)例分擔(dān)1/N的數(shù)據(jù)量 | 需要配置sentinel來(lái)實(shí)現(xiàn)復(fù)制和高可用性 |
| 集群模式 | 無(wú)中心節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)之間都有通信,當(dāng)節(jié)點(diǎn)較多時(shí)資源消耗較多 | 按照槽來(lái)進(jìn)行分片,通過(guò)為每個(gè)節(jié)點(diǎn)指派不同數(shù)量的槽,可以控制不同節(jié)點(diǎn)負(fù)責(zé)的數(shù)據(jù)量和請(qǐng)求數(shù)量。 | 集群的節(jié)點(diǎn)內(nèi)置了復(fù)制和高可用性。 |
單實(shí)例模式 :單實(shí)例模式是指單臺(tái)redis完成所有請(qǐng)求任務(wù),因此復(fù)用和不具備容錯(cuò)性;同時(shí)在單臺(tái)機(jī)器上如果只啟用一個(gè)redis實(shí)例會(huì)造成資源浪費(fèi) 。
Redis集群 :Redis 集群是一個(gè)由多個(gè)節(jié)點(diǎn)組成的分布式服務(wù)器群,它具有復(fù)制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成節(jié)點(diǎn)移除和故障轉(zhuǎn)移的功能。需要將每個(gè)節(jié)點(diǎn)設(shè)置成集群模式,這種集群模式?jīng)]有中心節(jié)點(diǎn),多個(gè)節(jié)點(diǎn)之間存在著網(wǎng)絡(luò)通信的消耗。
Redis Sentinel集群 :Sentinel是一個(gè)管理redis實(shí)例的工具,它可以實(shí)現(xiàn)對(duì)redis的監(jiān)控、通知、自動(dòng)故障轉(zhuǎn)移。sentinel不斷地檢測(cè)redis實(shí)例是否可以正常工作,通過(guò)API向其他程序報(bào)告redis的狀態(tài),如果redis master不能工作,則會(huì)自動(dòng)啟動(dòng)故障轉(zhuǎn)移進(jìn)程,將其中的一個(gè)slave提升為master,其他的slave重新設(shè)置新的master服務(wù)器。
Redis集群概念
分布式:Redis集群是一個(gè)由多個(gè)Redis服務(wù)器組成的分布式網(wǎng)絡(luò)服務(wù)器群,集群中的各個(gè)服務(wù)器被稱為節(jié)點(diǎn)(node),這些節(jié)點(diǎn)會(huì)相互連接并進(jìn)行通信。分布式的Redis集群沒(méi)有中心節(jié)點(diǎn),所以用戶不必?fù)?dān)心某個(gè)節(jié)點(diǎn)會(huì)成為整個(gè)集群的性能瓶頸。
復(fù)制 :Redis 集群的每個(gè)節(jié)點(diǎn)都有兩種角色可選,一個(gè)是主節(jié)點(diǎn)(master node),另一個(gè)是從節(jié)點(diǎn)(slavenode),其中主節(jié)點(diǎn)用于儲(chǔ)存數(shù)據(jù),而從節(jié)點(diǎn)則是某個(gè)主節(jié)點(diǎn)的復(fù)制品。當(dāng)用戶需要處理更多讀請(qǐng)求的時(shí)候,可以添加從節(jié)點(diǎn)以擴(kuò)展系統(tǒng)的讀性能。因?yàn)镽edis集群重用了單機(jī)Redis復(fù)制特性的代碼,所以集群的復(fù)制行為和我們之前介紹的單機(jī)復(fù)制特性的行為是完全一樣的。
節(jié)點(diǎn)故障檢測(cè)和自動(dòng)故障轉(zhuǎn)移 :Redis 集群的主節(jié)點(diǎn)內(nèi)置了類似Redis Sentinel的節(jié)點(diǎn)故障檢測(cè)和自動(dòng)故障轉(zhuǎn)移功能,當(dāng)集群中的某個(gè)主節(jié)點(diǎn)下線時(shí),集群中的其他在線主節(jié)點(diǎn)會(huì)注意到這一點(diǎn),并對(duì)已下線的主節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移。集群進(jìn)行故障轉(zhuǎn)移的方法和Redis Sentinel進(jìn)行故障轉(zhuǎn)移的方法基本一樣,不同的是,在集群里面,故障轉(zhuǎn)移是由集群中其他在線的主節(jié)點(diǎn)負(fù)責(zé)進(jìn)行的,所以集群不必另外使用Redis Sentinel 。
分片 :集群使用分片來(lái)擴(kuò)展數(shù)據(jù)庫(kù)的容量,并將命令請(qǐng)求的負(fù)載交給不同的節(jié)點(diǎn)來(lái)分擔(dān)。
集群將整個(gè)數(shù)據(jù)庫(kù)分為 16384 個(gè)槽(slot),所有鍵都屬于這 16384 個(gè)槽的其中一個(gè),計(jì)算鍵 key屬于哪個(gè)槽的公式為 slot_number = crc16(key) % 16384 ,其中 crc16 為 16 位的循環(huán)冗余校驗(yàn)和函數(shù)。集群中的每個(gè)主節(jié)點(diǎn)都可以處理 0 個(gè)至 16384 個(gè)槽,當(dāng) 16384 個(gè)槽都有某個(gè)節(jié)點(diǎn)在負(fù)責(zé)處理時(shí),集群進(jìn)入上線狀態(tài),并開(kāi)始處理客戶端發(fā)送的數(shù)據(jù)命令請(qǐng)求。轉(zhuǎn)向 :對(duì)于一個(gè)被指派了槽的主節(jié)點(diǎn)來(lái)說(shuō),這個(gè)主節(jié)點(diǎn)只會(huì)處理屬于指派給自己的槽的命令請(qǐng)求。如果一個(gè)節(jié)點(diǎn)接收到了與自己處理的槽無(wú)關(guān)的命令請(qǐng)求,那么節(jié)點(diǎn)會(huì)向客戶端返回一個(gè)轉(zhuǎn)向錯(cuò)誤(redirection error),告訴客戶端,哪個(gè)節(jié)點(diǎn)負(fù)責(zé)處理這條命令,之后客戶端需要根據(jù)錯(cuò)誤中包含的地址和端口號(hào)重新向正確的節(jié)點(diǎn)發(fā)送命令請(qǐng)求。
-
Redis集群客戶端 :因?yàn)榧汗δ鼙绕饐螜C(jī)功能要復(fù)雜得多,所以不同語(yǔ)言的 Redis 客戶端通常需要為集群添加特別的支持,或者專門開(kāi)發(fā)一個(gè)集群客戶端。
目前主要的 Redis 集群客戶端(或者說(shuō),支持集群功能的 Redis 客戶端)有以下這些:
- redis-rb-cluster:antirez 使用 Ruby 編寫的 Redis 集群客戶端,集群客戶端的官方實(shí)現(xiàn)。
- predis:Redis 的 PHP 客戶端,支持集群功能。
- jedis:Redis 的 JAVA 客戶端,支持集群功能。
- StackExchange.Redis:Redis 的 C# 客戶端,支持集群功能。
-內(nèi)置的 redis-cli :在啟動(dòng)時(shí)給定 -c 參數(shù)即可進(jìn)入集群模式,支持部分集群功能。redis-cluster架構(gòu)圖

- 所有的
redis節(jié)點(diǎn)彼此互聯(lián)(PING-PONG機(jī)制),內(nèi)部使用二進(jìn)制協(xié)議優(yōu)化傳輸速度和帶寬。 - 節(jié)點(diǎn)的
fail是通過(guò)集群中超過(guò)半數(shù)的節(jié)點(diǎn)檢測(cè)失效時(shí)才生效。 - 客戶端與
redis節(jié)點(diǎn)直連,不需要中間proxy層.客戶端不需要連接集群所有節(jié)點(diǎn),連接集群中任何一個(gè)可用節(jié)點(diǎn)即可。 -
redis-cluster把所有的物理節(jié)點(diǎn)映射到[0-16383]slot上,cluster負(fù)責(zé)維護(hù)node<->slot<->value
Redis集群中內(nèi)置了 16384 個(gè)哈希槽,當(dāng)需要在 Redis 集群中放置一個(gè) key-value 時(shí),redis 先對(duì)key 使用 crc16 算法算出一個(gè)結(jié)果,然后把結(jié)果對(duì) 16384 求余數(shù),這樣每個(gè) key 都會(huì)對(duì)應(yīng)一個(gè)編號(hào)在 0-16383 之間的哈希槽,redis 會(huì)根據(jù)節(jié)點(diǎn)數(shù)量大致均等的將哈希槽映射到不同的節(jié)點(diǎn)
redis-cluster投票:容錯(cuò)

1.投票過(guò)程是集群中所有master參與,如果半數(shù)以上master節(jié)點(diǎn)與master節(jié)點(diǎn)通信超時(shí)(cluster-node-timeout),認(rèn)為當(dāng)前master節(jié)點(diǎn)掛掉.
2.什么時(shí)候整個(gè)集群不可用(cluster_state:fail)
(1)如果集群任意master掛掉,且當(dāng)前master沒(méi)有slave.集群進(jìn)入fail狀態(tài),也可以理解成集群的slot映射[0-16383]不完整時(shí)進(jìn)入fail狀態(tài) 。
(2)如果集群超過(guò)半數(shù)以上master掛掉,無(wú)論是否有slave,集群進(jìn)入fail狀態(tài)。
客戶端對(duì)Redis集群的使用方法
(1)使用redis命令行客戶端連接
[root@localhost redis-cluster]# ./redis1/redis-cli -p 7001 -c
127.0.0.1:7001> get a
-> Redirected to slot [15495] located at 192.168.37.131:7003
(nil)
192.168.37.131:7003>
一定要加-c參數(shù),節(jié)點(diǎn)之間就可以互相跳轉(zhuǎn)
(2)使用jedis連接
package com.pc.jedis.test;
import java.util.HashSet;
import java.util.Set;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
/**
* Jedis集群測(cè)試
*
* @author Switch
* @data 2017年2月11日
* @version V1.0
*/
public class JedisClusterTest {
public static void main(String[] args) {
// 創(chuàng)建并填充節(jié)點(diǎn)信息
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.37.131", 7001));
nodes.add(new HostAndPort("192.168.37.131", 7002));
nodes.add(new HostAndPort("192.168.37.131", 7003));
nodes.add(new HostAndPort("192.168.37.131", 7004));
nodes.add(new HostAndPort("192.168.37.131", 7005));
nodes.add(new HostAndPort("192.168.37.131", 7006));
// 創(chuàng)建JedisCluster對(duì)象
JedisCluster jedisCluster = new JedisCluster(nodes);
// 使用jedisCluster操作redis
String key = "jedisCluster";
String setResult = jedisCluster.set(key, "hello redis!");
System.out.println(setResult);
String getResult = jedisCluster.get(key);
System.out.println(getResult);
// 關(guān)閉jedisCluster(程序執(zhí)行完后才能關(guān)閉,內(nèi)部封裝了連接池)
jedisCluster.close();
}
}
Redis Cluster['kl?st?] 集群
Redis集群搭建的方式有多種,例如使用zookeeper等,但從redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用無(wú)中心結(jié)構(gòu),每個(gè)節(jié)點(diǎn)保存數(shù)據(jù)和整個(gè)集群狀態(tài),每個(gè)節(jié)點(diǎn)都和其他所有節(jié)點(diǎn)連接。
結(jié)構(gòu)特點(diǎn):
1、所有的redis節(jié)點(diǎn)彼此互聯(lián)(PING-PONG機(jī)制),內(nèi)部使用二進(jìn)制協(xié)議優(yōu)化傳輸速度和帶寬。
2、節(jié)點(diǎn)的fail是通過(guò)集群中超過(guò)半數(shù)的節(jié)點(diǎn)檢測(cè)失效時(shí)才生效。
3、客戶端與redis節(jié)點(diǎn)直連,不需要中間proxy層.客戶端不需要連接集群所有節(jié)點(diǎn),連接集群中任何一個(gè)可用節(jié)點(diǎn)即可。
4、redis-cluster把所有的物理節(jié)點(diǎn)映射到[0-16383]slot上(不一定是平均分配),cluster 負(fù)責(zé)維護(hù)node<->slot<->value。
? 5、Redis集群預(yù)分好16384個(gè)桶,當(dāng)需要在 Redis 集群中放置一個(gè) key-value 時(shí),根據(jù) CRC16(key) mod 16384的值,決定將一個(gè)key放到哪個(gè)桶中。