mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 04:36:41 +08:00
1379 lines
40 KiB
HTML
1379 lines
40 KiB
HTML
<!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>15 答疑文章(一):日志和索引相关问题.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="/专栏/MySQL实战45讲/00 开篇词 这一次,让我们一起来搞懂MySQL.md.html">00 开篇词 这一次,让我们一起来搞懂MySQL.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/01 基础架构:一条SQL查询语句是如何执行的?.md.html">01 基础架构:一条SQL查询语句是如何执行的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/02 日志系统:一条SQL更新语句是如何执行的?.md.html">02 日志系统:一条SQL更新语句是如何执行的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/03 事务隔离:为什么你改了我还看不见?.md.html">03 事务隔离:为什么你改了我还看不见?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/04 深入浅出索引(上).md.html">04 深入浅出索引(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/05 深入浅出索引(下).md.html">05 深入浅出索引(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?.md.html">06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/07 行锁功过:怎么减少行锁对性能的影响?.md.html">07 行锁功过:怎么减少行锁对性能的影响?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/08 事务到底是隔离的还是不隔离的?.md.html">08 事务到底是隔离的还是不隔离的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/09 普通索引和唯一索引,应该怎么选择?.md.html">09 普通索引和唯一索引,应该怎么选择?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/10 MySQL为什么有时候会选错索引?.md.html">10 MySQL为什么有时候会选错索引?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/11 怎么给字符串字段加索引?.md.html">11 怎么给字符串字段加索引?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/12 为什么我的MySQL会“抖”一下?.md.html">12 为什么我的MySQL会“抖”一下?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/13 为什么表数据删掉一半,表文件大小不变?.md.html">13 为什么表数据删掉一半,表文件大小不变?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/14 count()这么慢,我该怎么办?.md.html">14 count()这么慢,我该怎么办?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/MySQL实战45讲/15 答疑文章(一):日志和索引相关问题.md.html">15 答疑文章(一):日志和索引相关问题.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/16 “order by”是怎么工作的?.md.html">16 “order by”是怎么工作的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/17 如何正确地显示随机消息?.md.html">17 如何正确地显示随机消息?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/18 为什么这些SQL语句逻辑相同,性能却差异巨大?.md.html">18 为什么这些SQL语句逻辑相同,性能却差异巨大?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/19 为什么我只查一行的语句,也执行这么慢?.md.html">19 为什么我只查一行的语句,也执行这么慢?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/20 幻读是什么,幻读有什么问题?.md.html">20 幻读是什么,幻读有什么问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/21 为什么我只改一行的语句,锁这么多?.md.html">21 为什么我只改一行的语句,锁这么多?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/22 MySQL有哪些“饮鸩止渴”提高性能的方法?.md.html">22 MySQL有哪些“饮鸩止渴”提高性能的方法?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/23 MySQL是怎么保证数据不丢的?.md.html">23 MySQL是怎么保证数据不丢的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/24 MySQL是怎么保证主备一致的?.md.html">24 MySQL是怎么保证主备一致的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/25 MySQL是怎么保证高可用的?.md.html">25 MySQL是怎么保证高可用的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/26 备库为什么会延迟好几个小时?.md.html">26 备库为什么会延迟好几个小时?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/27 主库出问题了,从库怎么办?.md.html">27 主库出问题了,从库怎么办?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/28 读写分离有哪些坑?.md.html">28 读写分离有哪些坑?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/29 如何判断一个数据库是不是出问题了?.md.html">29 如何判断一个数据库是不是出问题了?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/30 答疑文章(二):用动态的观点看加锁.md.html">30 答疑文章(二):用动态的观点看加锁.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/31 误删数据后除了跑路,还能怎么办?.md.html">31 误删数据后除了跑路,还能怎么办?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/32 为什么还有kill不掉的语句?.md.html">32 为什么还有kill不掉的语句?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/33 我查这么多数据,会不会把数据库内存打爆?.md.html">33 我查这么多数据,会不会把数据库内存打爆?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/34 到底可不可以使用join?.md.html">34 到底可不可以使用join?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/35 join语句怎么优化?.md.html">35 join语句怎么优化?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/36 为什么临时表可以重名?.md.html">36 为什么临时表可以重名?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/37 什么时候会使用内部临时表?.md.html">37 什么时候会使用内部临时表?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/38 都说InnoDB好,那还要不要使用Memory引擎?.md.html">38 都说InnoDB好,那还要不要使用Memory引擎?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/39 自增主键为什么不是连续的?.md.html">39 自增主键为什么不是连续的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/40 insert语句的锁为什么这么多?.md.html">40 insert语句的锁为什么这么多?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/41 怎么最快地复制一张表?.md.html">41 怎么最快地复制一张表?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/42 grant之后要跟着flush privileges吗?.md.html">42 grant之后要跟着flush privileges吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/43 要不要使用分区表?.md.html">43 要不要使用分区表?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/44 答疑文章(三):说一说这些好问题.md.html">44 答疑文章(三):说一说这些好问题.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/45 自增id用完怎么办?.md.html">45 自增id用完怎么办?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/我的MySQL心路历程.md.html">我的MySQL心路历程.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/MySQL实战45讲/结束语 点线网面,一起构建MySQL知识网络.md.html">结束语 点线网面,一起构建MySQL知识网络.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>15 答疑文章(一):日志和索引相关问题</h1>
|
||
|
||
<p>在今天这篇答疑文章更新前,MySQL 实战这个专栏已经更新了 14 篇。在这些文章中,大家在评论区留下了很多高质量的留言。现在,每篇文章的评论区都有热心的同学帮忙总结文章知识点,也有不少同学提出了很多高质量的问题,更有一些同学帮忙解答其他同学提出的问题。</p>
|
||
|
||
<p>在浏览这些留言并回复的过程中,我倍受鼓舞,也尽我所知地帮助你解决问题、和你讨论。可以说,你们的留言活跃了整个专栏的氛围、提升了整个专栏的质量,谢谢你们。</p>
|
||
|
||
<p>评论区的大多数留言我都直接回复了,对于需要展开说明的问题,我都拿出小本子记了下来。这些被记下来的问题,就是我们今天这篇答疑文章的素材了。</p>
|
||
|
||
<p>到目前为止,我已经收集了 47 个问题,很难通过今天这一篇文章全部展开。所以,我就先从中找了几个联系非常紧密的问题,串了起来,希望可以帮你解决关于日志和索引的一些疑惑。而其他问题,我们就留着后面慢慢展开吧。</p>
|
||
|
||
<h1>日志相关问题</h1>
|
||
|
||
<p>我在第 2 篇文章[《日志系统:一条 SQL 更新语句是如何执行的?》]中,和你讲到 binlog(归档日志)和 redo log(重做日志)配合崩溃恢复的时候,用的是反证法,说明了如果没有两阶段提交,会导致 MySQL 出现主备数据不一致等问题。</p>
|
||
|
||
<p>在这篇文章下面,很多同学在问,在两阶段提交的不同瞬间,MySQL 如果发生异常重启,是怎么保证数据完整性的?</p>
|
||
|
||
<p>现在,我们就从这个问题开始吧。</p>
|
||
|
||
<p>我再放一次两阶段提交的图,方便你学习下面的内容。</p>
|
||
|
||
<p><img src="assets/ee9af616e05e4b853eba27048351f62a.jpg" alt="img" /></p>
|
||
|
||
<p>图 1 两阶段提交示意图</p>
|
||
|
||
<p>这里,我要先和你解释一个误会式的问题。有同学在评论区问到,这个图不是一个 update 语句的执行流程吗,怎么还会调用 commit 语句?</p>
|
||
|
||
<p>他产生这个疑问的原因,是把<strong>两个“commit”的概念</strong>混淆了:</p>
|
||
|
||
<ul>
|
||
|
||
<li>他说的“commit 语句”,是指 MySQL 语法中,用于提交一个事务的命令。一般跟 begin/start transaction 配对使用。</li>
|
||
|
||
<li>而我们图中用到的这个“commit 步骤”,指的是事务提交过程中的一个小步骤,也是最后一步。当这个步骤执行完成后,这个事务就提交完成了。</li>
|
||
|
||
<li>“commit 语句”执行的时候,会包含“commit 步骤”。</li>
|
||
|
||
</ul>
|
||
|
||
<p>而我们这个例子里面,没有显式地开启事务,因此这个 update 语句自己就是一个事务,在执行完成后提交事务时,就会用到这个“commit 步骤“。</p>
|
||
|
||
<p>接下来,我们就一起分析一下<strong>在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象。</strong></p>
|
||
|
||
<p>如果在图中时刻 A 的地方,也就是写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。到这里,大家都可以理解。</p>
|
||
|
||
<p>大家出现问题的地方,主要集中在时刻 B,也就是 binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理?</p>
|
||
|
||
<p>我们先来看一下崩溃恢复时的判断规则。</p>
|
||
|
||
<ol>
|
||
|
||
<li>如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;</li>
|
||
|
||
<li>如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:
|
||
|
||
a. 如果是,则提交事务;
|
||
|
||
b. 否则,回滚事务。</li>
|
||
|
||
</ol>
|
||
|
||
<p>这里,时刻 B 发生 crash 对应的就是 2(a) 的情况,崩溃恢复过程中事务会被提交。</p>
|
||
|
||
<p>现在,我们继续延展一下这个问题。</p>
|
||
|
||
<h2>追问 1:MySQL 怎么知道 binlog 是完整的?</h2>
|
||
|
||
<p>回答:一个事务的 binlog 是有完整格式的:</p>
|
||
|
||
<ul>
|
||
|
||
<li>statement 格式的 binlog,最后会有 COMMIT;</li>
|
||
|
||
<li>row 格式的 binlog,最后会有一个 XID event。</li>
|
||
|
||
</ul>
|
||
|
||
<p>另外,在 MySQL 5.6.2 版本以后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL 可以通过校验 checksum 的结果来发现。所以,MySQL 还是有办法验证事务 binlog 的完整性的。</p>
|
||
|
||
<h2>追问 2:redo log 和 binlog 是怎么关联起来的?</h2>
|
||
|
||
<p>回答:它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log:</p>
|
||
|
||
<ul>
|
||
|
||
<li>如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;</li>
|
||
|
||
<li>如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。</li>
|
||
|
||
</ul>
|
||
|
||
<h2>追问 3:处于 prepare 阶段的 redo log 加上完整 binlog,重启就能恢复,MySQL 为什么要这么设计?</h2>
|
||
|
||
<p>回答:其实,这个问题还是跟我们在反证法中说到的数据与备份的一致性有关。在时刻 B,也就是 binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。</p>
|
||
|
||
<p>所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。</p>
|
||
|
||
<h2>追问 4:如果这样的话,为什么还要两阶段提交呢?干脆先 redo log 写完,再写 binlog。崩溃恢复的时候,必须得两个日志都完整才可以。是不是一样的逻辑?</h2>
|
||
|
||
<p>回答:其实,两阶段提交是经典的分布式系统问题,并不是 MySQL 独有的。</p>
|
||
|
||
<p>如果必须要举一个场景,来说明这么做的必要性的话,那就是事务的持久性问题。</p>
|
||
|
||
<p>对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了。</p>
|
||
|
||
<p>两阶段提交就是为了给所有人一个机会,当每个人都说“我 ok”的时候,再一起提交。</p>
|
||
|
||
<h2>追问 5:不引入两个日志,也就没有两阶段提交的必要了。只用 binlog 来支持崩溃恢复,又能支持归档,不就可以了?</h2>
|
||
|
||
<p>回答:这位同学的意思是,只保留 binlog,然后可以把提交流程改成这样:… -> “数据更新到内存” -> “写 binlog” -> “提交事务”,是不是也可以提供崩溃恢复的能力?</p>
|
||
|
||
<p>答案是不可以。</p>
|
||
|
||
<p>如果说<strong>历史原因</strong>的话,那就是 InnoDB 并不是 MySQL 的原生存储引擎。MySQL 的原生引擎是 MyISAM,设计之初就有没有支持崩溃恢复。</p>
|
||
|
||
<p>InnoDB 在作为 MySQL 的插件加入 MySQL 引擎家族之前,就已经是一个提供了崩溃恢复和事务支持的引擎了。</p>
|
||
|
||
<p>InnoDB 接入了 MySQL 后,发现既然 binlog 没有崩溃恢复的能力,那就用 InnoDB 原有的 redo log 好了。</p>
|
||
|
||
<p>而如果说<strong>实现上的原因</strong>的话,就有很多了。就按照问题中说的,只用 binlog 来实现崩溃恢复的流程,我画了一张示意图,这里就没有 redo log 了。</p>
|
||
|
||
<p><img src="assets/eb838b87e9c20fa00aca50ef154f2a63.jpg" alt="img" /></p>
|
||
|
||
<p>图 2 只用 binlog 支持崩溃恢复</p>
|
||
|
||
<p>这样的流程下,binlog 还是不能支持崩溃恢复的。我说一个不支持的点吧:binlog 没有能力恢复“数据页”。</p>
|
||
|
||
<p>如果在图中标的位置,也就是 binlog2 写完了,但是整个事务还没有 commit 的时候,MySQL 发生了 crash。</p>
|
||
|
||
<p>重启后,引擎内部事务 2 会回滚,然后应用 binlog2 可以补回来;但是对于事务 1 来说,系统已经认为提交完成了,不会再应用一次 binlog1。</p>
|
||
|
||
<p>但是,InnoDB 引擎使用的是 WAL 技术,执行事务的时候,写完内存和日志,事务就算完成了。如果之后崩溃,要依赖于日志来恢复数据页。</p>
|
||
|
||
<p>也就是说在图中这个位置发生崩溃的话,事务 1 也是可能丢失了的,而且是数据页级的丢失。此时,binlog 里面并没有记录数据页的更新细节,是补不回来的。</p>
|
||
|
||
<p>你如果要说,那我优化一下 binlog 的内容,让它来记录数据页的更改可以吗?但,这其实就是又做了一个 redo log 出来。</p>
|
||
|
||
<p>所以,至少现在的 binlog 能力,还不能支持崩溃恢复。</p>
|
||
|
||
<h2>追问 6:那能不能反过来,只用 redo log,不要 binlog?</h2>
|
||
|
||
<p>回答:如果只从崩溃恢复的角度来讲是可以的。你可以把 binlog 关掉,这样就没有两阶段提交了,但系统依然是 crash-safe 的。</p>
|
||
|
||
<p>但是,如果你了解一下业界各个公司的使用场景的话,就会发现在正式的生产库上,binlog 都是开着的。因为 binlog 有着 redo log 无法替代的功能。</p>
|
||
|
||
<p>一个是归档。redo log 是循环写,写到末尾是要回到开头继续写的。这样历史日志没法保留,redo log 也就起不到归档的作用。</p>
|
||
|
||
<p>一个就是 MySQL 系统依赖于 binlog。binlog 作为 MySQL 一开始就有的功能,被用在了很多地方。其中,MySQL 系统高可用的基础,就是 binlog 复制。</p>
|
||
|
||
<p>还有很多公司有异构系统(比如一些数据分析系统),这些系统就靠消费 MySQL 的 binlog 来更新自己的数据。关掉 binlog 的话,这些下游系统就没法输入了。</p>
|
||
|
||
<p>总之,由于现在包括 MySQL 高可用在内的很多系统机制都依赖于 binlog,所以“鸠占鹊巢”redo log 还做不到。你看,发展生态是多么重要。</p>
|
||
|
||
<h2>追问 7:redo log 一般设置多大?</h2>
|
||
|
||
<p>回答:redo log 太小的话,会导致很快就被写满,然后不得不强行刷 redo log,这样 WAL 机制的能力就发挥不出来了。</p>
|
||
|
||
<p>所以,如果是现在常见的几个 TB 的磁盘的话,就不要太小气了,直接将 redo log 设置为 4 个文件、每个文件 1GB 吧。</p>
|
||
|
||
<h2>追问 8:正常运行中的实例,数据写入后的最终落盘,是从 redo log 更新过来的还是从 buffer pool 更新过来的呢?</h2>
|
||
|
||
<p>回答:这个问题其实问得非常好。这里涉及到了,“redo log 里面到底是什么”的问题。</p>
|
||
|
||
<p>实际上,redo log 并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由 redo log 更新过去”的情况。</p>
|
||
|
||
<ol>
|
||
|
||
<li>如果是正常运行的实例的话,数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与 redo log 毫无关系。</li>
|
||
|
||
<li>在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。</li>
|
||
|
||
</ol>
|
||
|
||
<h2>追问 9:redo log buffer 是什么?是先修改内存,还是先写 redo log 文件?</h2>
|
||
|
||
<p>回答:这两个问题可以一起回答。</p>
|
||
|
||
<p>在一个事务的更新过程中,日志是要写多次的。比如下面这个事务:</p>
|
||
|
||
<pre><code>begin;
|
||
|
||
insert into t1 ...
|
||
|
||
insert into t2 ...
|
||
|
||
commit;
|
||
|
||
</code></pre>
|
||
|
||
<p>这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来,但又不能在还没 commit 的时候就直接写到 redo log 文件里。</p>
|
||
|
||
<p>所以,redo log buffer 就是一块内存,用来先存 redo 日志的。也就是说,在执行第一个 insert 的时候,数据的内存被修改了,redo log buffer 也写入了日志。</p>
|
||
|
||
<p>但是,真正把日志写到 redo log 文件(文件名是 ib_logfile+ 数字),是在执行 commit 语句的时候做的。</p>
|
||
|
||
<p>(这里说的是事务执行过程中不会“主动去刷盘”,以减少不必要的 IO 消耗。但是可能会出现“被动写入磁盘”,比如内存不够、其他事务提交等情况。这个问题我们会在后面第 22 篇文章《MySQL 有哪些“饮鸩止渴”的提高性能的方法?》中再详细展开)。</p>
|
||
|
||
<p>单独执行一个更新语句的时候,InnoDB 会自己启动一个事务,在语句执行完成的时候提交。过程跟上面是一样的,只不过是“压缩”到了一个语句里面完成。</p>
|
||
|
||
<p>以上这些问题,就是把大家提过的关于 redo log 和 binlog 的问题串起来,做的一次集中回答。如果你还有问题,可以在评论区继续留言补充。</p>
|
||
|
||
<h1>业务设计问题</h1>
|
||
|
||
<p>接下来,我再和你分享 @ithunter 同学在第 8 篇文章[《事务到底是隔离的还是不隔离的?》]的评论区提到的跟索引相关的一个问题。我觉得这个问题挺有趣、也挺实用的,其他同学也可能会碰上这样的场景,在这里解答和分享一下。</p>
|
||
|
||
<p>问题是这样的(我文字上稍微做了点修改,方便大家理解):</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>业务上有这样的需求,A、B 两个用户,如果互相关注,则成为好友。设计上是有两张表,一个是 like 表,一个是 friend 表,like 表有 user_id、liker_id 两个字段,我设置为复合唯一索引即 uk_user_id_liker_id。语句执行逻辑是这样的:</p>
|
||
|
||
</blockquote>
|
||
|
||
<blockquote>
|
||
|
||
<p>以 A 关注 B 为例:
|
||
|
||
第一步,先查询对方有没有关注自己(B 有没有关注 A)
|
||
|
||
select * from like where user_id = B and liker_id = A;</p>
|
||
|
||
</blockquote>
|
||
|
||
<blockquote>
|
||
|
||
<p>如果有,则成为好友
|
||
|
||
insert into friend;</p>
|
||
|
||
</blockquote>
|
||
|
||
<blockquote>
|
||
|
||
<p>没有,则只是单向关注关系
|
||
|
||
insert into like;</p>
|
||
|
||
</blockquote>
|
||
|
||
<blockquote>
|
||
|
||
<p>但是如果 A、B 同时关注对方,会出现不会成为好友的情况。因为上面第 1 步,双方都没关注对方。第 1 步即使使用了排他锁也不行,因为记录不存在,行锁无法生效。请问这种情况,在 MySQL 锁层面有没有办法处理?</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>首先,我要先赞一下这样的提问方式。虽然极客时间现在的评论区还不能追加评论,但如果大家能够一次留言就把问题讲清楚的话,其实影响也不大。所以,我希望你在留言提问的时候,也能借鉴这种方式。</p>
|
||
|
||
<p>接下来,我把 @ithunter 同学说的表模拟出来,方便我们讨论。</p>
|
||
|
||
<pre><code>CREATE TABLE `like` (
|
||
|
||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||
|
||
`user_id` int(11) NOT NULL,
|
||
|
||
`liker_id` int(11) NOT NULL,
|
||
|
||
PRIMARY KEY (`id`),
|
||
|
||
UNIQUE KEY `uk_user_id_liker_id` (`user_id`,`liker_id`)
|
||
|
||
) ENGINE=InnoDB;
|
||
|
||
|
||
|
||
CREATE TABLE `friend` (
|
||
|
||
id` int(11) NOT NULL AUTO_INCREMENT,
|
||
|
||
`friend_1_id` int(11) NOT NULL,
|
||
|
||
`firned_2_id` int(11) NOT NULL,
|
||
|
||
UNIQUE KEY `uk_friend` (`friend_1_id`,`firned_2_id`)
|
||
|
||
PRIMARY KEY (`id`)
|
||
|
||
) ENGINE=InnoDB;
|
||
|
||
</code></pre>
|
||
|
||
<p>虽然这个题干中,并没有说到 friend 表的索引结构。但我猜测 friend_1_id 和 friend_2_id 也有索引,为便于描述,我给加上唯一索引。</p>
|
||
|
||
<p>顺便说明一下,“like”是关键字,我一般不建议使用关键字作为库名、表名、字段名或索引名。</p>
|
||
|
||
<p>我把他的疑问翻译一下,在并发场景下,同时有两个人,设置为关注对方,就可能导致无法成功加为朋友关系。</p>
|
||
|
||
<p>现在,我用你已经熟悉的时刻顺序表的形式,把这两个事务的执行语句列出来:
|
||
|
||
<img src="assets/c45063baf1ae521bf5d98b6d7c0e0ced.png" alt="img" /></p>
|
||
|
||
<p>图 3 并发“喜欢”逻辑操作顺序</p>
|
||
|
||
<p>由于一开始 A 和 B 之间没有关注关系,所以两个事务里面的 select 语句查出来的结果都是空。</p>
|
||
|
||
<p>因此,session 1 的逻辑就是“既然 B 没有关注 A,那就只插入一个单向关注关系”。session 2 也同样是这个逻辑。</p>
|
||
|
||
<p>这个结果对业务来说就是 bug 了。因为在业务设定里面,这两个逻辑都执行完成以后,是应该在 friend 表里面插入一行记录的。</p>
|
||
|
||
<p>如提问里面说的,“第 1 步即使使用了排他锁也不行,因为记录不存在,行锁无法生效”。不过,我想到了另外一个方法,来解决这个问题。</p>
|
||
|
||
<p>首先,要给“like”表增加一个字段,比如叫作 relation_ship,并设为整型,取值 1、2、3。</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>值是 1 的时候,表示 user_id 关注 liker_id;
|
||
|
||
值是 2 的时候,表示 liker_id 关注 user_id;
|
||
|
||
值是 3 的时候,表示互相关注。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>然后,当 A 关注 B 的时候,逻辑改成如下所示的样子:</p>
|
||
|
||
<p>应用代码里面,比较 A 和 B 的大小,如果 A<B,就执行下面的逻辑</p>
|
||
|
||
<pre><code>mysql> begin; /* 启动事务 */
|
||
|
||
insert into `like`(user_id, liker_id, relation_ship) values(A, B, 1) on duplicate key update relation_ship=relation_ship | 1;
|
||
|
||
select relation_ship from `like` where user_id=A and liker_id=B;
|
||
|
||
/* 代码中判断返回的 relation_ship,
|
||
|
||
如果是 1,事务结束,执行 commit
|
||
|
||
如果是 3,则执行下面这两个语句:
|
||
|
||
*/
|
||
|
||
insert ignore into friend(friend_1_id, friend_2_id) values(A,B);
|
||
|
||
commit;
|
||
|
||
</code></pre>
|
||
|
||
<p>如果 A>B,则执行下面的逻辑</p>
|
||
|
||
<pre><code>mysql> begin; /* 启动事务 */
|
||
|
||
insert into `like`(user_id, liker_id, relation_ship) values(B, A, 2) on duplicate key update relation_ship=relation_ship | 2;
|
||
|
||
select relation_ship from `like` where user_id=B and liker_id=A;
|
||
|
||
/* 代码中判断返回的 relation_ship,
|
||
|
||
如果是 2,事务结束,执行 commit
|
||
|
||
如果是 3,则执行下面这两个语句:
|
||
|
||
*/
|
||
|
||
insert ignore into friend(friend_1_id, friend_2_id) values(B,A);
|
||
|
||
commit;
|
||
|
||
</code></pre>
|
||
|
||
<p>这个设计里,让“like”表里的数据保证 user_id < liker_id,这样不论是 A 关注 B,还是 B 关注 A,在操作“like”表的时候,如果反向的关系已经存在,就会出现行锁冲突。</p>
|
||
|
||
<p>然后,insert … on duplicate 语句,确保了在事务内部,执行了这个 SQL 语句后,就强行占住了这个行锁,之后的 select 判断 relation_ship 这个逻辑时就确保了是在行锁保护下的读操作。</p>
|
||
|
||
<p>操作符 “|” 是按位或,连同最后一句 insert 语句里的 ignore,是为了保证重复调用时的幂等性。</p>
|
||
|
||
<p>这样,即使在双方“同时”执行关注操作,最终数据库里的结果,也是 like 表里面有一条关于 A 和 B 的记录,而且 relation_ship 的值是 3, 并且 friend 表里面也有了 A 和 B 的这条记录。</p>
|
||
|
||
<p>不知道你会不会吐槽:之前明明还说尽量不要使用唯一索引,结果这个例子一上来我就创建了两个。这里我要再和你说明一下,之前文章我们讨论的,是在“业务开发保证不会插入重复记录”的情况下,着重要解决性能问题的时候,才建议尽量使用普通索引。</p>
|
||
|
||
<p>而像这个例子里,按照这个设计,业务根本就是保证“我一定会插入重复数据,数据库一定要要有唯一性约束”,这时就没啥好说的了,唯一索引建起来吧。</p>
|
||
|
||
<h1>小结</h1>
|
||
|
||
<p>这是专栏的第一篇答疑文章。</p>
|
||
|
||
<p>我针对前 14 篇文章,大家在评论区中的留言,从中摘取了关于日志和索引的相关问题,串成了今天这篇文章。这里我也要再和你说一声,有些我答应在答疑文章中进行扩展的话题,今天这篇文章没来得及扩展,后续我会再找机会为你解答。所以,篇幅所限,评论区见吧。</p>
|
||
|
||
<p>最后,虽然这篇是答疑文章,但课后问题还是要有的。</p>
|
||
|
||
<p>我们创建了一个简单的表 t,并插入一行,然后对这一行做修改。</p>
|
||
|
||
<pre><code>mysql> CREATE TABLE `t` (
|
||
|
||
`id` int(11) NOT NULL primary key auto_increment,
|
||
|
||
`a` int(11) DEFAULT NULL
|
||
|
||
) ENGINE=InnoDB;
|
||
|
||
insert into t values(1,2);
|
||
|
||
</code></pre>
|
||
|
||
<p>这时候,表 t 里有唯一的一行数据 (1,2)。假设,我现在要执行:</p>
|
||
|
||
<pre><code>mysql> update t set a=2 where id=1;
|
||
|
||
</code></pre>
|
||
|
||
<p>你会看到这样的结果:</p>
|
||
|
||
<p><img src="assets/367b3f299b94353f32f75ea825391170.png" alt="img" />
|
||
|
||
结果显示,匹配 (rows matched) 了一行,修改 (Changed) 了 0 行。</p>
|
||
|
||
<p>仅从现象上看,MySQL 内部在处理这个命令的时候,可以有以下三种选择:</p>
|
||
|
||
<ol>
|
||
|
||
<li>更新都是先读后写的,MySQL 读出数据,发现 a 的值本来就是 2,不更新,直接返回,执行结束;</li>
|
||
|
||
<li>MySQL 调用了 InnoDB 引擎提供的“修改为 (1,2)”这个接口,但是引擎发现值与原来相同,不更新,直接返回;</li>
|
||
|
||
<li>InnoDB 认真执行了“把这个值修改成 (1,2)"这个操作,该加锁的加锁,该更新的更新。</li>
|
||
|
||
</ol>
|
||
|
||
<p>你觉得实际情况会是以上哪种呢?你可否用构造实验的方式,来证明你的结论?进一步地,可以思考一下,MySQL 为什么要选择这种策略呢?</p>
|
||
|
||
<p>你可以把你的验证方法和思考写在留言区里,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。</p>
|
||
|
||
<h1>上期问题时间</h1>
|
||
|
||
<p>上期的问题是,用一个计数表记录一个业务表的总行数,在往业务表插入数据的时候,需要给计数值加 1。</p>
|
||
|
||
<p>逻辑实现上是启动一个事务,执行两个语句:</p>
|
||
|
||
<ol>
|
||
|
||
<li>insert into 数据表;</li>
|
||
|
||
<li>update 计数表,计数值加 1。</li>
|
||
|
||
</ol>
|
||
|
||
<p>从系统并发能力的角度考虑,怎么安排这两个语句的顺序。</p>
|
||
|
||
<p>这里,我直接复制 @阿建 的回答过来供你参考:</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>并发系统性能的角度考虑,应该先插入操作记录,再更新计数表。
|
||
|
||
知识点在[《行锁功过:怎么减少行锁对性能的影响?》]
|
||
|
||
因为更新计数表涉及到行锁的竞争,先插入再更新能最大程度地减少事务之间的锁等待,提升并发度。</p>
|
||
|
||
</blockquote>
|
||
|
||
<p>评论区有同学说,应该把 update 计数表放后面,因为这个计数表可能保存了多个业务表的计数值。如果把 update 计数表放到事务的第一个语句,多个业务表同时插入数据的话,等待时间会更长。</p>
|
||
|
||
<p>这个答案的结论是对的,但是理解不太正确。即使我们用一个计数表记录多个业务表的行数,也肯定会给表名字段加唯一索引。类似于下面这样的表结构:</p>
|
||
|
||
<pre><code>CREATE TABLE `rows_stat` (
|
||
|
||
`table_name` varchar(64) NOT NULL,
|
||
|
||
`row_count` int(10) unsigned NOT NULL,
|
||
|
||
PRIMARY KEY (`table_name`)
|
||
|
||
) ENGINE=InnoDB;
|
||
|
||
</code></pre>
|
||
|
||
<p>在更新计数表的时候,一定会传入 where table_name=$table_name,使用主键索引,更新加行锁只会锁在一行上。</p>
|
||
|
||
<p>而在不同业务表插入数据,是更新不同的行,不会有行锁。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/MySQL实战45讲/14 count()这么慢,我该怎么办?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/MySQL实战45讲/16 “order by”是怎么工作的?.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":"709972aaecdc3d60","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>
|
||
|