分佈式專題-分佈式緩存技術之Redis04-Redis的應用實戰

前言

關於Redis,一共分爲4節,本節主要以Redis的實際應用爲主:

  1. 初步瞭解分佈式緩存技術Redis的使用
    主要會以數據結構爲導向去了解Redis
  2. Redis內部的原理揭祕
    Redis的內部原理、Lua腳本的結合使用、Redis的回收策略、Redis持久化原理
  3. 瞭解分佈式Redis
    分佈式Redis、主從、哨兵、分片、企業級集羣方案
  4. Redis的應用實戰
    Redis的應用實戰,如何在實際應用中去使用redis

Redis Java客戶端介紹

客戶端支持

Redis Java客戶端有很多的開源產品比如Redission、Jedis、lettuce

差異對比

Jedis是Redis的Java實現的客戶端,其API提供了比較全面的Redis命令的支持;

Redisson實現了分佈式和可擴展的Java數據結構,和Jedis相比,功能較爲簡單,不支持字符串操作,不支持排序、事務、管道、分區等Redis特性。Redisson主要是促進使用者對Redis的關注分離,從而讓使用者能夠將精力更集中地放在處理業務邏輯上。

lettuce是基於Netty構建的一個可伸縮的線程安全的Redis客戶端,支持同步、異步、響應式模式。多個線程可以共享一個連接實例,而不必擔心多線程併發問題;

Jedis-Sentinel原理分析

Java是怎麼基於哨兵模式的Jedis-sentinel建立的連接,我們來看
程序入口

    public static void main(String[] args) {
        //sentinel
        HostAndPort hostAndPort=new HostAndPort();
        //哨兵集羣的地址
        JedisSentinelPool jedisSentinelPool=new JedisSentinelPool();
    }

源碼分析

我們看看在初始化JedisSentinelPool的過程中都做了那些事情?
跳過多層構造,找到主線的邏輯:


  public JedisSentinelPool(String masterName, Set<String> sentinels,
      final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
      final String password, final int database, final String clientName) {
    this.poolConfig = poolConfig;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = password;
    this.database = database;
    this.clientName = clientName;

	//根據哨兵和master節點的名稱獲取到集羣的host地址和port端口號信息
    HostAndPort master = initSentinels(sentinels, masterName);
    //初始化連接池
    initPool(master);
  }

接着看JedisSentinelPool.initSentinels方法:

  private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");
//有多個sentinels,遍歷這些個sentinels for 
    for (String sentinel : sentinels) {
    //host:port表示的sentinel地址轉化爲一個HostAndPort對象。
      final HostAndPort hap = HostAndPort.parseString(sentinel);

      log.fine("Connecting to Sentinel " + hap);

      Jedis jedis = null;
      try {
      // 連接到sentinel
        jedis = new Jedis(hap.getHost(), hap.getPort());
// 根據masterName得到master的地址,返回一個list,host= list[0], port =// list[1] 
        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

        // connected to sentinel...
        sentinelAvailable = true;

        if (masterAddr == null || masterAddr.size() != 2) {
          log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
              + ".");
          continue;
        }
		//如果在任何一個sentinel中找到了master,不再遍歷
        master = toHostAndPort(masterAddr);
        log.fine("Found Redis master at " + master);
        break;
      } catch (JedisException e) {
        // resolves #1036, it should handle JedisException there's another chance
        // of raising JedisDataException
        log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
            + ". Trying next one.");
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }
//到這裏,如果master爲null,則說明有兩種情況,一種是所有的sentinels節點都down掉了,一種是master節點沒有被存活的sentinels監控到
    if (master == null) {
      if (sentinelAvailable) {
        // can connect to sentinel, but master name seems to not
        // monitored
        throw new JedisException("Can connect to sentinel, but " + masterName
            + " seems to be not monitored...");
      } else {
        throw new JedisConnectionException("All sentinels down, cannot determine where is "
            + masterName + " master is running...");
      }
    }
//如果走到這裏,說明找到了master的地址
    log.info("Redis master running at " + master + ", starting Sentinel listeners...");
//啓動對每個sentinels的監聽,爲每個sentinel都啓動了一個監聽者MasterListener。MasterListener本身是一個線程,它會去訂閱sentinel上關於master節點地址改變的消息。
    for (String sentinel : sentinels) {
      final HostAndPort hap = HostAndPort.parseString(sentinel);
      MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
      // whether MasterListener threads are alive or not, process can be stopped
      masterListener.setDaemon(true);
      masterListeners.add(masterListener);
      masterListener.start();
    }

    return master;
  }

initSentinels引用sentinelGetMasterAddrByName:從哨兵節點獲取master信息的方法

  public List<String> sentinelGetMasterAddrByName(String masterName) {
    client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName);
    final List<Object> reply = client.getObjectMultiBulkReply();
    return BuilderFactory.STRING_LIST.build(reply);
  }

客戶端通過連接到哨兵集羣,通過發送Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME 命令,從哨兵機器中詢問master節點的信息,拿到master節點的ip和端口號以後,再到客戶端發起連接。連接以後,需要在客戶端建立監聽機制,當master重新選舉之後,客戶端需要重新連接到新的master節點

接下來,我們回到主線劇情,看看初始化連接池部分:
JedisSentinelPool.initPool

private void initPool(HostAndPort master) {
    if (!master.equals(currentHostMaster)) {
      currentHostMaster = master;
      if (factory == null) {
        factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
            soTimeout, password, database, clientName, false, null, null, null);
        initPool(poolConfig, factory);
      } else {
        factory.setHostAndPort(currentHostMaster);
        // although we clear the pool, we still have to check the
        // returned object
        // in getResource, this call only clears idle instances, not
        // borrowed instances
        internalPool.clear();
      }

      log.info("Created JedisPool to master at " + master);
    }
  }

這裏就是通過JedisFactory連接到真實的服務器,ok整體流程解析完畢~

Jedis-Cluster原理分析

Jedis-cluster是另一種區別於哨兵模式的集羣模式~
首先我們看Java如何與其建立連接,通常我們會這樣寫:

連接方式

public static void main(String[] args) {
        Set<HostAndPort> hostAndPorts=new HashSet<>();

        HostAndPort hostAndPort=new HostAndPort("192.168.200.111",7000);

        HostAndPort hostAndPort1=new HostAndPort("192.168.200.112",7001);

        HostAndPort hostAndPort2=new HostAndPort("192.168.200.113",7003);

        HostAndPort hostAndPort3=new HostAndPort("192.168.200.114",7006);

        hostAndPorts.add(hostAndPort);

        hostAndPorts.add(hostAndPort1);

        hostAndPorts.add(hostAndPort2);

        hostAndPorts.add(hostAndPort3);

        JedisCluster jedisCluster=new JedisCluster(hostAndPorts,6000); jedisCluster.set("mic","hello");
}

源碼分析

程序啓動初始化集羣環境
初始化JedisCluster時,跳過層層初始化調用直接看這裏:

  public BinaryJedisCluster(Set<HostAndPort> jedisClusterNode, int timeout, int maxAttempts,
      final GenericObjectPoolConfig poolConfig) {
    this.connectionHandler = new JedisSlotBasedConnectionHandler(jedisClusterNode, poolConfig,
        timeout);
    this.maxAttempts = maxAttempts;
  }

接着初始化JedisSlotBasedConnectionHandler,跳過調用,發現了核心代碼:

  public JedisClusterConnectionHandler(Set<HostAndPort> nodes,
                                       final GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password) {
    this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, password);
    initializeSlotsCache(nodes, poolConfig, password);
  }

接着看initializeSlotsCache方法:

1)、讀取配置文件中的節點配置,無論是主從,無論多少個,只拿第一個,獲取redis連接實例

  private void initializeSlotsCache(Set<HostAndPort> startNodes, GenericObjectPoolConfig poolConfig, String password) {
    for (HostAndPort hostAndPort : startNodes) {
    //獲取jedis連接的實例
      Jedis jedis = new Jedis(hostAndPort.getHost(), hostAndPort.getPort());
      if (password != null) {
        jedis.auth(password);
      }
      try {
        cache.discoverClusterNodesAndSlots(jedis);
        break;
      } catch (JedisConnectionException e) {
        // try next nodes
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }
  }

2)、用獲取的redis連接實例執行discoverClusterNodesAndSlots ()方法,實際執行redis服務端cluster nodes命令,獲取主從配置信息

 public void discoverClusterNodesAndSlots(Jedis jedis) {
    w.lock();

    try {
      reset();
      List<Object> slots = jedis.clusterSlots();

      for (Object slotInfoObj : slots) {
        List<Object> slotInfo = (List<Object>) slotInfoObj;

        if (slotInfo.size() <= MASTER_NODE_INDEX) {
          continue;
        }

        List<Integer> slotNums = getAssignedSlotArray(slotInfo);

        // hostInfos
        int size = slotInfo.size();
        for (int i = MASTER_NODE_INDEX; i < size; i++) {
          List<Object> hostInfos = (List<Object>) slotInfo.get(i);
          if (hostInfos.size() <= 0) {
            continue;
          }

          HostAndPort targetNode = generateHostAndPort(hostInfos);
          setupNodeIfNotExist(targetNode);
          if (i == MASTER_NODE_INDEX) {
            assignSlotsToNode(slotNums, targetNode);
          }
        }
      }
    } finally {
      w.unlock();
    }
  }

3)、解析主從配置信息,先把所有節點存放到nodes的map集合中,key爲節點的ip:port,value爲當前節點的 jedisPool

4)、解析主節點分配的slots區間段,把slot對應的索引值作爲key,第三步中拿到的jedisPool作爲value,存儲在slots的map集合中,就實現了slot槽索引值與jedisPool的映射,這個jedisPool包含了master的節點信息,所以槽和幾點是對應的,與redis服務端一致

從集羣環境存取值

在上面的demo演示中,我們用set方法設置了JedisCluster屬性,現在我們看set方法裏面都做了哪些事情?

1)、把key作爲參數,執行CRC16算法,獲取key對應的slot值
在這裏插入圖片描述
2)、通過該slot值,去slots的map集合中獲取jedisPool實例
在這裏插入圖片描述
3)、通過jedisPool實例獲取jedis實例,最終完成redis數據存取工作

Redisson客戶端的操作方式

Redisson連接方式

   public static void main(String[] args) {
        Config config=new Config();
        config.useClusterServers().
                addNodeAddress("redis://192.168.200.111:7000",
                        "redis://192.168.200.111:7001");
        RedissonClient redissonClient=Redisson.create(config);
         //分佈式鎖
        redissonClient.getLock("");
        //獲取字符串對象
        redissonClient.getBucket("mic").set("value");
    }

常規操作命令:

getBucket-> 獲取字符串對象;
getMap -> 獲取map對象
getSortedSet->獲取有序集合
getSet -> 獲取集合
getList ->獲取列表

Redis實戰

分佈式鎖的實現

關於鎖,其實我們或多或少都有接觸過一些,比如synchronized、 Lock這些,這類鎖的目的很簡單,在多線程環境下,對共享資源的訪問造成的線程安全問題,通過鎖的機制來實現資源訪問互斥。那麼什麼是分佈式鎖呢?或者爲什麼我們需要通過Redis來構建分佈式鎖,其實最根本原因就是Score(範圍),因爲在分佈式架構中,所有的應用都是進程隔離的,在多進程訪問共享資源的時候我們需要滿足互斥性,就需要設定一個所有進程都能看得到的範圍,而這個範圍就是Redis本身。所以我們才需要把鎖構建到Redis中。

Redis裏面提供了一些比較具有能夠實現鎖特性的命令,比如SETEX(在鍵不存在的情況下爲鍵設置值),那麼我們可以基於這個命令來去實現一些簡單的鎖的操作

Redisson實現分佈式鎖

Redisson它除了常規的操作命令以外,還基於redis本身的特性去實現了很多功能的封裝,比如分佈式鎖、原子操作、布隆過濾器、隊列等等。我們可以直接利用這個api提供的功能去實現

    public static void main(String[] args) {
        Config config=new Config();
        config.useSingleServer().setAddress("redis://192.168.200.111:6379");
        RedissonClient redissonClient=Redisson.create(config);
        RLock rLock=redissonClient.getLock("updateOrder");
        try {
            rLock.tryLock(100,10,TimeUnit.SECONDS);
            System.out.println("test");
            Thread.sleep(1000);
            rLock.unlock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            rLock.unlock();
            redissonClient.shutdown();
        }
    }
Redisson實現分佈式鎖的原理
原理分析

trylock
在這裏插入圖片描述
tryAcquireAsync
在這裏插入圖片描述
tryLockInnerAsync


    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

通過lua腳本來實現加鎖的操作

  1. 判斷lock鍵是否存在,不存在直接調用hset存儲當前線程信息並且設置過期時間,返回nil,告訴客戶端直接獲取到鎖。

  2. 判斷lock鍵是否存在,存在則將重入次數加1,並重新設置過期時間,返回nil,告訴客戶端直接獲取到鎖。

  3. 被其它線程已經鎖定,返回鎖有效期的剩餘時間,告訴客戶端需要等待。

unlock

  1. 如果lock鍵不存在,發消息說鎖已經可用,發送一個消息

  2. 如果鎖不是被當前線程鎖定,則返回nil

  3. 由於支持可重入,在解鎖時將重入次數需要減1

  4. 如果計算後的重入次數>0,則重新設置過期時間

  5. 如果計算後的重入次數<=0,則發消息說鎖已經可用

Jedis實現分佈式鎖

建立連接

public class JedisConnectionUtils {
    private static JedisPool pool=null;
    static {
        JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(100);
        pool=new JedisPool(jedisPoolConfig,"192.168.200.111",6379);
    }
    public static Jedis getJedis(){
        return pool.getResource();
    }
}

分佈式鎖的實現

public class DistributedLock {

    //獲得鎖

    /**
     *
     * @param lockName  鎖的名詞
     * @param acquireTimeout  獲得鎖的超時時間
     * @param lockTimeout 鎖本身的過期時間
     * @return
     */
    public String acquireLock(String lockName,long acquireTimeout,long lockTimeout){
        //保證釋放鎖的時候是同一個持有鎖的人
        String identifier=UUID.randomUUID().toString();
        String lockKey="lock:"+lockName;
        int lockExpire=(int)(lockTimeout/1000);
        Jedis jedis=null;
        try {
            jedis = com.test.chapter9.JedisConnectionUtils.getJedis();
            long end = System.currentTimeMillis() + acquireTimeout;
            //獲取鎖的限定時間
            while (System.currentTimeMillis() < end) {
                //設置值成功
                if (jedis.setnx(lockKey, identifier) == 1) {
                    //設置超時時間
                    jedis.expire(lockKey, lockExpire);
                    //獲得鎖成功
                    return identifier;
                }
                //
                if (jedis.ttl(lockKey) == -1) {
                    //設置超時時間
                    jedis.expire(lockKey, lockExpire);
                }
                try {
                    //等待片刻後進行獲取鎖的重試
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            //回收
            jedis.close();
        }
        return null;
    }

    public boolean releaseLockWithLua(String lockName,String identifier){
        System.out.println(lockName+"開始釋放鎖:"+identifier);

        Jedis jedis= com.test.chapter9.JedisConnectionUtils.getJedis();
        String lockKey="lock:"+lockName;

        String lua="if redis.call(\"get\",KEYS[1])==ARGV[1] then " +
                "return redis.call(\"del\",KEYS[1]) " +
                "else return 0 end";
        Long rs=(Long) jedis.eval(lua,1,new String[]{lockKey,identifier});
        if(rs.intValue()>0){
            return true;
        }
        return false;

    }

    //釋放鎖
    public boolean releaseLock(String lockName,String identifier){
        System.out.println(lockName+"開始釋放鎖:"+identifier);

        String lockKey="lock:"+lockName;
        Jedis jedis=null;
        boolean isRelease=false;
        try{
            jedis= com.test.chapter9.JedisConnectionUtils.getJedis();
            while(true){
                jedis.watch(lockKey);
                //判斷是否爲同一把鎖
                if(identifier.equals(jedis.get(lockKey))){
                    Transaction transaction=jedis.multi();
                    transaction.del(lockKey);
                    if(transaction.exec().isEmpty()){
                        continue;
                    }
                    isRelease=true;
                }
                //TODO 異常
                jedis.unwatch();
                break;
            }
        }finally {
            jedis.close();
        }
        return  isRelease;
    }

}

測試用例

public class UnitTest extends Thread{

    @Override
    public void run() {
        while(true){
            DistributedLock distributedLock=new DistributedLock();
            String rs=distributedLock.acquireLock("updateOrder",
                    2000,5000);
            if(rs!=null){
                System.out.println(Thread.currentThread().getName()+"-> 成功獲得鎖:"+rs);
                try {
                  //模擬處理業務邏輯
                    Thread.sleep(1000);
                    distributedLock.releaseLockWithLua("updateOrder",rs);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }

    public static void main(String[] args) {
        UnitTest unitTest=new UnitTest();
        for(int i=0;i<10;i++){
            new Thread(unitTest,"tName:"+i).start();
        }
    }
}

起了10個線程測試分佈式鎖,走你~
在這裏插入圖片描述這就是Jedis分佈式鎖~

管道模式

Redis服務是一種C/S模型,提供請求-響應式協議的TCP服務,所以當客戶端發起請求,服務端處理並返回結果到客戶端,一般是以阻塞形式等待服務端的響應,但這在批量處理連接時延遲問題比較嚴重,所以Redis爲了提升或彌補這個問題,引入了管道技術:可以做到服務端未及時響應的時候,客戶端也可以繼續發送命令請求,做到客戶端和服務端互不影響,服務端並最終返回所有服務端的響應,大大提高了C/S模型交互的響應速度上有了質的提高

使用方法

    public static void main(String[] args) {
        Jedis jedis=new Jedis("192.168.11.152",6379);
        Pipeline pipeline=jedis.pipelined();
        for(int i=0;i<1000;i++){
            pipeline.incr("test");
        }
        pipeline.sync();
    }

Redis的應用架構

對於讀多寫少的高併發場景,我們會經常使用緩存來進行優化。比如說支付寶的餘額展示功能,實際上99%的時候都是查詢,1%的請求是變更(除非是土豪,每秒鐘都有收入在不斷更改餘額),所以,我們在這樣的場景下,可以加入緩存,用戶->餘額

在這裏插入圖片描述

Redis緩存與數據一致性問題

那麼基於上面的這個出發點,問題就來了,當用戶的餘額發生變化的時候,如何更新緩存中的數據,也就是說。

  1. 我是先更新緩存中的數據再更新數據庫的數據;

  2. 還是修改數據庫中的數據再更新緩存中的數據

這就是我們經常會在面試遇到的問題,數據庫的數據和緩存中的數據如何達到一致性?首先,可以肯定的是,redis中的數據和數據庫中的數據不可能保證事務性達到統一的,這個是毫無疑問的,所以在實際應用中,我們都是基於當前的場景進行權衡降低出現不一致問題的出現概率

更新緩存還是讓緩存失效

更新緩存表示數據不但會寫入到數據庫,還會同步更新緩存; 而讓緩存失效是表示只更新數據庫中的數據,然後刪除緩存中對應的key。那麼這兩種方式怎麼去選擇?這塊有一個衡量的指標。

  1. 如果更新緩存的代價很小,那麼可以先更新緩存,這個代價很小的意思是我不需要很複雜的計算去獲得最新的餘額數字。

  2. 如果是更新緩存的代價很大,意味着需要通過多個接口調用和數據查詢才能獲得最新的結果,那麼可以先淘汰緩存。淘汰緩存以後後續的請求如果在緩存中找不到,自然去數據庫中檢索。

先操作數據庫還是先操作緩存?

當客戶端發起事務類型請求時,假設我們以讓緩存失效作爲緩存的的處理方式,那麼又會存在兩個情況,

  1. 先更新數據庫再讓緩存失效

  2. 先讓緩存失效,再更新數據庫

前面我們講過,更新數據庫和更新緩存這兩個操作,是無法保證原子性的,所以我們需要根據當前業務的場景的容忍性來選擇。也就是如果出現不一致的情況下,哪一種更新方式對業務的影響最小,就先執行影響最小的方案

最終一致性的解決方案

在這裏插入圖片描述

關於緩存雪崩的解決方案

當緩存大規模滲透在整個架構中以後,那麼緩存本身的可用性講決定整個架構的穩定性。那麼接下來我們來討論下緩存在應用過程中可能會導致的問題。

緩存雪崩

緩存雪崩是指設置緩存時採用了相同的過期時間,導致緩存在某一個時刻同時失效,或者緩存服務器宕機宕機導致緩存全面失效,請求全部轉發到了DB層面,DB由於瞬間壓力增大而導致崩潰。緩存失效導致的雪崩效應對底層系統的衝擊是很大的。

解決方式
  1. 對緩存的訪問,如果發現從緩存中取不到值,那麼通過加鎖或者隊列的方式保證緩存的單進程操作,從而避免失效時併發請求全部落到底層的存儲系統上;但是這種方式會帶來性能上的損耗

  2. 將緩存失效的時間分散,降低每一個緩存過期時間的重複率

  3. 如果是因爲緩存服務器故障導致的問題,一方面需要保證緩存服務器的高可用、另一方面,應用程序中可以採用多級緩存

緩存穿透

緩存穿透是指查詢一個根本不存在的數據,緩存和數據源都不會命中。出於容錯的考慮,如果從數據層查不到數據則不寫入緩存,即數據源返回值爲 null 時,不緩存 null。緩存穿透問題可能會使後端數據源負載加大,由於很多後端數據源不具備高併發性,甚至可能造成後端數據源宕掉

解決方式
  1. 如果查詢數據庫也爲空,直接設置一個默認值存放到緩存,這樣第二次到緩衝中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。比如,”key” , “&&”。

在返回這個&&值的時候,我們的應用就可以認爲這是不存在的key,那我們的應用就可以決定是否繼續等待繼續訪問,還是放棄掉這次操作。如果繼續等待訪問,過一個時間輪詢點後,再次請求這個key,如果取到的值不再是&&,則可以認爲這時候key有值了,從而避免了透傳到數據庫,從而把大量的類似請求擋在了緩存之中。

  1. 根據緩存數據Key的設計規則,將不符合規則的key進行過濾

採用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的BitSet中,不存在的數據將會被攔截掉,從而避免了對底層存儲系統的查詢壓力

布隆過濾器

布隆過濾器是Burton Howard Bloom在1970年提出來的,一種空間效率極高的概率型算法和數據結構,主要用來判斷一個元素是否在集合中存在。因爲他是一個概率型的算法,所以會存在一定的誤差,如果傳入一個值去布隆過濾器中檢索,可能會出現檢測存在的結果但是實際上可能是不存在的,但是肯定不會出現實際上不存在然後反饋存在的結果。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter 通過極少的錯誤換取了存儲空間的極大節省。

bitmap

所謂的Bit-map就是用一個bit位來標記某個元素對應的Value,通過Bit爲單位來存儲數據,可以大大節省存儲空間.所以我們可以通過一個int型的整數的32比特位來存儲32個10進制的數字,那麼這樣所帶來的好處是內存佔用少、效率很高(不需要比較和位移)比如我們要存儲5(101)、3(11)四個數字,那麼我們申請int型的內存空間,會有32個比特位。這四個數字的二進制分別對應

從右往左開始數,比如第一個數字是5,對應的二進制數據是101, 那麼從右往左數到第5位,把對應的二進制數據存儲到32個比特位上。

第一個5就是 00000000000000000000000000101000

輸入3時候 00000000000000000000000000001100

布隆過濾器原理

有了對位圖的理解以後,我們對布隆過濾器的原理理解就會更容易了,仍然以前面提到的40億數據爲案例,假設這40億數據爲某郵件服務器的黑名單數據,郵件服務需要根據郵箱地址來判斷當前郵箱是否屬於垃圾郵件。原理如下

假設集合裏面有3個元素{x, y, z},哈希函數的個數爲3。首先將位數組進行初始化,將裏面每個位都設置位0。對於集合裏面的每一個元素,將元素依次通過3個哈希函數進行映射,每次映射都會產生一個哈希值,這個值對應位數組上面的一個點,然後將位數組對應的位置標記爲1。查詢W元素是否存在集合中的時候,同樣的方法將W通過哈希映射到位數組上的3個點。如果3個點的其中有一個點不爲1,則可以判斷該元素一定不存在集合中。反之,如果3個點都爲1,則該元素可能存在集合中
在這裏插入圖片描述
接下來按照該方法處理所有的輸入對象,每個對象都可能把bitMap中一些白位置塗黑,也可能會遇到已經塗黑的位置,遇到已經爲黑的讓他繼續爲黑即可。處理完所有的輸入對象之後,在bitMap中可能已經有相當多的位置已經被塗黑。至此,一個布隆過濾器生成完成,這個布隆過濾器代表之前所有輸入對象組成的集合。

如何去判斷一個元素是否存在bit array中呢? 原理是一樣,根據k個哈希函數去得到的結果,如果所有的結果都是1,表示這個元素可能(假設某個元素通過映射對應下標爲4,5,6這3個點。雖然這3個點都爲1,但是很明顯這3個點是不同元素經過哈希得到的位置,因此這種情況說明元素雖然不在集合中,也可能對應的都是1)存在。 如果一旦發現其中一個比特位的元素是0,表示這個元素一定不存在

至於k個哈希函數的取值爲多少,能夠最大化的降低錯誤率(因爲哈希函數越多,映射衝突會越少),這個地方就會涉及到最優的哈希函數個數的一個算法邏輯

在這裏插入圖片描述

後記

我在雲盤裏面上傳了一本電子書,《Redis實戰》,大家可以先下載去看看。

附贈《Redis實戰的PDF版電子書
提取碼:9lr8

更多架構知識,歡迎關注本套Java系列文章Java架構師成長之路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章