40. 【實戰】緩存數據生產服務zk分佈式鎖解決方案的代碼實現

通過zookeeper java client api去封裝連接zk,以及獲取分佈式鎖,還有釋放分佈式鎖的代碼。

zk分佈式鎖原理

  1. 通過去創建zk的一個臨時node,來模擬給摸一個商品id加鎖
  2. zk保證只會創建一個臨時node,其他請求過來如果再要創建臨時node,就會報錯,NodeExistsException
  3. 所謂上鎖,其實就是去創建某個product id對應的一個臨時node
  4. 如果臨時node創建成功,說明成功加鎖,此時就可以去執行對redis立面數據的操作
  5. 如果臨時node創建失敗,說明有人已經在拿到鎖了操作reids中的數據,那麼就不斷的等待,直到自己可以獲取到鎖爲止

zk分佈式鎖的代碼封裝

項目地址參考:0. 【緩存高可用微服務實戰】資料總結
切換到相應分支:v0.4
在這裏插入圖片描述

zookeeper接口代碼封裝

  1. 基於zk client api,去封裝上面原理對應代碼邏輯;
  2. 釋放一個分佈式鎖,去刪除掉那個臨時node就可以了,就代表釋放了一個鎖,那麼此時其他的機器就可以成功創建臨時node,獲取到鎖
  3. 依賴pom.xml
<dependency>
	<groupId>org.apache.zookeeper</groupId>
	<artifactId>zookeeper</artifactId>
	<version>3.4.5</version>
	<exclusions>
		<exclusion>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
		</exclusion>
	</exclusions>
</dependency>

kafka 和 zookeeper 日誌依賴衝突

  1. zookeeper api 代碼類

實現zookeeper連接,分佈式鎖獲取和釋放。

/**
 * 功能描述: ZooKeeperSession   -- 實現靜態內部單例
 * <p>
 * 作者: luohq
 * 日期: 2020/3/10 21:50
 */
public class ZooKeeperSession {

    private static CountDownLatch connectedSemaphore = new CountDownLatch(1);

    private ZooKeeper zooKeeper;

    public ZooKeeperSession() {
        // 連接 zookeeper server,創建會話的時候,是異步進行的
        // 所以要設置一個監聽器watcher,告訴我們什麼時候完成了zk server的連接
        try {
            this.zooKeeper = new ZooKeeper(
                    "192.168.0.106:2181,192.168.0.107:2181,192.168.0.108:2181",
                    50000,
                    new ZooKeeperWatcher()
            );
            // 給一個狀態 CONNECTING,連接中
            System.out.println(zooKeeper.getState());
            try {
                // CountDownLatch
                // java多線程併發同步一個工具類,會傳遞進去一些數字,比如1,2,3都可以
                // 執行await(),如果當前數字非0,阻塞住,等待
                // 其他線程執行後,調用countDown(),減1,如果數字減到0,之前await的所有線程
                // 脫離阻塞狀態,競爭執行
                connectedSemaphore.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Zookeeper session established......");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** 
     * @Author luohongquan
     * @Description 獲取分佈式鎖
     * @Date 22:14 2020/3/10
     * @Param [productId]
     * @return void
     */
    public void acquireDistributedLock(Long productId) {
        String path = "/product-lock-" + productId;
        try {
            zooKeeper.create(
                    path,   // 目錄
                    "".getBytes(),  // 節點內容爲空
                    ZooDefs.Ids.OPEN_ACL_UNSAFE,   // 權限公開
                    CreateMode.EPHEMERAL);  // 臨時節點
            System.out.format("success to acquire lock for product[id=%d]", productId);
        } catch (Exception e) {
            // 如果商品id對應的鎖node,已經存在,就是已經被別人獲取加鎖,這裏報錯
            // NodeExistsException
            // 這裏循環等待獲取鎖,一直到成功爲止
            int count = 0;
            while (true) {
                try {
                    Thread.sleep(20);   // 每次嘗試獲取鎖前等待20ms
                    zooKeeper.create(
                            path,   // 目錄
                            "".getBytes(),  // 節點內容爲空
                            ZooDefs.Ids.OPEN_ACL_UNSAFE,   // 權限公開
                            CreateMode.EPHEMERAL);  // 臨時節點
                } catch (Exception e2) {
                    e2.printStackTrace();
                    count++;
                    continue;
                }
                System.out.format("success to acquire lock for product[id=%d] after %d times try......",
                        productId, count);
                break;
            }
        }
    }

    /**
     * @Author luohongquan
     * @Description 釋放掉一個分佈式鎖
     * @Date 22:24 2020/3/10
     * @Param [productId]
     * @return void
     */
    public void releaseDistributedLock(Long productId) {
        String path = "/product-lock-" + productId;
        try {
            zooKeeper.delete(path, -1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** 
     * @Author luohongquan
     * @Description zookeeper連接狀態監聽類
     * @Date 22:12 2020/3/10
     * @Param 
     * @return 
     */
    private class ZooKeeperWatcher implements Watcher {
        @Override
        public void process(WatchedEvent event) {
            System.out.println("Receive watched event: " + event.getState());
            if (Event.KeeperState.SyncConnected == event.getState()) {
                connectedSemaphore.countDown();
            }
        }
    }
    
    /** 
     * @Author luohongquan
     * @Description 封裝的靜態內部單例類
     * @Date 21:54 2020/3/10
     * @Param 
     * @return 
     */
    private static class Singleton {
        private static ZooKeeperSession instance;
        static {
            instance = new ZooKeeperSession();
        }
        
        public static ZooKeeperSession getInstance() {
            return instance;
        }
    }

    /** 
     * @Author luohongquan
     * @Description 獲取單例
     * @Date 21:56 2020/3/10
     * @Param []
     * @return com.roncoo.eshop.cache.zk.ZooKeeperSession
     */
    public static ZooKeeperSession getInstance() {
        return Singleton.getInstance();
    }

    /**
     * @Author luohongquan
     * @Description 初始化單例的便捷方法
     * @Date 21:57 2020/3/10
     * @Param []
     * @return void
     */
    public static void init() {
        getInstance();
    }
}

業務代碼

主動更新

  1. 之前34. 【實戰】基於kafka+ehcache+redis完成緩存數據生產服務的開發與測試,已經實現監聽kafka消息隊列,獲取到一個商品變更的消息之後,去源服務中調用接口拉取數據,更新到ehcacheredis緩存中的業務邏輯。

  2. 接着上面,更改邏輯,先獲取分佈式鎖,然後才能更新redis,同時更新時要比較時間版本

被動重建

  1. 之前業務接口中獲取緩存如果不存在,直接讀取數據庫中的源頭數據,直接返回給nginx,同時推送一條消息到一個隊列,後臺線程異步消費重建緩存。

我們這裏模擬數據庫查詢,並且通過內存隊列模擬消息消費隊列

@RequestMapping("getProductInfo")
    @ResponseBody
    public ProductInfo getProductInfo(Long productId) {
	    // 1. 先從redis獲取
	    ProductInfo productInfo = cacheService.getProductInfoFromRedisCache(productId);
        System.out.println("==========================從redis中獲取緩存,商品信息=" + productInfo);

	    // 2. 如果爲空,從本地緩存ehcache獲取
	    if (null == productInfo) {
	        productInfo = cacheService.getProductInfoFromLocalCache(productId);
            System.out.println("==========================從ehcache中獲取緩存,商品信息=" + productInfo);
        }

        // 3. 如果還爲空,從數據庫里拉取數據,重建緩存,暫時不講
        if (null == productInfo) {
            // 模擬從數據庫中查詢的數據
            String productInfoJSON = "{\"id\": 1, \"name\": \"iphone7手機\", \"price\": 5599, " +
                    "\"pictureList\":\"a.jpg,b.jpg\", \"specification\": \"iphone7的規格\", " +
                    "\"service\": \"iphone7的售後服務\", \"color\": \"紅色,白色,黑色\", " +
                    "\"size\": \"5.5\", \"shopId\": 1, \"modifiedTime\": \"2020-03-10 22:01:00\"}";
            productInfo = JSONObject.parseObject(productInfoJSON, ProductInfo.class);
            // 將數據推送到一個內存隊列中
            RebuildCacheQueue rebuildCacheQueue = RebuildCacheQueue.getInstance();
            rebuildCacheQueue.putProductInfo(productInfo);
        }
        // 返回 nginx
        return productInfo;
    }
  1. 內存隊列循環更新緩存時,先獲取分佈式鎖,然後才能更新redis,同時要比較時間版本

這裏通過線程循環消費內存隊列,模擬消息消費

/**
 * 功能描述: 緩存重建線程
 * <p>
 * 作者: luohq
 * 日期: 2020/3/10 23:05
 */
public class RebuildCacheThread implements Runnable {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public void run() {
        RebuildCacheQueue rebuildCacheQueue = RebuildCacheQueue.getInstance();
        ZooKeeperSession zkSession = ZooKeeperSession.getInstance();
        CacheService cacheService = (CacheService) SpringContext.getApplicationContext()
                .getBean("cacheService");

        while(true) {
            ProductInfo productInfo = rebuildCacheQueue.takeProductInfo();

            zkSession.acquireDistributedLock(productInfo.getId());

            ProductInfo existedProductInfo = cacheService.getProductInfoFromRedisCache(productInfo.getId());

            if(existedProductInfo != null) {
                // 比較當前數據的時間版本比已有數據的時間版本是新還是舊
                try {
                    Date date = sdf.parse(productInfo.getModifiedTime());
                    Date existedDate = sdf.parse(existedProductInfo.getModifiedTime());

                    if(date.before(existedDate)) {
                        System.out.println("current date[" + productInfo.getModifiedTime() + "] is before existed date[" + existedProductInfo.getModifiedTime() + "]");
                        continue;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("current date[" + productInfo.getModifiedTime() + "] is after existed date[" + existedProductInfo.getModifiedTime() + "]");
            } else {
                System.out.println("existed product info is null......");
            }

            cacheService.saveProductInfo2LocalCache(productInfo);
            cacheService.saveProductInfo2RedisCache(productInfo);

            zkSession.releaseDistributedLock(productInfo.getId());
        }
    }
}

測試

模擬基於分佈式鎖實現併發的。

  1. kafka producer 發出一個商品id=2的商品變更請求
  2. 拿到分佈式鎖後,更新redis緩存前,休眠60s,觀察效果
  3. eshop-cache01 創建kafka producer

34. 【實戰】基於kafka+ehcache+redis完成緩存數據生產服務的開發與測試

cd /usr/local/kafka

bin/kafka-console-producer.sh --broker-list 192.168.0.106:9092,192.168.0.107:9092,192.168.0.108:9092 --topic cache-message

4.兩個併發:

  • kafka producer 發送一個獲取新的商品id=6的請求(保證ehcache,redis沒有改商品id緩存)
{"serviceId":"productInfoService","productId":6}
  • http請求獲取商品信息
http://localhost:8080/getProductInfo?productId=6

在這裏插入圖片描述

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