通过
zookeeper java client api
去封装连接zk
,以及获取分布式锁
,还有释放分布式锁的代码。
zk分布式锁原理
- 通过去创建
zk
的一个临时node
,来模拟给摸一个商品id加锁 - zk保证只会创建一个临时node,其他请求过来如果再要创建临时node,就会报错,
NodeExistsException
- 所谓上锁,其实就是去创建
某个product id对应的一个临时node
- 如果临时node创建成功,说明成功加锁,此时就可以去执行对redis立面数据的操作
- 如果临时node创建失败,说明有人已经在拿到锁了操作reids中的数据,那么就不断的等待,直到自己可以获取到锁为止
zk分布式锁的代码封装
项目地址:eshop-study
切换到相应分支:
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