learn.lianglianglee.com/专栏/分布式技术原理与实战45讲-完/33 消息幂等:如何保证消息不被重复消费?.md.html
2022-05-11 18:52:13 +08:00

1113 lines
30 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>33 消息幂等:如何保证消息不被重复消费?.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="/专栏/分布式技术原理与实战45讲-完/00 开篇词:搭建分布式知识体系,挑战高薪 Offer.md">00 开篇词:搭建分布式知识体系,挑战高薪 Offer.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/01 如何证明分布式系统的 CAP 理论?.md">01 如何证明分布式系统的 CAP 理论?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/02 不同数据一致性模型有哪些应用?.md">02 不同数据一致性模型有哪些应用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/03 如何透彻理解 Paxos 算法?.md">03 如何透彻理解 Paxos 算法?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/04 ZooKeeper 如何保证数据一致性?.md">04 ZooKeeper 如何保证数据一致性?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/05 共识问题:区块链如何确认记账权?.md">05 共识问题:区块链如何确认记账权?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/06 如何准备一线互联网公司面试?.md">06 如何准备一线互联网公司面试?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/07 分布式事务有哪些解决方案?.md">07 分布式事务有哪些解决方案?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/08 对比两阶段提交,三阶段协议有哪些改进?.md">08 对比两阶段提交,三阶段协议有哪些改进?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/09 MySQL 数据库如何实现 XA 规范?.md">09 MySQL 数据库如何实现 XA 规范?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/10 如何在业务中体现 TCC 事务模型?.md">10 如何在业务中体现 TCC 事务模型?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/11 分布式锁有哪些应用场景和实现?.md">11 分布式锁有哪些应用场景和实现?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/12 如何使用 Redis 快速实现分布式锁?.md">12 如何使用 Redis 快速实现分布式锁?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/13 分布式事务考点梳理 + 高频面试题.md">13 分布式事务考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/14 如何理解 RPC 远程服务调用?.md">14 如何理解 RPC 远程服务调用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/15 为什么微服务需要 API 网关?.md">15 为什么微服务需要 API 网关?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/16 如何实现服务注册与发现?.md">16 如何实现服务注册与发现?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/17 如何实现分布式调用跟踪?.md">17 如何实现分布式调用跟踪?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/18 分布式下如何实现配置管理?.md">18 分布式下如何实现配置管理?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/19 容器化升级对服务有哪些影响?.md">19 容器化升级对服务有哪些影响?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/20 ServiceMesh服务网格有哪些应用.md">20 ServiceMesh服务网格有哪些应用.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/21 Dubbo vs Spring Cloud两大技术栈如何选型.md">21 Dubbo vs Spring Cloud两大技术栈如何选型.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/22 分布式服务考点梳理 + 高频面试题.md">22 分布式服务考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/23 读写分离如何在业务中落地?.md">23 读写分离如何在业务中落地?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/24 为什么需要分库分表,如何实现?.md">24 为什么需要分库分表,如何实现?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/25 存储拆分后,如何解决唯一主键问题?.md">25 存储拆分后,如何解决唯一主键问题?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/26 分库分表以后,如何实现扩容?.md">26 分库分表以后,如何实现扩容?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/27 NoSQL 数据库有哪些典型应用?.md">27 NoSQL 数据库有哪些典型应用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/28 ElasticSearch 是如何建立索引的?.md">28 ElasticSearch 是如何建立索引的?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/29 分布式存储考点梳理 + 高频面试题.md">29 分布式存储考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/30 消息队列有哪些应用场景?.md">30 消息队列有哪些应用场景?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/31 集群消费和广播消费有什么区别?.md">31 集群消费和广播消费有什么区别?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/32 业务上需要顺序消费,怎么保证时序性?.md">32 业务上需要顺序消费,怎么保证时序性?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/分布式技术原理与实战45讲-完/33 消息幂等:如何保证消息不被重复消费?.md">33 消息幂等:如何保证消息不被重复消费?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/34 高可用:如何实现消息队列的 HA.md">34 高可用:如何实现消息队列的 HA.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/35 消息队列选型Kafka 如何实现高性能?.md">35 消息队列选型Kafka 如何实现高性能?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/36 消息队列选型RocketMQ 适用哪些场景?.md">36 消息队列选型RocketMQ 适用哪些场景?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/37 消息队列考点梳理 + 高频面试题.md">37 消息队列考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/38 不止业务缓存,分布式系统中还有哪些缓存?.md">38 不止业务缓存,分布式系统中还有哪些缓存?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/39 如何避免缓存穿透、缓存击穿、缓存雪崩?.md">39 如何避免缓存穿透、缓存击穿、缓存雪崩?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/40 经典问题:先更新数据库,还是先更新缓存?.md">40 经典问题:先更新数据库,还是先更新缓存?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/41 失效策略:缓存过期都有哪些策略?.md">41 失效策略:缓存过期都有哪些策略?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/42 负载均衡:一致性哈希解决了哪些问题?.md">42 负载均衡:一致性哈希解决了哪些问题?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/43 缓存高可用:缓存如何保证高可用?.md">43 缓存高可用:缓存如何保证高可用?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/44 分布式缓存考点梳理 + 高频面试题.md">44 分布式缓存考点梳理 + 高频面试题.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/45 从双十一看高可用的保障方式.md">45 从双十一看高可用的保障方式.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/46 高并发场景下如何实现系统限流?.md">46 高并发场景下如何实现系统限流?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/47 降级和熔断:如何增强服务稳定性?.md">47 降级和熔断:如何增强服务稳定性?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/48 如何选择适合业务的负载均衡策略?.md">48 如何选择适合业务的负载均衡策略?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/49 线上服务有哪些稳定性指标?.md">49 线上服务有哪些稳定性指标?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/50 分布式下有哪些好用的监控组件?.md">50 分布式下有哪些好用的监控组件?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/51 分布式下如何实现统一日志系统?.md">51 分布式下如何实现统一日志系统?.md.html</a>
</li>
<li>
<a href="/专栏/分布式技术原理与实战45讲-完/52 分布式路漫漫,厚积薄发才是王道.md">52 分布式路漫漫,厚积薄发才是王道.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>33 消息幂等:如何保证消息不被重复消费?</h1>
<p>应用的幂等是在分布式系统设计时必须要考虑的一个方面,如果对幂等没有额外的考虑,那么在消息失败重新投递,或者远程服务重试时,可能会出现许多诡异的问题。这一课时一起来看一下,在消息队列应用中,如何处理因为重复投递等原因导致的幂等问题。</p>
<h3>对业务幂等的理解</h3>
<p>首先明确一下,<strong>幂等并不是问题</strong>,而是业务的一个特性。幂等问题体现在对于不满足幂等性的业务,在消息重复消费,或者远程服务调用失败重试时,出现的数据不一致,业务数据错乱等现象。</p>
<p>幂等最早是一个数学上的概念,幂等函数指的是对一个函数或者方法,使用相同的参数执行多次,数据结果是一致的。</p>
<p>以 HTTP 协议为例,我们知道 HTTP 协议中定义了交互的不同方法,比如 GET 和 POST以及 PUT、DELETE 等,其中 GET、DELETE 等方法都是幂等的,而 POST 方法不是。</p>
<p>这个很好理解GET 方法用于获取资源不管调用多少次接口结果都不会改变所以是幂等的DELETE 等可以类比。</p>
<p>这里有一点需要注意,业务上的幂等指的是操作不影响资源本身,并不是每次读取的结果都保证一致。比如通过 GET 接口查询一条订单记录,在多次查询的时间段内,订单状态可能会有新的更新而发生变化,查询到的数据可能不同,但是读接口本身仍然是一个幂等的操作。</p>
<p>在业务开发中对数据的操作主要是 CRUD即在做数据处理时的 Create、Read、Update、Delete 这几种操作。很明显,这里的 Create 操作不是幂等的Update 操作可能幂等也可能不幂等。例如,现在有一个订单表,下面的操作就是幂等的:</p>
<pre><code>UPDATE order SET status=1 WHERE id=100
</code></pre>
<p>下面的这个操作,就不符合幂等性的要求:</p>
<pre><code>UPDATE order SET price=price+1 WHERE id=100
</code></pre>
<p>对应的Read 和 Delete 操作则是幂等的。</p>
<h3>各类中间件对幂等性的处理</h3>
<p>幂等处理不好,可能会出现很多问题,比如使用 binlog 分发进行数据同步,如果数据库更新消息被多次消费,可能会导致数据的不一致。</p>
<ul>
<li>远程服务调用的幂等问题</li>
</ul>
<p>因为存在网络抖动等,远程服务调用出现失败,一般是通过配置重试,保证请求调用成功率,提高整体服务的可用性。</p>
<p>以 Apache Dubbo 为例,我一直觉得 Dubbo 对容错的支持特别全面,它支持多种集群容错的方式,并且可以针对业务特性,配置不同的失败重试机制,包括 Failover 失败自动切换、Failsafe 失败安全、Failfast 快速失败等。比如在 Failover 下,失败会重试两次;在 Failfast 下,失败则不会重试,直接抛出异常。</p>
<p>Dubbo 的容错机制考虑了多种业务场景的需求,根据不同的业务场景,可以选择不同的容错机制,进而有不同的重试策略,保证业务正确性。</p>
<p>Dubbo RPC 的重试和容错机制不是本课时的重点,如果想对 Dubbo 集群容错方式有进一步的了解,可以点击查看 <a href="https://dubbo.apache.org/zh-cn/docs/user/quick-start.html">Dubbo 官方文档</a></p>
<ul>
<li>消息消费中的重试问题</li>
</ul>
<p>从本质上来讲,消息队列的消息发送重试,和微服务中的失败调用重试是一样的,都是通过重试的方式,解决网络抖动、传输不稳定等导致的偶发调用失败。这两者其实是一个问题,两个问题的解决方式也可以互相借鉴。</p>
<p>在分布式系统中,要解决这个问题,需从中间件和业务的不同层面,来保证服务调用的幂等性。下面从消息队列投递语义,以及业务中如何处理幂等,两个方面进行拆解。</p>
<h4>消息投递的几种语义</h4>
<p>为了进一步规范消息的调用,业界有许多消息队列的应用协议,其中也对消息投递标准做了一些约束。</p>
<ul>
<li>At most once</li>
</ul>
<p>消息在传递时,最多会被送达一次,在这种场景下,消息可能会丢,但绝不会重复传输,一般用于对消息可靠性没有太高要求的场景,比如一些允许数据丢失的日志报表、监控信息等。</p>
<ul>
<li>At least once</li>
</ul>
<p>消息在传递时,至少会被送达一次,在这种情况下,消息绝不会丢,但可能会出现重复传输。</p>
<p>绝大多数应用中,都是使用至少投递一次这种方式,同时,大部分消息队列都支持到这个级别,应用最广泛。</p>
<ul>
<li>Exactly once</li>
</ul>
<p>每条消息肯定会被传输一次且仅传输一次,并且保证送达,因为涉及发送端和生产端的各种协同机制,绝对的 Exactly once 级别是很难实现的,通用的 Exactly once 方案几乎不可能存在可以参考分布式系统的「FLP 不可能定理」。</p>
<p>我觉得消息投递的语义,和数据库的隔离级别很像,不同语义的实现,付出的成本也不一样。上面定义的消息投递语义,主要在消息发送端,在消费端也可以定义类似的消费语义,比如消费端保证最多被消费一次,至少被消费一次等,这两种语义是相对应的,可以认为是同一个级别的两种描述。</p>
<h4>不同消息队列支持的投递方式</h4>
<p>以 RocketMQ 为例,我们来看下对应的投递支持。</p>
<p>RocketMQ 支持 At least once 的投递语义,也就是保证每个消息至少被投递一次。在 RocketMQ 中,是通过消费端消费的 ACK 机制来实现的:</p>
<blockquote>
<p>在消息消费过程中,消费端在消息消费完成后,才返回 ACK如果消息已经 pull 到本地,但还没有消费,则不会返回 ack 响应。</p>
</blockquote>
<p>在业务上应用 RcoketMQ 时,也可以根据不同的业务场景实现其他级别的投递语义,比如最多送达一次等,由于篇幅限制这里不展开详细讲解了,感兴趣的同学可以查阅 RocketMQ 相关的源码和文档学习。</p>
<h3>业务上如何处理幂等</h3>
<p>消息消费的幂等和我们在上一课时中提到的时序性一样,本质上也是一个系统设计的问题。</p>
<p>消息队列是我们为了实现系统目标而引入的手段之一,并且分布式消息队列天然存在消费时序、消息失败重发等问题。所以要保证消息队列的消费幂等,还是要回到业务中,结合具体的设计方案解决。</p>
<h4>天然幂等不需要额外设计</h4>
<p>参考上面对 HTTP 协议方法的幂等性分析,有部分业务是天然幂等的,这部分业务,允许重复调用,即允许重试,在配置消息队列时,还可以通过合理的重试,来提高请求的成功率。</p>
<h4>利用数据库进行去重</h4>
<p>业务上的幂等操作可以添加一个过滤的数据库,比如设置一个去重表,也可以在数据库中通过唯一索引来去重。</p>
<p>举一个例子,现在要根据订单流转的消息在数据库中写一张订单 Log 表,我们可以把订单 ID 和修改时间戳做一个唯一索引进行约束。</p>
<p>当消费端消费消息出现重复投递时,会多次去订单 Log 表中进行写入,由于我们添加了唯一索引,除了第一条之外,后面的都会失败,这就从业务上保证了幂等,即使消费多次,也不会影响最终的数据结果。</p>
<h4>设置全局唯一消息 ID 或者任务 ID</h4>
<p>还记得我们在第 15 课时「分布式调用链跟踪」中,提到的调用链 ID 吗?调用链 ID 也可以应用在这里。我们在消息投递时,给每条业务消息附加一个唯一的消息 ID然后就可以在消费端利用类似分布式锁的机制实现唯一性的消费。</p>
<p>还是用上面记录订单状态流转消息的例子,我们在每条消息中添加一个唯一 ID消息被消费后在缓存中设置一个 Key 为对应的唯一 ID代表数据已经被消费当其他的消费端去消费时就可以根据这条记录来判断是否已经处理过。</p>
<h3>总结</h3>
<p>这一课时分享了消息幂等的知识点,包括对幂等的理解,以及消息队列投递时的不同语义,另外简单介绍了业务上处理幂等的两种方式。</p>
<p>西方有一句谚语:当你有了一个锤子,你看什么都像钉子。在我刚开始学习分布式系统时,学习了各种中间件,每个中间件都希望能用上,这其实脱离了系统设计的初衷。</p>
<p>课程内容到这里,已经展开了许多分布式系统的常用组件,提到这个谚语,主要是希望你在做技术方案,特别是做分布式系统设计方案时,不是为了设计而设计。方案设计的目的是<strong>实现业务目标</strong>,并不是在系统中加入各种高大上的中间件,这个方案就是正确的。</p>
<p>我之前读过一本《系统之美》的图书,从复杂系统的角度来看,系统中的元素越多,为了维持系统的平衡,需要付出的势能必然也越大。</p>
<p>对应到系统设计中,系统拆解的粒度越大,对应各个组件之间的耦合就越小,但是需要解决的组件协同问题也越多,实现数据的一致性也越困难。我们在系统设计时,要避免过度设计,把握技术方案的核心目的,在这个基础上进行针对性设计。</p>
<p>对于这一课时的内容,你可以思考下当前的项目中是如何处理重复消息的,有没有考虑消息处理的幂等性?欢迎留言分享。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/分布式技术原理与实战45讲-完/32 业务上需要顺序消费,怎么保证时序性?.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/分布式技术原理与实战45讲-完/34 高可用:如何实现消息队列的 HA.md">下一页</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":"709976e138bd3cfa","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>