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分布式锁的代码封装

项目地址:eshop-study
切换到相应分支:
在这里插入图片描述

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值