通過
zookeeper java client api
去封裝連接zk
,以及獲取分佈式鎖
,還有釋放分佈式鎖的代碼。
zk分佈式鎖原理
- 通過去創建
zk
的一個臨時node
,來模擬給摸一個商品id加鎖 - zk保證只會創建一個臨時node,其他請求過來如果再要創建臨時node,就會報錯,
NodeExistsException
- 所謂上鎖,其實就是去創建
某個product id對應的一個臨時node
- 如果臨時node創建成功,說明成功加鎖,此時就可以去執行對redis立面數據的操作
- 如果臨時node創建失敗,說明有人已經在拿到鎖了操作reids中的數據,那麼就不斷的等待,直到自己可以獲取到鎖爲止
zk分佈式鎖的代碼封裝
項目地址參考:0. 【緩存高可用微服務實戰】資料總結
切換到相應分支:v0.4
zookeeper接口代碼封裝
- 基於
zk client api
,去封裝上面原理對應代碼邏輯; - 釋放一個分佈式鎖,去刪除掉那個臨時node就可以了,就代表釋放了一個鎖,那麼此時其他的機器就可以成功創建臨時node,獲取到鎖
- 依賴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 日誌依賴衝突
- 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();
}
}
業務代碼
主動更新
-
之前34. 【實戰】基於kafka+ehcache+redis完成緩存數據生產服務的開發與測試,已經實現
監聽kafka
消息隊列,獲取到一個商品變更的消息之後,去源服務中
調用接口拉取數據,更新到ehcache
和redis緩存
中的業務邏輯。 -
接着上面,更改邏輯,先
獲取分佈式鎖
,然後才能更新redis
,同時更新時要比較時間版本
;
被動重建
- 之前業務接口中獲取緩存如果不存在,直接讀取數據庫中的源頭數據,直接返回給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;
}
- 內存隊列循環更新緩存時,先獲取分佈式鎖,然後才能更新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());
}
}
}
測試
模擬基於分佈式鎖實現併發的。
- kafka producer 發出一個商品id=2的商品變更請求
- 拿到分佈式鎖後,更新redis緩存前,休眠60s,觀察效果
eshop-cache01
創建kafka producer
:
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