負載均衡 '全局鎖' 和 '頻繁提交' 的問題

轉載請註明出處 http://www.paraller.com
原文排版地址 點擊獲取更好閱讀體驗git

負載均衡架構須要解決兩個問題: 頻繁提交全局鎖github

頻繁提交,接口冪等性問題

獨立主機: 內存鎖

以往的處理方式: 內存鎖redis

private static Map<String, Integer> order_sync = new HashMap<String, Integer>();

/**
 * 訂單的同步處理
 * 
 * @param order_no
 * @param remove
 *            是否在內存隊列中刪除
 * @return 流程是否正常
 */
private synchronized boolean orderSync(String order_no, Boolean remove) {

    if (remove) {
        order_sync.remove(order_no);
        return true;
    }
    Integer s = order_sync.get(order_no);

    if (s == null) {
        order_sync.put(order_no, 99); // 處理中
        return true;
    } else {
        return false;
    }
}

使用方法:調用方法體:
{
    if (!this.orderSync(orderNo, false)) {
        return new Message<String>(OrderError.REPEAT.value, OrderError.REPEAT.alias);
    }   
    
    //TODO  業務處理

    this.orderSync(orderNo, true);     
}

分佈式主機:redis鎖

分佈式集羣的狀況下,每一個接口都須要作頻繁提交的處理,作接口冪等性設計。spring

解決方案:設計模式

Node中間件

一、有狀態 : user_id + ip + url
二、無狀態 : ip + url服務器

這種方式有一個小問題,沒有對參數進行判斷,冪等性的原則是:函數/接口可使用相同的參數重複執行, 不該該影響系統狀態, 也不會對系統形成改變。數據結構

實際的場景: 一個用戶提現,一次提現100,一次提現500, 若是100的接口尚未返回,用戶提現500被拒絕,在實際的場景中也是被容許的。架構

後臺服務

對內存鎖作修改,將內存鎖修改成 redis鎖,對關鍵的函數進行處理。細節:set NX (redis自帶的鎖)負載均衡

@Resource(name = "redisTemplate")
private ValueOperations<String, String> redisString;

/**
 * 方法是否能夠調用
 * 
 * @author zhidaliao
 * @param key
 * @return remove false表明第一次加鎖 true表明釋放鎖
 */
public boolean atomProcessor(String key, boolean remove) {

    if (!remove) {
        boolean flag = true;
        try {

            flag = redisString.setIfAbsent(key, "true");
            redisString.getOperations().expire(key, 90, TimeUnit.SECONDS);   //要確保你的程序有事務支持,或者中間狀態

        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.getMessage());
            flag = false;
        }

        if (!flag) {
            logger.debug("atomProcessor false key:" + key);
        }

        return flag;
    } else {
        redisString.getOperations().delete(key);
        return true;
    }

}

全局鎖

全局鎖是爲了讓 synchronized 可以分佈式化。 和內存鎖這樣的設計不一樣的是,全局鎖須要阻塞(可使用同步工具類實現阻塞),等待鎖釋放框架

思考一:

redis鎖 + while循環

T1 -> getLock -> bussiness Code -> release Lock

T2 -> getLock - > while(判斷上一個Key是否被釋放,判斷是否超時) -> delete Lock -> get Lock -> bussiness Code -> release Lock

T3 -> getLock - > while(判斷上一個Key是否被釋放,判斷是否超時) -> delete Lock -> get Lock -> bussiness Code -> release Lock

弊端: T2 T3同時獲取鎖的時候,會形成 T3把T2的鎖刪除掉,而後獲取鎖。

思考二:

修改一下數據結構,改爲Hash的格式。

T1 Set key ("createOrder","2017-01-01  10:10")

T2 while ("存在")
    檢測時間 當前 "2017-01-01  10:30"
    Set key ("createOrder","2017-01-01  10:40")
    delete T1

T3 while ("存在")
    檢測時間 當前 "2017-01-01  10:30"
    Get key ("createOrder") = "2017-01-01  10:40"
    wait();

思考三:

避免了線程衝突,可是沒有實現線程優先請求,優先分配鎖。能夠繼承Java的公平鎖的實現方案處理。

在網上找了一個輪子:redisson
注意細節:有Java 1.7 和 Java 1.8 的版本

Redisson簡單使用方法

pom.xml

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.4.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>1.5.2.RELEASE</version>
</dependency> 

<!-- 會致使須要太高JDK版本編譯  消除依賴錯誤以後可刪除 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-parent</artifactId>
    <version>1.5.2.RELEASE</version>
    <relativePath>../spring-boot-parent</relativePath>
</parent>

使用RedissonAutoConfiguration初始化

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedissonAutoConfiguration {
    
    @Autowired
    private RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
        
        Config config = new Config();
                
        //sentinel
        if (redisProperties.getSentinel() != null) {
            
            SentinelServersConfig sentinelServersConfig = config.useSentinelServers();
            sentinelServersConfig.setMasterName(redisProperties.getSentinel().getMaster());
            redisProperties.getSentinel().getNodes();
            sentinelServersConfig.addSentinelAddress(redisProperties.getSentinel().getNodes().split(","));
            sentinelServersConfig.setDatabase(redisProperties.getDatabase());
            if (redisProperties.getPassword() != null) {
                sentinelServersConfig.setPassword(redisProperties.getPassword());
            }
            
        } else { 
            
            
            
            //single server
            SingleServerConfig singleServerConfig = config.useSingleServer();
            // format as redis://127.0.0.1:7181 or rediss://127.0.0.1:7181 for SSL
            String schema = redisProperties.isSsl() ? "rediss://" : "redis://";
            singleServerConfig.setAddress(schema + redisProperties.getHost() + ":" + redisProperties.getPort());
            singleServerConfig.setDatabase(redisProperties.getDatabase());
            if (redisProperties.getPassword() != null) {
                singleServerConfig.setPassword(redisProperties.getPassword());
            }
        }
        
        return Redisson.create(config);
    }
}

調用全局鎖

@Service
public class GlobalLock {

    @Autowired
    private RedissonClient redissonClient;

    private static final Logger logger = LoggerFactory.getLogger(GlobalLock.class);

    private final int tryTime = 60;
    private final int unLockTime = 40;

    /**
     * 是否釋放鎖 ,不然就加鎖
     * 
     * @param key
     * @param flag
     * @return
     */
    public boolean unlockOrNot(String key, boolean flag) {

        RLock fairLock = redissonClient.getFairLock(key);

        boolean result = false;

        // 加鎖
        if (!flag) {

            logger.debug("start Get lock ~");

            try {
                fairLock.lock(unLockTime,TimeUnit.SECONDS);
                result = fairLock.tryLock(tryTime, unLockTime, TimeUnit.SECONDS);
                if (result) {
                    logger.debug("get " + key + " succ");
                } else {
                    logger.debug("get " + key + " fail");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                logger.error(e.getMessage());
            }

        }

        // 釋放鎖
        else {

            fairLock.unlock();
            logger.debug("release " + key + " succ");
            result = true;
        }

        return result;
    }
}

延伸

在作分佈式架構選型的時候,優先考慮的是Spring 社區的Cloud項目,netflix在分佈式開源對Cloud項目有很大的貢獻。
在作第一步發現服務的選型的時候,基於以上的考慮,在和Zookeeper作了對比,將Eureka做爲發現服務的框架。

在作負載均衡的時候,業界比較主流的方案,是經過Zookeeper的內存文件去實現全局鎖,可是若是引入了Zookeeper以後,既然自帶了發現服務,那Eureka的發現服務就顯得很雞肋。

先介紹一下Zookeeper的一些大概設計,Zookeeper主要是基於內存目錄結構,監聽者的設計模式。

主要的數據結構,以樹形的結構創建節點

image

咱們看一下Zookeeper主要提供的服務:

名稱服務
名稱服務是將一個名稱映射到與該名稱有關聯的一些信息的服務。電話目錄是將人的名字映射到其電話號碼的一個名稱服務。一樣,DNS 服務也是一個名稱服務,它將一個域名映射到一個 IP 地址。在分佈式系統中,您可能想跟蹤哪些服務器或服務在運行,並經過名稱查看其狀態。ZooKeeper 暴露了一個簡單的接口來完成此工做。也能夠將名稱服務擴展到組成員服務,這樣就能夠得到與正在查找其名稱的實體有關聯的組的信息。

鎖定
爲了容許在分佈式系統中對共享資源進行有序的訪問,可能須要實現分佈式互斥(distributed mutexes)。ZooKeeper 提供一種簡單的方式來實現它們。

同步
與互斥同時出現的是同步訪問共享資源的需求。不管是實現一個生產者-消費者隊列,仍是實現一個障礙,ZooKeeper 都提供一個簡單的接口來實現該操做。您能夠在 Apache ZooKeeper 維基上查看示例,瞭解如何作到這一點(參閱 參考資料)。

配置管理
您可使用 ZooKeeper 集中存儲和管理分佈式系統的配置。這意味着,全部新加入的節點都將在加入系統後就能夠當即使用來自 ZooKeeper 的最新集中式配置。這還容許您經過其中一個 ZooKeeper 客戶端更改集中式配置,集中地更改分佈式系統的狀態。

集羣管理
分佈式系統可能必須處理節點停機的問題,您可能想實現一個自動故障轉移策略。ZooKeeper 經過領導者選舉對此提供現成的支持。
若有多臺 Server 組成一個服務集羣,那麼必需要一個「總管(Zookeeper)」知道當前集羣中每臺機器的服務狀態,一旦有機器不能提供服務,集羣中其它集羣必須知道,從而作出調整從新分配服務策略。一樣當增長集羣的服務能力時,就會增長一臺或多臺 Server,一樣也必須讓「總管」知道。

全局鎖

共享鎖在同一個進程中很容易實現,可是在跨進程或者在不一樣 Server 之間就很差實現了。Zookeeper 卻很容易實現這個功能,實現方式也是須要得到鎖的 Server 建立一個 EPHEMERAL_SEQUENTIAL 目錄節點,而後調用 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是否是就是本身建立的目錄節點,若是正是本身建立的,那麼它就得到了這個鎖,若是不是那麼它就調用 exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到本身建立的節點是列表中最小編號的目錄節點,從而得到鎖,釋放鎖很簡單,只要刪除前面它本身所建立的目錄節點就好了。

配置管理:

將配置信息保存在 Zookeeper 的某個目錄節點中,而後將全部須要修改的應用機器監控配置信息的狀態,一旦配置信息發生變化,每臺應用機器就會收到 Zookeeper 的通知,而後從 Zookeeper 獲取新的配置信息應用到系統中。

使用spring Cloud 的方案:

  • 名稱服務: Eureka
  • 鎖定: redisson
  • 同步: mq
  • 配置管理: spring config Or Ctrip config
  • 集羣管理: 未知

由於集羣管理暫時沒有用到,因此最後拋棄了使用zookeeper。

參考網站

redisson

redisson教程

相關文章
相關標籤/搜索