learn.lianglianglee.com/专栏/ZooKeeper源码分析与实战-完/01 ZooKeeper 数据模型:节点的特性与应用.md.html
2022-05-11 18:57:05 +08:00

927 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>01 ZooKeeper 数据模型:节点的特性与应用.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/00 开篇词:选择 ZooKeeper一步到位掌握分布式开发.md.html">00 开篇词:选择 ZooKeeper一步到位掌握分布式开发.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/ZooKeeper源码分析与实战-完/01 ZooKeeper 数据模型:节点的特性与应用.md.html">01 ZooKeeper 数据模型:节点的特性与应用.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/02 发布订阅模式:如何使用 Watch 机制实现分布式通知.md.html">02 发布订阅模式:如何使用 Watch 机制实现分布式通知.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/03 ACL 权限控制:如何避免未经授权的访问?.md.html">03 ACL 权限控制:如何避免未经授权的访问?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/04 ZooKeeper 如何进行序列化?.md.html">04 ZooKeeper 如何进行序列化?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/05 深入分析 Jute 的底层实现原理.md.html">05 深入分析 Jute 的底层实现原理.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/06 ZooKeeper 的网络通信协议详解.md.html">06 ZooKeeper 的网络通信协议详解.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/07 单机模式:服务器如何从初始化到对外提供服务?.md.html">07 单机模式:服务器如何从初始化到对外提供服务?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/08 集群模式:服务器如何从初始化到对外提供服务?.md.html">08 集群模式:服务器如何从初始化到对外提供服务?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/09 创建会话:避开日常开发的那些“坑”.md.html">09 创建会话:避开日常开发的那些“坑”.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/10 ClientCnxn客户端核心工作类工作原理解析.md.html">10 ClientCnxn客户端核心工作类工作原理解析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/11 分桶策略:如何实现高效的会话管理?.md.html">11 分桶策略:如何实现高效的会话管理?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/12 服务端是如何处理一次会话请求的?.md.html">12 服务端是如何处理一次会话请求的?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/13 Curator如何降低 ZooKeeper 使用的复杂性?.md.html">13 Curator如何降低 ZooKeeper 使用的复杂性?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/14 Leader 选举:如何保证分布式数据的一致性?.md.html">14 Leader 选举:如何保证分布式数据的一致性?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/15 ZooKeeper 究竟是怎么选中 Leader 的?.md.html">15 ZooKeeper 究竟是怎么选中 Leader 的?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/16 ZooKeeper 集群中 Leader 与 Follower 的数据同步策略.md.html">16 ZooKeeper 集群中 Leader 与 Follower 的数据同步策略.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/17 集群中 Leader 的作用:事务的请求处理与调度分析.md.html">17 集群中 Leader 的作用:事务的请求处理与调度分析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/18 集群中 Follow 的作用:非事务请求的处理与 Leader 的选举分析.md.html">18 集群中 Follow 的作用:非事务请求的处理与 Leader 的选举分析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/19 Observer 的作用与 Follow 有哪些不同?.md.html">19 Observer 的作用与 Follow 有哪些不同?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/20 一个运行中的 ZooKeeper 服务会产生哪些数据和文件?.md.html">20 一个运行中的 ZooKeeper 服务会产生哪些数据和文件?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/21 ZooKeeper 分布式锁:实现和原理解析.md.html">21 ZooKeeper 分布式锁:实现和原理解析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/22 基于 ZooKeeper 命名服务的应用:分布式 ID 生成器.md.html">22 基于 ZooKeeper 命名服务的应用:分布式 ID 生成器.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/23 使用 ZooKeeper 实现负载均衡服务器功能.md.html">23 使用 ZooKeeper 实现负载均衡服务器功能.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/24 ZooKeeper 在 Kafka 和 Dubbo 中的工业级实现案例分析.md.html">24 ZooKeeper 在 Kafka 和 Dubbo 中的工业级实现案例分析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/25 如何搭建一个高可用的 ZooKeeper 生产环境?.md.html">25 如何搭建一个高可用的 ZooKeeper 生产环境?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/26 JConsole 与四字母命令:如何监控服务器上 ZooKeeper 的运行状态?.md.html">26 JConsole 与四字母命令:如何监控服务器上 ZooKeeper 的运行状态?.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/27 crontab 与 PurgeTxnLog线上系统日志清理的最佳时间和方式.md.html">27 crontab 与 PurgeTxnLog线上系统日志清理的最佳时间和方式.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/28 彻底掌握二阶段提交三阶段提交算法原理.md.html">28 彻底掌握二阶段提交三阶段提交算法原理.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/29 ZAB 协议算法:崩溃恢复和消息广播.md.html">29 ZAB 协议算法:崩溃恢复和消息广播.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/30 ZAB 与 Paxos 算法的联系与区别.md.html">30 ZAB 与 Paxos 算法的联系与区别.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/31 ZooKeeper 中二阶段提交算法的实现分析.md.html">31 ZooKeeper 中二阶段提交算法的实现分析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/32 ZooKeeper 数据存储底层实现解析.md.html">32 ZooKeeper 数据存储底层实现解析.md.html</a>
</li>
<li>
<a href="/专栏/ZooKeeper源码分析与实战-完/33 结束语 分布技术发展与 ZooKeeper 应用前景.md.html">33 结束语 分布技术发展与 ZooKeeper 应用前景.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>01 ZooKeeper 数据模型:节点的特性与应用</h1>
<p>你好我是那朋ZooKeeper 专栏作者。</p>
<p>正如开篇词提到的ZooKeeper 作为一个分布式协调服务,给出了在分布式环境下一致性问题的工业解决方案,目前流行的很多开源框架技术背后都有 ZooKeeper 的身影。那么 ZooKeeper 是如何做到这一点的,在平时开发中我们应该如何使用 ZooKeeper要想了解这些问题我们先要对 ZooKeeper 的基础知识进行全面的掌握。</p>
<p>ZooKeeper 基础知识基本分为三大模块:</p>
<ul>
<li>数据模型</li>
<li>ACL 权限控制</li>
<li>Watch 监控</li>
</ul>
<p>其中,数据模型是最重要的,很多 ZooKeeper 中典型的应用场景都是利用这些基础模块实现的。比如我们可以利用数据模型中的临时节点和 Watch 监控机制来实现一个发布订阅的功能。</p>
<p>因此,今天主要通过理论知识结合实际的应用场景来给你介绍数据模型。掌握本课时的知识对于理解 ZooKeeper 内部原理,以及在日常工作中使用好 ZooKeeper 非常重要。</p>
<h3>数据模型</h3>
<p>计算机最根本的作用其实就是处理和存储数据作为一款分布式一致性框架ZooKeeper 也是如此。数据模型就是 ZooKeeper 用来存储和处理数据的一种逻辑结构。就像我们用 MySQL 数据库一样要想处理复杂业务。前提是先学会如何往里边新增数据。ZooKeeper 数据模型最根本的功能就像一个数据库。</p>
<p>现在,数据模型对我们来说还是一个比较抽象的概念,接下来我们开始部署一个开发测试环境,并在上面做一些简单的操作。来看看 ZooKeeper 的数据模型究竟是什么样的:</p>
<ol>
<li>配置文件</li>
</ol>
<pre><code class="language-java">tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
</code></pre>
<ol>
<li>服务启动</li>
</ol>
<pre><code class="language-java">bin/zkServer.sh start
</code></pre>
<ol>
<li>使用客户端连接服务器</li>
</ol>
<pre><code class="language-java">bin/zkCli.sh -server 127.0.0.1:2181
</code></pre>
<ol>
<li>这样单机版的开发环境就已经构建完成了,接下来我们通过 ZooKeeper 提供的 create 命令来创建几个节点,分别是:“/locks”“/servers”“/works”</li>
</ol>
<pre><code class="language-java">create /locks
create /servers
create /works
</code></pre>
<p>最终在 ZooKeeper 服务器上会得到一个具有层级关系的数据结构,如下图所示,这个数据结构就是 ZooKeeper 中的数据模型。</p>
<p><img src="assets/Ciqc1F6yL9OAUUguAAAtBwGI74E989.png" alt="image" /></p>
<p>ZooKeeper 中的数据模型是一种树形结构非常像电脑中的文件系统有一个根文件夹下面还有很多子文件夹。ZooKeeper 的数据模型也具有一个固定的根节点(/我们可以在根节点下创建子节点并在子节点下继续创建下一级节点。ZooKeeper 树中的每一层级用斜杠(/分隔开且只能用绝对路径如“get /work/task1”的方式查询 ZooKeeper 节点,而不能使用相对路径。具体的结构你可以看看下面这张图:</p>
<p><img src="assets/CgqCHl6yL9uAbpHYAABF_GHyGNc950.png" alt="image" /></p>
<h3>znode 节点类型与特性</h3>
<p>知道了 ZooKeeper 的数据模型是一种树形结构,就像在 MySQL 中数据是存在于数据表中ZooKeeper 中的数据是由多个数据节点最终构成的一个层级的树状结构,和我们在创建 MySOL 数据表时会定义不同类型的数据列字段ZooKeeper 中的数据节点也分为持久节点、临时节点和有序节点三种类型:</p>
<h4>1、持久节点</h4>
<p>我们第一个介绍的是持久节点,这种节点也是在 ZooKeeper 最为常用的,几乎所有业务场景中都会包含持久节点的创建。之所以叫作持久节点是因为一旦将节点创建为持久节点,该数据节点会一直存储在 ZooKeeper 服务器上,即使创建该节点的客户端与服务端的会话关闭了,该节点依然不会被删除。如果我们想删除持久节点,就要显式调用 delete 函数进行删除操作。</p>
<h4>2、临时节点</h4>
<p>接下来我们来介绍临时节点。从名称上我们可以看出该节点的一个最重要的特性就是临时性。所谓临时性是指,如果将节点创建为临时节点,那么该节点数据不会一直存储在 ZooKeeper 服务器上。当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在 ZooKeeper 服务器上被删除。同样,我们可以像删除持久节点一样主动删除临时节点。</p>
<p>在平时的开发中,我们可以利用临时节点的这一特性来做服务器集群内机器运行情况的统计,将集群设置为“/servers”节点并为集群下的每台服务器创建一个临时节点“/servers/host”当服务器下线时该节点自动被删除最后统计临时节点个数就可以知道集群中的运行情况。如下图所示</p>
<p><img src="assets/CgqCHl6yL-SAb0zaAABQBLohKvo019.png" alt="image" /></p>
<h4>3、有序节点</h4>
<p>最后我们再说一下有序节点其实有序节点并不算是一种单独种类的节点而是在之前提到的持久节点和临时节点特性的基础上增加了一个节点有序的性质。所谓节点有序是说在我们创建有序节点的时候ZooKeeper 服务器会自动使用一个单调递增的数字作为后缀,追加到我们创建节点的后边。例如一个客户端创建了一个路径为 works/task- 的有序节点,那么 ZooKeeper 将会生成一个序号并追加到该节点的路径后,最后该节点的路径为 works/task-1。通过这种方式我们可以直观的查看到节点的创建顺序。</p>
<p>到目前为止我们知道在 ZooKeeper 服务器上存储数据的基本信息,知道了 ZooKeeper 中的数据节点种类有持久节点和临时节点等。上述这几种数据节点虽然类型不同,但 ZooKeeper 中的每个节点都维护有这些内容一个二进制数组byte data[]用来存储节点的数据、ACL 访问控制信息、子节点数据(因为临时节点不允许有子节点,所以其子节点字段为 null除此之外每个数据节点还有一个记录自身状态信息的字段 stat。</p>
<p>下面我们详细说明节点的状态信息。</p>
<h3>节点的状态结构</h3>
<p>每个节点都有属于自己的状态信息,这就很像我们每个人的身份信息一样,我们打开之前的客户端,执行 stat /zk_test可以看到控制台输出了一些信息这些就是节点状态信息。</p>
<p><img src="assets/Ciqc1F6yL-yAKn9QAABsJSpQkFI688.png" alt="image" /></p>
<p>每一个节点都有一个自己的状态属性,记录了节点本身的一些信息,这些属性包括的内容我列在了下面这个表格里:</p>
<p><img src="assets/Ciqc1F6zbwWAVkt5AAC_yMQVCFo712.png" alt="表.png" /></p>
<h3>数据节点的版本</h3>
<p>这里我们重点讲解一下版本相关的属性,在 ZooKeeper 中为数据节点引入了版本的概念,每个数据节点有 3 种类型的版本信息对数据节点的任何更新操作都会引起版本号的变化。ZooKeeper 的版本信息表示的是对节点数据内容、子节点信息或者是 ACL 信息的修改次数。</p>
<h4>使用 ZooKeeper 实现锁</h4>
<p>学习了 ZooKeeper 的数据模型和数据节点的相关知识,下面我们通过实际的应用进一步加深理解。</p>
<p>设想这样一个情景:一个购物网站,某个商品库存只剩一件,客户 A 搜索到这件商品并准备下单,但在这期间客户 B 也查询到了该件商品并提交了购买,于此同时,客户 A 也下单购买了此商品,这样就出现了只有一件库存的商品实际上卖出了两件的情况。为了解决这个问题,我们可以在客户 A 对商品进行操作的时候对这件商品进行锁定从而避免这种超卖的情况发生。</p>
<p>实现锁的方式有很多中,这里我们主要介绍两种:悲观锁、乐观锁。</p>
<p><strong>悲观锁</strong>
悲观锁认为进程对临界区的竞争总是会出现,为了保证进程在操作数据时,该条数据不被其他进程修改。数据会一直处于被锁定的状态。</p>
<p>我们假设一个具有 n 个进程的应用,同时访问临界区资源,我们通过进程创建 ZooKeeper 节点 /locks 的方式获取锁。</p>
<p>线程 a 通过成功创建 ZooKeeper 节点“/locks”的方式获取锁后继续执行如下图所示</p>
<p><img src="assets/CgqCHl6yL_WAAnymAAB32xbrhxQ973.png" alt="image" /></p>
<p>这时进程 b 也要访问临界区资源,于是进程 b 也尝试创建“/locks”节点来获取锁因为之前进程 a 已经创建该节点,所以进程 b 创建节点失败无法获得锁。</p>
<p><img src="assets/CgqCHl6yL_6AOIONAAB3daUjikw147.png" alt="image" /></p>
<p>这样就实现了一个简单的悲观锁,不过这也有一个隐含的问题,就是当进程 a 因为异常中断导致 /locks 节点始终存在,其他线程因为无法再次创建节点而无法获取锁,这就产生了一个死锁问题。针对这种情况我们可以通过将节点设置为临时节点的方式避免。并通过在服务器端添加监听事件来通知其他进程重新获取锁。</p>
<p><strong>乐观锁</strong>
乐观锁认为,进程对临界区资源的竞争不会总是出现,所以相对悲观锁而言。加锁方式没有那么激烈,不会全程的锁定资源,而是在数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则拒绝操作。</p>
<p>**乐观锁基本可以分为读取、校验、写入三个步骤。**CASCompare-And-Swap即比较并替换就是一个乐观锁的实现。CAS 有 3 个操作数,内存值 V旧的预期值 A要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B否则什么都不做。</p>
<p>在 ZooKeeper 中的 version 属性就是用来实现乐观锁机制中的“校验”的ZooKeeper 每个节点都有数据版本的概念,在调用更新操作的时候,假如有一个客户端试图进行更新操作,它会携带上次获取到的 version 值进行更新。而如果在这段时间内ZooKeeper 服务器上该节点的数值恰好已经被其他客户端更新了,那么其数据版本一定也会发生变化,因此肯定与客户端携带的 version 无法匹配,便无法成功更新,因此可以有效地避免一些分布式更新的并发问题。</p>
<p>在 ZooKeeper 的底层实现中,当服务端处理 setDataRequest 请求时,首先会调用 checkAndIncVersion 方法进行数据版本校验。ZooKeeper 会从 setDataRequest 请求中获取当前请求的版本 version同时通过 getRecordForPath 方法获取服务器数据记录 nodeRecord 从中得到当前服务器上的版本信息 currentversion。如果 version 为 -1表示该请求操作不使用乐观锁可以忽略版本对比如果 version 不是 -1那么就对比 version 和 currentversion如果相等则进行更新操作否则就会抛出 BadVersionException 异常中断操作。</p>
<p><img src="assets/CgqCHl6yMBKAZzwGAABPrrtajyI575.png" alt="image" /></p>
<h3>总结</h3>
<p>本节课主要介绍了ZooKeeper的基础知识点——数据模型。并深入介绍了节点类型、stat 状态属性等知识,并利用目前学到的知识解决了集群中服务器运行情况统计、悲观锁、乐观锁等问题。这些知识对接下来的课程至关重要,请务必掌握。</p>
<p>了解了 ZooKeeper 数据模型的基本原理后,我们来思考一个问题:<strong>为什么 ZooKeeper 不能采用相对路径查找节点呢?</strong></p>
<p>这是因为 ZooKeeper 大多是应用场景是定位数据模型上的节点,并在相关节点上进行操作。像这种查找与给定值相等的记录问题最适合用散列来解决。因此 ZooKeeper 在底层实现的时候,使用了一个 hashtable即 hashtableConcurrentHashMap&lt;String, DataNode&gt; nodes ,用节点的完整路径来作为 key 存储节点数据。这样就大大提高了 ZooKeeper 的性能。</p>
<blockquote>
<p>引用参考
官网:
<a href="https://zookeeper.apache.org/doc/r3.6.0/zookeeperProgrammers.html">https://zookeeper.apache.org/doc/r3.6.0/zookeeperProgrammers.html</a></p>
</blockquote>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/ZooKeeper源码分析与实战-完/00 开篇词:选择 ZooKeeper一步到位掌握分布式开发.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/ZooKeeper源码分析与实战-完/02 发布订阅模式:如何使用 Watch 机制实现分布式通知.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709975c36e973cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>