Redis為什么速度快
1疯坤、完全基于內(nèi)存,絕大部分請(qǐng)求是純粹的內(nèi)存操作深浮,非逞沟。快速。數(shù)據(jù)存在內(nèi)存中飞苇,類似于HashMap菌瘫,HashMap的優(yōu)勢(shì)就是查找和操作的時(shí)間復(fù)雜度都是O(1)蜗顽;
2、數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)單雨让,對(duì)數(shù)據(jù)操作也簡(jiǎn)單雇盖,Redis中的數(shù)據(jù)結(jié)構(gòu)是專門(mén)進(jìn)行設(shè)計(jì)的;
3栖忠、采用單線程崔挖,避免了不必要的上下文切換和競(jìng)爭(zhēng)條件,也不存在多進(jìn)程或者多線程導(dǎo)致的切換而消耗 CPU庵寞,不用去考慮各種鎖的問(wèn)題狸相,不存在加鎖釋放鎖操作,沒(méi)有因?yàn)榭赡艹霈F(xiàn)死鎖而導(dǎo)致的性能消耗捐川;
4脓鹃、使用多路I/O復(fù)用模型,非阻塞IO古沥;
5瘸右、使用底層模型不同,它們之間底層實(shí)現(xiàn)方式以及與客戶端之間通信的應(yīng)用協(xié)議不一樣岩齿,Redis直接自己構(gòu)建了VM 機(jī)制 太颤,因?yàn)橐话愕南到y(tǒng)調(diào)用系統(tǒng)函數(shù)的話,會(huì)浪費(fèi)一定的時(shí)間去移動(dòng)和請(qǐng)求纯衍;
以上幾點(diǎn)都比較好理解栋齿,下邊我們針對(duì)多路 I/O 復(fù)用模型進(jìn)行簡(jiǎn)單的探討:
(1)多路 I/O 復(fù)用模型
多路I/O復(fù)用模型是利用 select苗胀、poll襟诸、epoll 可以同時(shí)監(jiān)察多個(gè)流的 I/O 事件的能力,在空閑的時(shí)候基协,會(huì)把當(dāng)前線程阻塞掉歌亲,當(dāng)有一個(gè)或多個(gè)流有 I/O 事件時(shí),就從阻塞態(tài)中喚醒澜驮,于是程序就會(huì)輪詢一遍所有的流(epoll 是只輪詢那些真正發(fā)出了事件的流)陷揪,并且只依次順序的處理就緒的流,這種做法就避免了大量的無(wú)用操作杂穷。
這里“多路”指的是多個(gè)網(wǎng)絡(luò)連接悍缠,“復(fù)用”指的是復(fù)用同一個(gè)線程。采用多路 I/O 復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請(qǐng)求(盡量減少網(wǎng)絡(luò) IO 的時(shí)間消耗)耐量,且 Redis 在內(nèi)存中操作數(shù)據(jù)的速度非撤沈荆快,也就是說(shuō)內(nèi)存內(nèi)的操作不會(huì)成為影響Redis性能的瓶頸廊蜒,主要由以上幾點(diǎn)造就了 Redis 具有很高的吞吐量趴拧。
==select和epoll溅漾,poll的區(qū)別==
Redis 有哪些數(shù)據(jù)結(jié)構(gòu)?
- 普通數(shù)據(jù)結(jié)構(gòu):
- 字符串 String
- 字典Hash
- 列表List
- 集合Set
- 有序集合 SortedSet
- 中級(jí)數(shù)據(jù)結(jié)構(gòu):
- HyperLogLog
- Geo
- Pub / Sub
- 高端數(shù)據(jù)結(jié)構(gòu):
- BloomFilter
- RedisSearch
- Redis-ML
- JSON
==Redis數(shù)據(jù)結(jié)構(gòu)的應(yīng)用場(chǎng)景==
有序集合底層原理
- 有序集合對(duì)象的編碼可以是ziplist或者skiplist
- ziplist編碼的有序集合對(duì)象底層實(shí)現(xiàn)是壓縮列表著榴,每個(gè)有序集合的元素使用兩個(gè)緊挨在一起的壓縮列表節(jié)點(diǎn)來(lái)保存添履,第一個(gè)節(jié)點(diǎn)保存有序集合的元素,第二個(gè)節(jié)點(diǎn)保存元素的分值脑又。壓縮列表的集合元素按照分值從小到大開(kāi)始排序暮胧,分值較小的節(jié)點(diǎn)靠近壓縮列表的表頭方向,分值較大的節(jié)點(diǎn)靠近壓縮列表的表尾方向
- skiplist編碼的有序集合對(duì)象使用zset作為底層實(shí)現(xiàn)问麸,一個(gè)zset結(jié)構(gòu)同時(shí)包含一個(gè)字典和一個(gè)跳躍表叔壤。zset結(jié)構(gòu)中的zsl跳躍表按照分值從小到大保存了所有集合元素,每個(gè)跳躍表節(jié)點(diǎn)都保存了一個(gè)集合元素口叙,跳躍表節(jié)點(diǎn)的object屬性保存了元素的成員炼绘,score屬性保存了元素的分支,通過(guò)跳躍表妄田,程序可以對(duì)有序集合進(jìn)行范圍型操作俺亮,如ZRANK、ZRANGE等命令疟呐。zset結(jié)構(gòu)中的dict字典為有序集合創(chuàng)建了一個(gè)從成員到分值的映射脚曾,字典的每一個(gè)鍵值對(duì)都保存了一個(gè)集合元素,字典的鍵保存了元素的成員启具,字典的值保存了元素的分值本讥,通過(guò)這個(gè)字典,程序可以以O(shè)(1)復(fù)雜度查找給定成員的分值鲁冯,ZSCORE命令就是根據(jù)這一特性實(shí)現(xiàn)的拷沸。
==跳躍表原理==
為什么有序集合需要同時(shí)使用跳躍表和字典來(lái)實(shí)現(xiàn)呢
理論上來(lái)講,無(wú)論有序集合單獨(dú)使用跳躍表和字典來(lái)實(shí)現(xiàn)有序集合薯演,性能都會(huì)比同時(shí)使用跳躍表和字典有所降低撞芍。例如,如只使用字典來(lái)實(shí)現(xiàn)有序集合跨扮,那么在使用有序集合的ZRANK序无、ZRANGE命令時(shí),程序都需要對(duì)字典保存的所有元素進(jìn)行排序衡创,完成這種排序的復(fù)雜度為O(nlog(n)),以及額外的O(N)的空間(創(chuàng)建數(shù)組保存排序后的元素)帝嗡;如只使用跳躍表實(shí)現(xiàn)有序集合,那么在根據(jù)指定成員查詢分值時(shí)璃氢,復(fù)雜度就會(huì)變成O(log(n))哟玷,而不是字典的O(1)。綜合以上原因拔莱,為了讓有序集合同事?lián)碛凶值浜吞S表的所有特點(diǎn)碗降,Redis選擇同時(shí)使用跳躍表和字典來(lái)實(shí)現(xiàn)有序集合隘竭。
skiplist中的zset為什么不使用紅黑數(shù)而使用跳躍表
簡(jiǎn)單分析一下skiplist的時(shí)間復(fù)雜度和空間復(fù)雜度,以便對(duì)于skiplist的性能有一個(gè)直觀的了解讼渊。
我們先來(lái)計(jì)算一下每個(gè)節(jié)點(diǎn)所包含的平均指針數(shù)目(概率期望)动看。節(jié)點(diǎn)包含的指針數(shù)目,相當(dāng)于這個(gè)算法在空間上的額外開(kāi)銷(overhead)爪幻,可以用來(lái)度量空間復(fù)雜度菱皆。
跳躍表中產(chǎn)生越高的節(jié)點(diǎn)層數(shù),概率越低挨稿。定量的分析如下:
- 節(jié)點(diǎn)層數(shù)至少為1仇轻。而大于1的節(jié)點(diǎn)層數(shù),滿足一個(gè)概率分布奶甘。
- 節(jié)點(diǎn)層數(shù)恰好等于1的概率為1-p篷店。
- 節(jié)點(diǎn)層數(shù)大于等于2的概率為p,而節(jié)點(diǎn)層數(shù)恰好等于2的概率為p(1-p)臭家。
- 節(jié)點(diǎn)層數(shù)大于等于3的概率為p^2疲陕,
而節(jié)點(diǎn)層數(shù)恰好等于3的概率為p^2(1-p)。 - 節(jié)點(diǎn)層數(shù)大于等于4的概率為p^3钉赁,
而節(jié)點(diǎn)層數(shù)恰好等于4的概率為p^3(1-p)蹄殃。
因此,一個(gè)節(jié)點(diǎn)的平均層數(shù)(也即包含的平均指針數(shù)目)你踩,計(jì)算如下:
(1-p)+2p(1-p)+3p^2 (1-p)+4p^3 (1-)p+kp^(k-1) (1-p)=(1-)p·1/(1-p)^2=1/(1-p)
上述結(jié)果可以使用錯(cuò)位相減法計(jì)算得到诅岩。
現(xiàn)在很容易計(jì)算出:
- 當(dāng)p=1/2時(shí),每個(gè)節(jié)點(diǎn)所包含的平均指針數(shù)目為2带膜;
- 當(dāng)p=1/4時(shí)吩谦,每個(gè)節(jié)點(diǎn)所包含的平均指針數(shù)目為1.33。這也是Redis里的skiplist實(shí)現(xiàn)在空間上的開(kāi)銷钱慢。
==skiplist與平衡樹(shù)逮京、哈希表的比較==
- skiplist和各種平衡樹(shù)(如AVL卿堂、紅黑樹(shù)等)的元素是有序排列的束莫,而哈希表不是有序的。因此草描,在哈希表上只能做單個(gè)key的查找览绿,不適宜做范圍查找。所謂范圍查找穗慕,指的是查找那些大小在指定的兩個(gè)值之間的所有節(jié)點(diǎn)饿敲。
- 在做范圍查找的時(shí)候,平衡樹(shù)比skiplist操作要復(fù)雜逛绵。在平衡樹(shù)上怀各,我們找到指定范圍的小值之后倔韭,還需要以中序遍歷的順序繼續(xù)尋找其它不超過(guò)大值的節(jié)點(diǎn)。如果不對(duì)平衡樹(shù)進(jìn)行一定的改造瓢对,這里的中序遍歷并不容易實(shí)現(xiàn)寿酌。而在skiplist上進(jìn)行范圍查找就非常簡(jiǎn)單,只需要在找到小值之后硕蛹,對(duì)第1層鏈表進(jìn)行若干步的遍歷就可以實(shí)現(xiàn)醇疼。
- 平衡樹(shù)的插入和刪除操作可能引發(fā)子樹(shù)的調(diào)整,邏輯復(fù)雜法焰,而skiplist的插入和刪除只需要修改相鄰節(jié)點(diǎn)的指針秧荆,操作簡(jiǎn)單又快速。
- 從內(nèi)存占用上來(lái)說(shuō)埃仪,skiplist比平衡樹(shù)更靈活一些乙濒。一般來(lái)說(shuō),平衡樹(shù)每個(gè)節(jié)點(diǎn)包含2個(gè)指針(分別指向左右子樹(shù))卵蛉,而skiplist每個(gè)節(jié)點(diǎn)包含的指針數(shù)目平均為1/(1-p)琉兜,具體取決于參數(shù)p的大小。如果像Redis里的實(shí)現(xiàn)一樣毙玻,取p=1/4豌蟋,那么平均每個(gè)節(jié)點(diǎn)包含1.33個(gè)指針,比平衡樹(shù)更有優(yōu)勢(shì)桑滩。
- 查找單個(gè)key梧疲,skiplist和平衡樹(shù)的時(shí)間復(fù)雜度都為O(log n),大體相當(dāng)运准;而哈希表在保持較低的哈希值沖突概率的前提下幌氮,查找時(shí)間復(fù)雜度接近O(1),性能更高一些胁澳。所以我們平常使用的各種Map或dictionary結(jié)構(gòu)该互,大都是基于哈希表實(shí)現(xiàn)的。
- 從算法實(shí)現(xiàn)難度上來(lái)比較韭畸,skiplist比平衡樹(shù)要簡(jiǎn)單得多宇智。
Redis中的skiplist和經(jīng)典有何不同
- 分?jǐn)?shù)(score)允許重復(fù),即skiplist的key允許重復(fù)胰丁。這在最開(kāi)始介紹的經(jīng)典skiplist中是不允許的随橘。
- 在比較時(shí),不僅比較分?jǐn)?shù)(相當(dāng)于skiplist的key)锦庸,還比較數(shù)據(jù)本身机蔗。在Redis的skiplist實(shí)現(xiàn)中,數(shù)據(jù)本身的內(nèi)容唯一標(biāo)識(shí)這份數(shù)據(jù),而不是由key來(lái)唯一標(biāo)識(shí)萝嘁。另外梆掸,當(dāng)多個(gè)元素分?jǐn)?shù)相同的時(shí)候,還需要根據(jù)數(shù)據(jù)內(nèi)容來(lái)進(jìn)字典排序牙言。
- 第1層鏈表不是一個(gè)單向鏈表沥潭,而是一個(gè)雙向鏈表。這是為了方便以倒序方式獲取一個(gè)范圍內(nèi)的元素嬉挡。
- 在skiplist中可以很方便地計(jì)算出每個(gè)元素的排名(rank)钝鸽。
如何使用Redis進(jìn)行限流
1、SortedSet
利用zset數(shù)據(jù)結(jié)構(gòu)的score值來(lái)作為時(shí)間窗口庞钢,value保證唯一性即可,比如UUID或者雪花算法snowflake拔恰。
用一個(gè)zset結(jié)構(gòu)記錄用戶的歷史行為,每一個(gè)行為都會(huì)作為zset中的一個(gè)key保存下來(lái)基括,同一個(gè)用戶的同一種行為用一個(gè)zset記錄颜懊。
為節(jié)省內(nèi)存,只保留時(shí)間窗口內(nèi)的行為記錄风皿,如果用戶是冷用戶河爹,窗口內(nèi)的行為是空記錄,則這個(gè)zset可以從內(nèi)存中移除桐款。
通過(guò)統(tǒng)計(jì)窗口內(nèi)的行為數(shù)量與閾值進(jìn)行比較就可以得出當(dāng)前行為是否允許咸这。
代碼
public class SimpleRateLimiter {
private Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
// 指定用戶 user_id 的某個(gè)行為 action_key 在特定的時(shí)間內(nèi) period 只允許發(fā)生一定的次數(shù) max_co
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey);
long nowTs = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
// value 沒(méi)有實(shí)際的意義,保證唯一就可以
pipe.zadd(key, nowTs, "" + nowTs);
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
Response<Long> count = pipe.zcard(key);
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
// 調(diào)用這個(gè)接口 , 一分鐘內(nèi)只允許最多回復(fù) 5 個(gè)帖子
System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
}
}
}
缺點(diǎn):因?yàn)樗涗洉r(shí)間窗口內(nèi)所有的行為記錄魔眨, 如果這個(gè)量很大媳维,比如“限定 60s 內(nèi)操作不得超過(guò) 100 萬(wàn)次”之類,它是不適合做這樣的限流的,因?yàn)闀?huì)消耗大量的存儲(chǔ)空間。
Redission版本
public static void main(String[] args) {
RedissonClient redisson = Redisson.create();
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
String id = "123";
RBucket<Boolean> bucket = redisson.getBucket("blackId:" + id);
// 是否是黑名單得
if(bucket.get() == true){
// 或者返回最近的一個(gè)請(qǐng)求的緩存
return;
}
long nanoTime = System.nanoTime();
RScoredSortedSet<Object> zset = redisson.getScoredSortedSet(id);
zset.expire(10, TimeUnit.SECONDS);
zset.add(nanoTime, nanoTime);
zset.removeRangeByScore(0, true,
nanoTime - 10 * 1000 * 1000 * 1000, true);
int size = zset.size();
// 超過(guò)了10次武鲁,則進(jìn)入黑名單
if(size > 10){
// 加入黑名單,30秒之后不能再訪問(wèn)
bucket.set(true, 30, TimeUnit.SECONDS);
// 或者返回最近的一個(gè)請(qǐng)求的緩存
return;
}
// 放行
}
不使用Redis的單機(jī)Java版本
public class TestLimitFlow {
private Lock lock = new ReentrantLock();
private Map<String, Entity> map = new ConcurrentHashMap<>();
public TestLimitFlow() {
new Thread(() -> {
while (true) {
System.out.println(map.size());
map.forEach((key, value) -> {
long now = System.nanoTime();
if (value.expireTime < now) {
map.remove(key);
}
});
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
class Entity<T> {
public long expireTime;
T o;
}
public void interceptor(String ip) throws InterruptedException {
lock.lock();
Entity e = map.get(ip);
long nanoTime = System.nanoTime();
if (e == null) {
TreeSet<Long> treeSet = new TreeSet<>();
e = new Entity<TreeSet<Long>>();
e.expireTime = nanoTime + 10L * 1000 * 1000 * 1000;
e.o = treeSet;
map.put(ip, e);
}
lock.unlock();
synchronized (e) {
e.expireTime = nanoTime + 10L * 1000 * 1000 * 1000;
TreeSet<Long> zset = (TreeSet<Long>) e.o;
zset.add(nanoTime);
List<Long> deleteKey = Lists.newArrayList();
for(Long item : zset){
if(item < nanoTime - 10L * 1000 * 1000 * 1000){
System.out.println("true");
// zset.remove(item);
deleteKey.add(item);
}else {
break;
}
}
deleteKey.forEach(item -> {
zset.remove(item);
});
System.out.println(zset.size());
if (zset.size() > 10) {
// 加入黑名單
System.out.println("黑名單:" + ip + ":" + zset.size());
}
}
}
public static void main(String[] args) throws InterruptedException {
TestLimitFlow testXL = new TestLimitFlow();
for(int j = 0; j < 10;j++){
int finalJ = j;
new Thread(){
@Override
public void run() {
for (int i = 0; i < 12; i++) {
// TimeUnit.SECONDS.sleep(1);
try {
testXL.interceptor("localhost" + finalJ);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
}
2欢搜、令牌桶算法
如何使用 Redis 實(shí)現(xiàn)分布式鎖?
分布式鎖要解決的問(wèn)題
- 互斥性
- 安全性
- 死鎖
- 容錯(cuò)
版本1
使用 SEATNX key value:如果key不存在,則創(chuàng)建并賦值
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public String deductStock() {
String lockKey = "lambo";
try {
String value = "value";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
if (!result) {
return "error_code";
}
// 此處機(jī)器宕機(jī),將發(fā)生死鎖
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣減成功墓毒,剩余庫(kù)存:" + realStock);
} else {
System.out.println("扣減失敗,庫(kù)存不足");
}
} catch (Exception e) {
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
分析
在setnx后如果機(jī)器宕機(jī)盖灸,鎖得不到釋放蚁鳖,就會(huì)產(chǎn)生死鎖
版本2
給key設(shè)置過(guò)期時(shí)間
EXPIRE key seconds
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 設(shè)置過(guò)期時(shí)間,防止宕機(jī)帶來(lái)的死鎖
*
* @return
*/
public String deductStock2() {
String lockKey = "lambo";
try {
String value = "value";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
// 設(shè)置過(guò)期時(shí)間赁炎,但是set和expire不是原子性操作,還沒(méi)有設(shè)置過(guò)期時(shí)間,機(jī)器宕機(jī)了
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣減成功徙垫,剩余庫(kù)存:" + realStock);
} else {
System.out.println("扣減失敗讥裤,庫(kù)存不足");
}
} catch (Exception e) {
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
缺點(diǎn)
原子性等不到滿足,死鎖問(wèn)題依舊會(huì)出現(xiàn)
版本3
使用原子性操作
SETNX key value [EX seconds] [PX milliseconds] [NX|XX]
NX:只在鍵不存在時(shí)姻报,才對(duì)鍵進(jìn)行設(shè)置
XX:只在鍵存在時(shí)己英,才對(duì)鍵進(jìn)行設(shè)置
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* set+expire變更成原子操作
*
* @return
*/
public String deductStock3() {
String lockKey = "lambo";
try {
String value = "value";
// 原子操作
Boolean result = stringRedisTemplate.opsForValue().setIfPresent(lockKey, value, 10, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣減成功,剩余庫(kù)存:" + realStock);
} else {
System.out.println("扣減失敗吴旋,庫(kù)存不足");
}
} catch (Exception e) {
} finally {
// 線程可能會(huì)釋放其他線程的鎖损肛,無(wú)法保證安全性
stringRedisTemplate.delete(lockKey);
}
return "end";
}
}
分析
比如線程1設(shè)置key10秒過(guò)期,執(zhí)行業(yè)務(wù)需要15s荣瑟,10秒后線程2進(jìn)入治拿,由于key過(guò)期自動(dòng)刪除后,線程2拿到鎖笆焰,執(zhí)行業(yè)務(wù)需要8秒劫谅,在第15秒的時(shí)候線程一將鎖釋放了,但是此時(shí)線程也還沒(méi)操作完嚷掠,這樣其他線程又可以搶到鎖了
版本4
給線程設(shè)置唯一值捏检,釋放鎖的時(shí)候只有加鎖的線程才可以釋放鎖
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 線程只能釋放自己加的鎖,不能釋放別的線程加的鎖
*
* @return
*/
public String deductStock4() {
String lockKey = "lambo";
String clientId = UUID.randomUUID().toString();
try {
// 原子操作
Boolean result =
// 設(shè)置唯一值
stringRedisTemplate.opsForValue().setIfPresent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (!result) {
return "error_code";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣減成功不皆,剩余庫(kù)存:" + realStock);
} else {
System.out.println("扣減失敗贯城,庫(kù)存不足");
}
} catch (Exception e) {
} finally {
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
}
分析
此時(shí)線程是只能釋放自己加的鎖了,無(wú)法釋放其他線程的鎖霹娄,但是有可能業(yè)務(wù)本身執(zhí)行所需要的時(shí)間就是超過(guò)過(guò)期時(shí)間冤狡,而且這個(gè)時(shí)間是無(wú)法預(yù)估的,此時(shí)我們一種機(jī)制项棠,每隔一段時(shí)間去判斷線程是否還持有鎖悲雳,如果持有鎖則延長(zhǎng)鎖的過(guò)期時(shí)間。
版本5
使用redisson的看門(mén)狗機(jī)制
@Service
public class RedisSeckillServiceImpl {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 基于redisson設(shè)置分布式鎖
*
* @return
*/
public String deductStock5() {
String lockKey = "lambo";
RLock redissonLock = redissonClient.getLock(lockKey);
try {
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣減成功香追,剩余庫(kù)存:" + realStock);
} else {
System.out.println("扣減失敗合瓢,庫(kù)存不足");
}
} catch (Exception e) {
} finally {
redissonLock.unlock();
}
return "end";
}
}
如果Redis有1億個(gè)key,使用keys命令是否會(huì)影響線上服務(wù)
首先keys pattern命令可以從redis中查找所有符合給定模式pattern的key
如果Redis有1億個(gè)key透典,使用keys命令是會(huì)影響線上服務(wù)的晴楔,因?yàn)镽edis是單線程模型,當(dāng)然可以部署多個(gè)節(jié)點(diǎn)峭咒。
但是redis提供從海量數(shù)據(jù)里查詢某一固定前綴的key的命令:
SCAN cursor [MATCH pattern] [COUNT count]
1税弃、基于游標(biāo)的迭代器。需要基于上一次的游標(biāo)延續(xù)之前的迭代過(guò)程
2凑队、以0作為游標(biāo)開(kāi)始一次新的迭代则果,直到命令返回游標(biāo)0完成一次遍歷
3、不保證每次執(zhí)行都返回某個(gè)給定數(shù)量的元素,支持模糊查詢
4西壮、一次返回的數(shù)量不可控遗增,只能是大概率符合count數(shù)
例如 scan 0 match k* count 1;
如何使用 Redis 實(shí)現(xiàn)消息隊(duì)列?
一般使用 list 結(jié)構(gòu)作為隊(duì)列款青,rpush 生產(chǎn)消息做修,lpop 消費(fèi)消息。當(dāng) lpop 沒(méi)有消息的時(shí)候抡草,要適當(dāng) sleep 一會(huì)再重試饰及。
可不可以不用 sleep 呢?
list 還有個(gè)指令叫 blpop 康震,在沒(méi)有消息的時(shí)候燎含,它會(huì)阻塞住直到消息到來(lái)。
能不能生產(chǎn)一次消費(fèi)多次呢签杈?
使用 pub / sub 主題訂閱者模式瘫镇,發(fā)送者(pub)發(fā)送消息,訂閱者(sub)接收消息答姥∠吵可以實(shí)現(xiàn) 1:N 的消息隊(duì)列。
- 訂閱命令
subscribe channelName - 發(fā)布命令
publish channelName value
pub / sub 有什么缺點(diǎn)鹦付?
在消費(fèi)者下線的情況下尚粘,生產(chǎn)的消息會(huì)丟失,得使用專業(yè)的消息隊(duì)列如 rabbitmq 等敲长。
如何實(shí)現(xiàn)延時(shí)隊(duì)列郎嫁?
使用 sortedset ,拿時(shí)間戳作為 score 祈噪,消息內(nèi)容作為 key 調(diào)用 zadd 來(lái)生產(chǎn)消息泽铛,消費(fèi)者用 zrangebyscore 指令獲取 N 秒之前的數(shù)據(jù)輪詢進(jìn)行處理。
談?wù)劸彺鎿舸┘穑彺嫜┍揽唬彺娲┩?/p>
緩存擊穿(單個(gè)key失效同時(shí)有大量請(qǐng)求)
定義
緩存中的key一般有設(shè)置過(guò)期時(shí)間,如果某個(gè)key過(guò)期了月褥,在這個(gè)時(shí)候弛随,有大量的并發(fā)請(qǐng)求訪問(wèn)這個(gè)key,則這寫(xiě)請(qǐng)求都會(huì)進(jìn)入到DB中宁赤,導(dǎo)致DB瞬間壓力過(guò)大舀透,壓垮DB
解決方案
1、設(shè)置互斥鎖决左,mutex愕够。當(dāng)緩存失效的時(shí)候不是立即去訪問(wèn)數(shù)據(jù)庫(kù)走贪,而是使用分布式鎖,比如redis链烈,redission,zookeeper,
缺點(diǎn)
可能造成死鎖厉斟,或者線程池阻塞
緩存穿透(key本身不存在于數(shù)據(jù)庫(kù))
定義
數(shù)據(jù)庫(kù)中不存在的key挚躯,自然而然緩存中而不存在强衡,如果有大量地訪問(wèn)不存在的key的請(qǐng)求,請(qǐng)求會(huì)直接到數(shù)據(jù)庫(kù)码荔,壓垮數(shù)據(jù)庫(kù)
解決方案
1漩勤、布隆過(guò)濾器
布隆過(guò)濾器是一個(gè)非常神奇的數(shù)據(jù)結(jié)構(gòu),通過(guò)它我們可以非常方便地判斷一個(gè)給定數(shù)據(jù)是否存在與海量數(shù)據(jù)中缩搅。我們需要的就是判斷 key 是否合法越败,有沒(méi)有感覺(jué)布隆過(guò)濾器就是我們想要找的那個(gè)“人”。具體是這樣做的:把所有可能存在的請(qǐng)求的值都存放在布隆過(guò)濾器中硼瓣,當(dāng)用戶請(qǐng)求過(guò)來(lái)究飞,我會(huì)先判斷用戶發(fā)來(lái)的請(qǐng)求的值是否存在于布隆過(guò)濾器中。不存在的話堂鲤,直接返回請(qǐng)求參數(shù)錯(cuò)誤信息給客戶端亿傅,存在的話才會(huì)走下面的流程。
具體可看這篇文章
https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md
2瘟栖、不存在的key葵擎,在緩存中的value值設(shè)置為null,并且設(shè)置過(guò)期時(shí)間半哟,
java偽代碼如下
public Object getObjectInclNullById(Integer id) {
// 從緩存中獲取數(shù)據(jù)
Object cacheValue = cache.get(id);
// 緩存為空
if (cacheValue != null) {
// 從數(shù)據(jù)庫(kù)中獲取
Object storageValue = storage.get(key);
// 緩存空對(duì)象
cache.set(key, storageValue);
// 如果存儲(chǔ)數(shù)據(jù)為空酬滤,需要設(shè)置一個(gè)過(guò)期時(shí)間(300秒)
if (storageValue == null) {
// 必須設(shè)置過(guò)期時(shí)間,否則有被攻擊的風(fēng)險(xiǎn)
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
緩存雪崩(大量key同時(shí)失效)
定義
緩存中大量的key在同一時(shí)間失效寓涨,這時(shí)候大量的請(qǐng)求就會(huì)直接到數(shù)據(jù)庫(kù)盯串,使數(shù)據(jù)庫(kù)的壓力過(guò)大
解決方案
1、在過(guò)期時(shí)間上添加隨機(jī)值戒良,把緩存失效的時(shí)間錯(cuò)開(kāi)体捏,這樣的話失效的時(shí)間的重復(fù)率就降低了,降低了集體失效的概率
Redis 有幾種數(shù)據(jù)“過(guò)期”策略蔬墩?
Redis key過(guò)期的方式有三種:
首先刪除策略有三種
定時(shí)刪除
在設(shè)置鍵的過(guò)期時(shí)間的同時(shí)译打,創(chuàng)建一個(gè)定時(shí)器(timer),讓定時(shí)器在鍵的過(guò)期時(shí)間來(lái)臨拇颅,立即執(zhí)行對(duì)鍵的刪除操作
優(yōu)點(diǎn):對(duì)內(nèi)存友好
缺點(diǎn):對(duì)CPU不友好
惰性刪除
放任鍵過(guò)期時(shí)間不管奏司,但是每次從鍵空間中獲取鍵時(shí),都檢查取得的鍵是否過(guò)期樟插,如果過(guò)期的話就刪除該鍵韵洋;如果沒(méi)有過(guò)期就返回該鍵
優(yōu)點(diǎn):對(duì)CPU友好
缺點(diǎn):對(duì)內(nèi)存不友好竿刁,造成內(nèi)存泄漏
定期刪除
每隔一段時(shí)間,程序就對(duì)數(shù)據(jù)庫(kù)進(jìn)行一次檢查沒(méi)刪除里面的過(guò)期鍵搪缨。至于要?jiǎng)h除多少過(guò)期鍵食拜,以及檢查多少個(gè)數(shù)據(jù)庫(kù),則由算法決定
優(yōu)點(diǎn):是對(duì)定時(shí)刪除和惰性刪除的一種整合和折中
缺點(diǎn):難以缺點(diǎn)刪除操作執(zhí)行的時(shí)長(zhǎng)和頻率副编。執(zhí)行太頻繁會(huì)退化成定時(shí)策略负甸,執(zhí)行太少又會(huì)退化成惰性刪除策略
定時(shí)刪除和定期刪除為主動(dòng)刪除策略,惰性刪除為被動(dòng)刪除策略
Redis刪除策略
被動(dòng)刪除:當(dāng)讀/寫(xiě)一個(gè)已經(jīng)過(guò)期的key時(shí)痹届,會(huì)觸發(fā)惰性刪除策略呻待,直接刪除掉這個(gè)過(guò)期key
主動(dòng)刪除(定期刪除策略):由于惰性刪除策略無(wú)法保證冷數(shù)據(jù)被及時(shí)刪掉,所以Redis會(huì)定期主動(dòng)淘汰一批已過(guò)期的key
當(dāng)前已用內(nèi)存超過(guò)maxmemory限定時(shí)队腐,觸發(fā)主動(dòng)清理策略(即下面的淘汰策略)
Redis 有哪幾種數(shù)據(jù)“淘汰”策略蚕捉?
==Redis 提供了 6 種數(shù)據(jù)淘汰策略:==
- volatile-lru 只對(duì)設(shè)置了過(guò)期時(shí)間的key進(jìn)行LRU
- volatile-ttl 刪除即將過(guò)期了的key
- volatile-random
只對(duì)設(shè)置了過(guò)期時(shí)間的key進(jìn)行隨機(jī)刪除 - allkeys-lru
對(duì)所有key進(jìn)行LRU算法刪除 - allkeys-random
對(duì)所有key進(jìn)行隨機(jī)刪除 - no-enviction
永不過(guò)期,直接拋錯(cuò)
Redis持久化機(jī)制有哪些柴淘?
Redis有兩種持久化機(jī)制迫淹,AOF和RDB。
- AOF为严,記錄每次寫(xiě)請(qǐng)求的命令敛熬,以追加的方式在文件尾部追加,直接在尾部追加梗脾,效率比較高荸型。
對(duì)于操作系統(tǒng)來(lái)說(shuō),不是每次寫(xiě)都直接寫(xiě)到磁盤(pán)炸茧,操作系統(tǒng)自己會(huì)有一層cache瑞妇,redis寫(xiě)磁盤(pán)的數(shù)據(jù)會(huì)先緩存在os cache里,redis每隔1秒調(diào)用一次操作系統(tǒng)的fsync操作梭冠,強(qiáng)制將os cache中的數(shù)據(jù)刷入AOF文件中辕狰。
當(dāng)redis重啟的時(shí)候,就把AOF中記錄的命令重新執(zhí)行一遍就可以了控漠,但是如果文件很大的話蔓倍,執(zhí)行會(huì)耗費(fèi)較多的時(shí)間,對(duì)于數(shù)據(jù)恢復(fù)來(lái)說(shuō)耗時(shí)會(huì)多一點(diǎn)盐捷。
- RDB偶翅,是快照文件,每隔一定時(shí)間將redis內(nèi)存中的數(shù)據(jù)生成一份完整的RDB快照文件碉渡,當(dāng)redis重啟的時(shí)候直接加載數(shù)據(jù)即可聚谁,同樣的數(shù)據(jù)比AOF恢復(fù)的要快。
說(shuō)說(shuō)這兩種持久化機(jī)制各自的特點(diǎn)滞诺、優(yōu)缺點(diǎn)吧
==RDB優(yōu)點(diǎn)==
第一點(diǎn)就是他會(huì)生成多個(gè)數(shù)據(jù)文件形导,每個(gè)數(shù)據(jù)文件都代表了某一時(shí)刻redis中的全量數(shù)據(jù)环疼,非常適合做冷備。
第二點(diǎn)朵耕,RDB持久化機(jī)制對(duì)redis對(duì)外提供的讀寫(xiě)服務(wù)影響非常小炫隶,可以讓redis保持高性能,因?yàn)閞edis主進(jìn)程只需要fork一個(gè)子進(jìn)程阎曹,讓子進(jìn)程執(zhí)行磁盤(pán)IO操作來(lái)進(jìn)行RDB持久化即可伪阶。
第三點(diǎn),相對(duì)于AOF持久化機(jī)制來(lái)說(shuō)芬膝,直接基于RDB數(shù)據(jù)文件來(lái)重啟和恢復(fù)redis進(jìn)程望门,更加快速形娇。
RDB锰霜,就是一份數(shù)據(jù)文件,恢復(fù)的時(shí)候桐早,直接加載到內(nèi)存中即可癣缅。
==RBD的缺點(diǎn)==
1)故障時(shí)可能數(shù)據(jù)丟失的比AOF要多。
可能會(huì)因?yàn)镽edis掛起而且是從當(dāng)前值最近一次快照期間的數(shù)據(jù)
這個(gè)問(wèn)題哄酝,也是rdb最大的缺點(diǎn)友存,就是不適合做第一優(yōu)先的恢復(fù)方案,如果你依賴RDB做第一優(yōu)先恢復(fù)方案陶衅,會(huì)導(dǎo)致數(shù)據(jù)丟失的比較多
2)RDB每次在fork子進(jìn)程來(lái)執(zhí)行RDB快照數(shù)據(jù)文件生成的時(shí)候屡立,如果數(shù)據(jù)文件特別大,可能會(huì)導(dǎo)致對(duì)客戶端提供的服務(wù)暫停數(shù)毫秒搀军,或者甚至數(shù)秒膨俐。RDB是對(duì)內(nèi)存數(shù)據(jù)的全量同步,數(shù)據(jù)量大會(huì)由于I/O而影響性能
所以一般不要讓RDB的間隔太長(zhǎng)罩句,否則每次生成的RDB文件太大了焚刺,對(duì)redis本身的性能可能會(huì)有影響的。
==AOF的優(yōu)點(diǎn)==
AOF持久化门烂,記錄下除了查詢以外的所有變更數(shù)據(jù)量的指令乳愉,以append的形式追加保存到AOF文件中
1)AOF可以更好的保護(hù)數(shù)據(jù)不丟失
一般AOF會(huì)每隔1秒,通過(guò)一個(gè)后臺(tái)線程執(zhí)行一次fsync操作屯远,最多丟失1秒鐘的數(shù)據(jù)蔓姚。
每隔1秒,就執(zhí)行一次fsync操作慨丐,保證os cache中的數(shù)據(jù)寫(xiě)入磁盤(pán)中坡脐。
redis進(jìn)程掛了,最多丟掉1秒鐘的數(shù)據(jù).
2)AOF持久化性能高
AOF日志文件以append-only模式寫(xiě)入咖气,所以沒(méi)有任何磁盤(pán)尋址的開(kāi)銷挨措,寫(xiě)入性能非常高挖滤,而且文件不容易破損,即使文件尾部破損浅役,也很容易修復(fù)斩松。
3)AOF日志文件即使過(guò)大的時(shí)候,出現(xiàn)后臺(tái)重寫(xiě)操作觉既,也不會(huì)影響客戶端的讀寫(xiě)惧盹。
因?yàn)樵趓ewrite log的時(shí)候,會(huì)對(duì)其中的指令進(jìn)行壓縮瞪讼,創(chuàng)建出一份需要恢復(fù)數(shù)據(jù)的最小日志出來(lái)钧椰。再創(chuàng)建新日志文件的時(shí)候,老的日志文件還是照常寫(xiě)入符欠。當(dāng)新的merge后的日志文件ready的時(shí)候嫡霞,再交換新老日志文件即可。
4)AOF日志文件的命令通過(guò)非诚J粒可讀的方式進(jìn)行記錄诊沪,這個(gè)特性非常適合做災(zāi)難性的誤刪除的緊急恢復(fù)。
比如某人不小心用flushall命令清空了所有數(shù)據(jù)曾撤,只要這個(gè)時(shí)候后臺(tái)rewrite還沒(méi)有發(fā)生端姚,那么就可以立即拷貝AOF文件,將最后一條flushall命令給刪了挤悉,然后再將該AOF文件放回去渐裸,就可以通過(guò)恢復(fù)機(jī)制,自動(dòng)恢復(fù)所有數(shù)據(jù)装悲。
==AOF的缺點(diǎn)==
(1)對(duì)于同一份數(shù)據(jù)來(lái)說(shuō)昏鹃,AOF日志文件通常比RDB數(shù)據(jù)快照文件更大
(2)AOF開(kāi)啟后,支持的寫(xiě)QPS會(huì)比RDB支持的寫(xiě)QPS低衅斩,因?yàn)锳OF一般會(huì)配置成每秒fsync一次日志文件盆顾,當(dāng)然,每秒一次fsync畏梆,性能也還是很高的您宪。
如果你要保證一條數(shù)據(jù)都不丟,也是可以的奠涌,AOF的fsync設(shè)置成沒(méi)寫(xiě)入一條數(shù)據(jù)宪巨,fsync一次,但是那樣導(dǎo)致redis的QPS大幅度下降溜畅。
(3)以前AOF發(fā)生過(guò)bug捏卓,就是通過(guò)AOF記錄的日志,進(jìn)行數(shù)據(jù)恢復(fù)的時(shí)候慈格,沒(méi)有恢復(fù)一模一樣的數(shù)據(jù)出來(lái)怠晴。
所以說(shuō)遥金,類似AOF這種較為復(fù)雜的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的數(shù)據(jù)快照文件的方式蒜田,更加脆弱一些稿械,容易有bug。
不過(guò)AOF就是為了避免rewrite過(guò)程導(dǎo)致的bug冲粤,因此每次rewrite并不是基于舊的指令日志進(jìn)行merge的美莫,而是基于當(dāng)時(shí)內(nèi)存中的數(shù)據(jù)進(jìn)行指令的重新構(gòu)建,這樣健壯性會(huì)好很多梯捕。
(4)唯一的比較大的缺點(diǎn)厢呵,其實(shí)就是做數(shù)據(jù)恢復(fù)的時(shí)候,會(huì)比較慢傀顾,做冷備不太合適襟铭。
AOF持久化過(guò)程
調(diào)用fork()創(chuàng)建一個(gè)子進(jìn)程
子進(jìn)程將新的AOF寫(xiě)到一個(gè)臨時(shí)文件里,不依賴原來(lái)的AOF文件
主進(jìn)程持續(xù)將新的變動(dòng)寫(xiě)到內(nèi)存和原來(lái)的AOF文件里
主進(jìn)程獲取子進(jìn)程重新AOF的完成信息锣笨,往新AOF增量變動(dòng)
使用新的AOF文件替換掉舊的AOF文件
==主從復(fù)制==
首先說(shuō)一下Redis Sentinel是怎么工作的蝌矛?重點(diǎn)描述一下故障轉(zhuǎn)移的過(guò)程
Sentinel會(huì)以每秒一次的頻率向所有與它創(chuàng)建了命令連接的實(shí)例(包括主服務(wù)器,從服務(wù)器错英,其他Sentinel在內(nèi))發(fā)送PING命令
如果一個(gè)實(shí)例在down-after-milliseconds毫秒內(nèi)沒(méi)有向Sentinel返回有效回復(fù),則該Sentinel任務(wù)該實(shí)例處于下線狀態(tài)隆豹,稱為主觀下線
當(dāng)Sentinel從其他Sentinel那里收到足夠數(shù)量的已下線的判斷后椭岩,Sentinel就會(huì)將從服務(wù)器從主觀下線判定為客觀下線,并對(duì)主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作
當(dāng)一個(gè)主服務(wù)器被判斷為客觀下線后璃赡,監(jiān)視這個(gè)下線主服務(wù)器的各個(gè)Sentinel會(huì)進(jìn)行協(xié)商判哥,選舉出一個(gè)領(lǐng)頭的Sentinel,并由它對(duì)下線主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作碉考。
故障轉(zhuǎn)移操作主要步驟:
在已下線主服務(wù)器下的所有從服務(wù)器器里塌计,挑選出一個(gè)從服務(wù)器,并將其轉(zhuǎn)換成新的主服務(wù)器
讓已下線主服務(wù)器下的其他從服務(wù)器改為復(fù)制新的主服務(wù)器
將已下線主服務(wù)器設(shè)置為新的主服務(wù)器的從服務(wù)器侯谁,當(dāng)這個(gè)舊的主服務(wù)器重新上線時(shí)锌仅,它就會(huì)成為新的主服務(wù)器的從服務(wù)器
故障轉(zhuǎn)移時(shí)會(huì)從剩下的slave選舉一個(gè)新的master,被選舉為master的標(biāo)準(zhǔn)是什么墙贱?
Sentinel會(huì)將主服務(wù)器和從服務(wù)器的信息保存在一個(gè)列表中
主服務(wù)器下線時(shí)热芹,會(huì)刪除列表中所有處于下線或者斷線狀態(tài)的從服務(wù)器
刪除列表中所有最近5秒內(nèi)沒(méi)有回復(fù)過(guò)領(lǐng)頭Sentinel的INFO命令的從服務(wù)器,
刪除所有與已下線的主服務(wù)器連接斷開(kāi)炒貨down-after-milliseconds毫秒的從服務(wù)器惨撇,
然后根據(jù)優(yōu)先級(jí)(從高到低)伊脓,復(fù)制偏移量(從高到低),運(yùn)行id(從小到大)進(jìn)行排序選出領(lǐng)頭的Sentinel
執(zhí)行切換的那個(gè)哨兵在完成故障轉(zhuǎn)移后會(huì)做什么魁衙?
會(huì)進(jìn)行configuraiton配置信息傳播报腔。
哨兵完成切換之后株搔,會(huì)在自己本地更新生成最新的master配置,然后通過(guò)pub/sub消息機(jī)制同步給其他的哨兵纯蛾。
==Redis集群==