Redis總結(jié)(未拆分)

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):
  1. 字符串 String
  2. 字典Hash
  3. 列表List
  4. 集合Set
  5. 有序集合 SortedSet
  • 中級(jí)數(shù)據(jù)結(jié)構(gòu):
  1. HyperLogLog
  2. Geo
  3. Pub / Sub
  • 高端數(shù)據(jù)結(jié)構(gòu):
  1. BloomFilter
  2. RedisSearch
  3. Redis-ML
  4. 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ì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集群==

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • NOSQL類型簡(jiǎn)介鍵值對(duì):會(huì)使用到一個(gè)哈希表,表中有一個(gè)特定的鍵和一個(gè)指針指向特定的數(shù)據(jù),如redis,volde...
    MicoCube閱讀 4,168評(píng)論 2 27
  • Redis是啥 Redis是一個(gè)開(kāi)源的key-value存儲(chǔ)系統(tǒng),由于擁有豐富的數(shù)據(jù)結(jié)構(gòu),又被其作者戲稱為數(shù)據(jù)結(jié)構(gòu)...
    一凡呀閱讀 1,240評(píng)論 0 5
  • 一、Redis高可用概述 在介紹Redis高可用之前,先說(shuō)明一下在Redis的語(yǔ)境中高可用的含義。 我們知道,在w...
    空語(yǔ)閱讀 1,686評(píng)論 0 2
  • redis簡(jiǎn)介 redis是典型的NOSQL數(shù)據(jù)庫(kù),也是一個(gè)高性能的key-value型數(shù)據(jù)庫(kù)。通過(guò)源碼對(duì)redi...
    江北曉白閱讀 507評(píng)論 0 2
  • 一. 數(shù)據(jù)結(jié)構(gòu) 我們知道redis有5種基本類型:string、list、hash、set、zset,我們來(lái)看一下...
    漂泊的胡蘿卜閱讀 685評(píng)論 1 0

友情鏈接更多精彩內(nèi)容