數據一致性算法


最近工作中遇到了數據一致性問題,為方便以后使用,特學習記錄一下:

目前遇到現象:

(1)緩存與數據庫數據不一致情況

(2)分布式系統中各節點數據不一致情況

原因:

並發情況下,執行順序會引起寫請求和讀請求拿到的數據不一致,導致臟讀、幻讀等。

解決方案:

(1)針對本地緩存與數據庫數據不一致問題,可以通過先更新數據庫后刪除緩存+讀寫分離來解決,具體可參考另一篇文章《guava緩存使用

<1>緩存失效

<2>請求A讀緩存失敗,讀數據庫

<3>請求A寫緩存

<4>請求B寫數據庫,刪除緩存

<5>下個請求讀緩存失敗,寫緩存

為什么不會出現3/4步驟對調的情況,主要是數據庫的讀操作遠快於寫操作且讀寫分離的一個實現。如果還有擔心會對調可用消息中間件進行處理。

(2)針對分布式數據不一致問題,一致性哈希算法,將數據存儲在不同的機器上

  • 原理

1)一致性哈希將整個哈希值空間組織成一個虛擬的圓環,如某哈希函數H的值空間為0-2^32-1(即哈希值是一個32位無符號整形),整個哈希空間環起點和終點分別是0和2^32-1。

2)空間制定:整個空間按順時針方向組織。0和232-1在零點中方向重合。

3)服務器節點位置:將分布式各個服務器使用Hash進行一個哈希,具體可以選擇服務器的ip或主機名作為關鍵字進行哈希,這樣每台機器就能確定其在哈希環上的位置。

  數據傾斜問題優化:可對每個服務器進行多個hash以保證機器節點的均衡分布。

4)數據節點位置:將數據key使用相同的函數Hash計算出哈希值,並確定此數據在環上的位置,從此位置沿環順時針“行走”,第一台遇到的服務器就是其應該定位到的服務器。

 

 

  •  實操
  1. hash值計算:通過支持MD5與MurmurHash兩種計算方式。
  2. 一致性的實現:通過java的TreeMap來模擬環狀結構,實現均勻分布

 1.hash計算方法:MD5

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/*
 * 實現一致性哈希算法中使用的哈希函數,使用MD5算法來保證一致性哈希的平衡性
 */
public class HashFunction {
    private MessageDigest md5 = null;

    public long hash(String key) {
        if (md5 == null) {
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException("no md5 algrithm found");
            }
        }

        md5.reset();
        md5.update(key.getBytes());
        byte[] bKey = md5.digest();
        //具體的哈希函數實現細節--每個字節 & 0xFF 再移位
        long result = ((long) (bKey[3] & 0xFF) << 24)
                | ((long) (bKey[2] & 0xFF) << 16
                        | ((long) (bKey[1] & 0xFF) << 8) | (long) (bKey[0] & 0xFF));
        return result & 0xffffffffL;
    }
}

2.環的定義

for (int i = 0; i < numberOfReplicas; i++) {
            // 對於一個實際機器節點 node, 對應 numberOfReplicas 個虛擬節點
            /*
             * 不同的虛擬節點(i不同)有不同的hash值,但都對應同一個實際機器node
             * 虛擬node一般是均衡分布在環上的,數據存儲在順時針方向的虛擬node上
             */
            circle.put(hashFunction.hash(node.toString() + i), node);
}

3.數據hash映射關系確定

if (!circle.containsKey(hash)) {
    //數據映射在兩台虛擬機器所在環之間,就需要按順時針方向尋找機器
    SortedMap<Long, T> tailMap = circle.tailMap(hash);
    hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}

4.測試

package com.unionpay.techjoin.admin.controller.userbuss;

import java.util.*;

public class ConsistentHash<T> {
    private HashFunction hashFunction;
    private int numberOfReplicas;// 節點的復制因子,實際節點個數 * numberOfReplicas = 虛擬節點個數
    private final SortedMap<Long, T> circle = new TreeMap<Long, T>();// 存儲虛擬節點的hash值到真實節點的映射

    public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
                          Collection<T> nodes) {
        this.hashFunction = hashFunction;
        this.numberOfReplicas = numberOfReplicas;
        for (T node : nodes) {
            add(node);
        }
    }

    public void add(T node) {
        for (int i = 0; i < numberOfReplicas; i++)
            // 對於一個實際機器節點 node, 對應 numberOfReplicas 個虛擬節點
            /*
             * 不同的虛擬節點(i不同)有不同的hash值,但都對應同一個實際機器node
             * 虛擬node一般是均衡分布在環上的,數據存儲在順時針方向的虛擬node上
             */
            circle.put(hashFunction.hash(node.toString() + i), node);
    }

    public void remove(T node) {
        for (int i = 0; i < numberOfReplicas; i++)
            circle.remove(hashFunction.hash(node.toString() + i));
    }

    /*
     * 獲得一個最近的順時針節點,根據給定的key 取Hash
     * 然后再取得順時針方向上最近的一個虛擬節點對應的實際節點
     * 再從實際節點中取得 數據
     */
    public T get(Object key) {
        if (circle.isEmpty())
            return null;
        long hash = hashFunction.hash((String) key);// node 用String來表示,獲得node在哈希環中的hashCode
        if (!circle.containsKey(hash)) {//數據映射在兩台虛擬機器所在環之間,就需要按順時針方向尋找機器
            SortedMap<Long, T> tailMap = circle.tailMap(hash);
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hash);
    }

    public long getSize() {
        return circle.size();
    }

    /*
     * 查看MD5算法生成的hashCode值---表示整個哈希環中各個虛擬節點位置
     */
    public void testBalance(){
        Set<Long> sets  = circle.keySet();//獲得TreeMap中所有的Key
        SortedSet<Long> sortedSets= new TreeSet<Long>(sets);//將獲得的Key集合排序
        System.out.println("[1]節點為: ----");
        for(Long hashCode : sortedSets){
            System.out.print(hashCode);
            System.out.print(",");
        }
        System.out.println("----");
        System.out.println("[2]節點間距為: ----");
        /*
         * 查看用MD5算法生成的long hashCode 相鄰兩個hashCode的差值
         */
        Iterator<Long> it = sortedSets.iterator();
        Iterator<Long> it2 = sortedSets.iterator();
        if(it2.hasNext())
            it2.next();
        long keyPre, keyAfter;
        while(it.hasNext() && it2.hasNext()){
            keyPre = it.next();
            keyAfter = it2.next();
            System.out.print(keyAfter - keyPre);
            System.out.print(",");
        }
        System.out.println("----");
    }

    //根據key查找對應的節點
    public String getNodeForKey(String key) {
        long hash = hashFunction.hash(key);
        if (!circle.containsKey(hash)) {
            //數據映射在兩台虛擬機器所在環之間,就需要按順時針方向尋找機器
            SortedMap<Long, T> tailMap = circle.tailMap(hash);
            hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
        }
        return circle.get(hash)+",hash為:"+hash;
    }

    public static void main(String[] args) {
        Set<String> nodes = new HashSet<String>();
        nodes.add("A");
        nodes.add("B");
        nodes.add("C");

        ConsistentHash<String> consistentHash = new ConsistentHash<String>(new HashFunction(), 2, nodes);
        System.out.println("hash circle size: " + consistentHash.getSize());
        consistentHash.testBalance();
        System.out.println("[3]數據分配情況為: ----");
        for (int i = 0; i < 10; i++){
            String Key="user_" + i;
            System.out.println("Key:"+i+"分配到的Server為:"+consistentHash.getNodeForKey(Key));
        }

        //新加一節點
        consistentHash.add("D");
        System.out.println("hash circle size: " + consistentHash.getSize());
        consistentHash.testBalance();
        System.out.println("[3]數據分配情況為: ----");
        for (int i = 0; i < 10; i++){
            String Key="user_" + i;
            System.out.println("Key:"+i+"分配到的Server為:"+consistentHash.getNodeForKey(Key));
        }

        //刪除一節點
        consistentHash.remove("A");
        System.out.println("hash circle size: " + consistentHash.getSize());
        consistentHash.testBalance();
        System.out.println("[3]數據分配情況為: ----");
        for (int i = 0; i < 10; i++){
            String Key="user_" + i;
            System.out.println("Key:"+i+"分配到的Server為:"+consistentHash.getNodeForKey(Key));
        }
    }

}
  • 結果
hash circle size: 6
[1]節點為: ----
748451404,769404186,1696944585,1830063320,3862426151,3864615324,----
[2]節點間距為: ----
20952782,927540399,133118735,2032362831,2189173,----
[3]數據分配情況為: ----
Key:0分配到的Server為:B,hash為:748451404
Key:1分配到的Server為:B,hash為:1696944585
Key:2分配到的Server為:A,hash為:1830063320
Key:3分配到的Server為:A,hash為:3862426151
Key:4分配到的Server為:A,hash為:3862426151
Key:5分配到的Server為:B,hash為:748451404
Key:6分配到的Server為:A,hash為:3862426151
Key:7分配到的Server為:B,hash為:748451404
Key:8分配到的Server為:B,hash為:748451404
Key:9分配到的Server為:A,hash為:3862426151
hash circle size: 8
[1]節點為: ----
748451404,769404186,1696944585,1830063320,3372629518,3766042698,3862426151,3864615324,----
[2]節點間距為: ----
20952782,927540399,133118735,1542566198,393413180,96383453,2189173,----
[3]數據分配情況為: ----
Key:0分配到的Server為:B,hash為:748451404
Key:1分配到的Server為:B,hash為:1696944585
Key:2分配到的Server為:A,hash為:1830063320
Key:3分配到的Server為:D,hash為:3372629518
Key:4分配到的Server為:D,hash為:3372629518
Key:5分配到的Server為:B,hash為:748451404
Key:6分配到的Server為:D,hash為:3766042698
Key:7分配到的Server為:B,hash為:748451404
Key:8分配到的Server為:B,hash為:748451404
Key:9分配到的Server為:D,hash為:3372629518
hash circle size: 6
[1]節點為: ----
748451404,769404186,1696944585,3372629518,3766042698,3864615324,----
[2]節點間距為: ----
20952782,927540399,1675684933,393413180,98572626,----
[3]數據分配情況為: ----
Key:0分配到的Server為:B,hash為:748451404
Key:1分配到的Server為:B,hash為:1696944585
Key:2分配到的Server為:D,hash為:3372629518
Key:3分配到的Server為:D,hash為:3372629518
Key:4分配到的Server為:D,hash為:3372629518
Key:5分配到的Server為:B,hash為:748451404
Key:6分配到的Server為:D,hash為:3766042698
Key:7分配到的Server為:B,hash為:748451404
Key:8分配到的Server為:B,hash為:748451404
Key:9分配到的Server為:D,hash為:3372629518

 

從上面執行結果可以看出哈希一致性算法的優勢,對於刪除節點或新增節點僅影響對於片區內的。通過一致性hash算法可以很好的解決Redis分布式的問題,且當Redis server增加或減少的時候,之前存儲的緩存命中率還是比較高的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM