mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 04:36:41 +08:00
313 lines
23 KiB
HTML
313 lines
23 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>18 金融级高可用架构:必不可少的数据核对.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实战宝典/00 开篇词 从业务出发,开启海量 MySQL 架构设计.md.html">00 开篇词 从业务出发,开启海量 MySQL 架构设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/01 数字类型:避免自增踩坑.md.html">01 数字类型:避免自增踩坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/02 字符串类型:不能忽略的 COLLATION.md.html">02 字符串类型:不能忽略的 COLLATION</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/03 日期类型:TIMESTAMP 可能是巨坑.md.html">03 日期类型:TIMESTAMP 可能是巨坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/04 非结构存储:用好 JSON 这张牌.md.html">04 非结构存储:用好 JSON 这张牌</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/05 表结构设计:忘记范式准则.md.html">05 表结构设计:忘记范式准则</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/06 表压缩:不仅仅是空间压缩.md.html">06 表压缩:不仅仅是空间压缩</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/07 表的访问设计:你该选择 SQL 还是 NoSQL?.md.html">07 表的访问设计:你该选择 SQL 还是 NoSQL?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/08 索引:排序的艺术.md.html">08 索引:排序的艺术</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/09 索引组织表:万物皆索引.md.html">09 索引组织表:万物皆索引</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/10 组合索引:用好,性能提升 10 倍!.md.html">10 组合索引:用好,性能提升 10 倍!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/11 索引出错:请理解 CBO 的工作原理.md.html">11 索引出错:请理解 CBO 的工作原理</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/12 JOIN 连接:到底能不能写 JOIN?.md.html">12 JOIN 连接:到底能不能写 JOIN?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/13 子查询:放心地使用子查询功能吧!.md.html">13 子查询:放心地使用子查询功能吧!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/14 分区表:哪些场景我不建议用分区表?.md.html">14 分区表:哪些场景我不建议用分区表?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/15 MySQL 复制:最简单也最容易配置出错.md.html">15 MySQL 复制:最简单也最容易配置出错</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/16 读写分离设计:复制延迟?其实是你用错了.md.html">16 读写分离设计:复制延迟?其实是你用错了</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/17 高可用设计:你怎么活用三大架构方案?.md.html">17 高可用设计:你怎么活用三大架构方案?</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/MySQL实战宝典/18 金融级高可用架构:必不可少的数据核对.md.html">18 金融级高可用架构:必不可少的数据核对</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/19 高可用套件:选择这么多,你该如何选?.md.html">19 高可用套件:选择这么多,你该如何选?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/20 InnoDB Cluster:改变历史的新产品.md.html">20 InnoDB Cluster:改变历史的新产品</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/21 数据库备份:备份文件也要检查!.md.html">21 数据库备份:备份文件也要检查!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/22 分布式数据库架构:彻底理解什么叫分布式数据库.md.html">22 分布式数据库架构:彻底理解什么叫分布式数据库</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/23 分布式数据库表结构设计:如何正确地将数据分片?.md.html">23 分布式数据库表结构设计:如何正确地将数据分片?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md.html">24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/25 分布式数据库架构选型:分库分表 or 中间件 ?.md.html">25 分布式数据库架构选型:分库分表 or 中间件 ?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/26 分布式设计之禅:全链路的条带化设计.md.html">26 分布式设计之禅:全链路的条带化设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/27 分布式事务:我们到底要不要使用 2PC?.md.html">27 分布式事务:我们到底要不要使用 2PC?</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>18 金融级高可用架构:必不可少的数据核对</h1>
|
||
<p>在 17 讲中,我们学习了高可用的三大架构设计,基于数据层的高可用、基于业务层的高可用,以及融合的高可用架构设计。</p>
|
||
<p>在这些架构中,仅仅解决了业务连续性的问题:也就是当服务器因为各种原因,发生宕机,导致MySQL 数据库不可用之后,快速恢复业务。但对有状态的数据库服务来说,在一些核心业务系统中,比如电商、金融等,还要保证数据一致性。</p>
|
||
<p>这里的“数据一致性”是指在任何灾难场景下,<strong>一条数据都不允许丢失</strong>(一般也把这种数据复制方式叫作“强同步”)。</p>
|
||
<p>今天我们就来看一看,怎么在这种最高要求(数据一致性)的业务场景中,设计 MySQL 的高可用架构。</p>
|
||
<h3>复制类型的选择</h3>
|
||
<p>在 15 讲中,我们已经谈到银行、保险、证券等核心业务,需要严格保障数据一致性。那么要想实现数据的强同步,在进行复制的配置时,就要使用无损半同步复制模式。</p>
|
||
<p>在 MySQL 内部就是要把参数 rpl_semi_sync_master_wait_point 设置成 AFTER_SYNC 。</p>
|
||
<p>但是在高可用设计时,当数据库 FAILOVER 完后,有时还要对原来的主机做额外的操作,这样才能保证主从数据的完全一致性。</p>
|
||
<p>我们来看这样一张图:</p>
|
||
<p><img src="assets/CioPOWDTCOaAVe7oAADUFFKmNU4530.png" alt="png" /></p>
|
||
<p>从图中可以看到,即使启用无损半同步复制,依然存在当发生主机宕机时,最后一组事务没有上传到从机的可能。图中宕机的主机已经提交事务到 101,但是从机只接收到事务 100。如果这个时候 Failover,从机提升为主机,那么这时:</p>
|
||
<p><img src="assets/Cgp9HWDTCPCAC922AADW2MxBbwQ555.png" alt="png" /></p>
|
||
<p>可以看到当主从切换完成后,新的 MySQL 开始写入新的事务102,如果这时老的主服务器从宕机中恢复,则这时事务 101 不会同步到新主服务器,导致主从数据不一致。</p>
|
||
<p><strong>但设置 AFTER_SYNC 无损半同步的好处是,虽然事务 101 在原主机已经提交,但是在从机没有收到并返回 ACK 前,这个事务对用户是不可见的,所以,用户感受不到事务已经提交了</strong>。</p>
|
||
<p>所以,在做高可用设计时,当老主机恢复时,需要做一次额外的处理,把事务101给“回滚”(具体怎么实现我们将在 20 讲,高可用套件中具体分析)。</p>
|
||
<p>这里我们只要记住,设计数据强一致的高可用方案时,要选择无损半同步复制,另外在发生宕机FAILOVER 后,若老主机恢复,还需要额外处理老主机上已提交但还未发送到从机的数据。</p>
|
||
<h3>容灾级别</h3>
|
||
<p>高可用用于处理各种宕机问题,而宕机可以分成服务器宕机、机房级宕机,甚至是一个城市发生宕机。</p>
|
||
<ul>
|
||
<li><strong>机房级宕机:</strong> 机房光纤不通/被挖断,机房整体掉电(双路备用电源也不可用);</li>
|
||
<li><strong>城市级宕机:</strong> 一般指整个城市的进出口网络,骨干交换机发生的故障(这种情况发生的概率很小)。</li>
|
||
</ul>
|
||
<p>如果综合考虑的话,高可用就成了一种容灾处理机制,对应的高可用架构的评判标准就上升了。</p>
|
||
<ul>
|
||
<li><strong>机房内容灾:</strong> 机房内某台数据库服务器不可用,切换到同机房的数据库实例,保障业务连续性;</li>
|
||
<li><strong>同城容灾:</strong> 机房不可用,切换到同城机房的数据库实例,保障业务连续性;</li>
|
||
<li><strong>跨城容灾:</strong> 单个城市机房都不可用,切换到跨城机房的数据库实例,保障业务连续性。</li>
|
||
</ul>
|
||
<p>前面我们谈到的高可用设计,都只是机房内的容灾。也就是说,我们的主服务器和从服务器都在一个机房内,现在我们来看一下同城和跨城的容灾设计(我提醒一下,不论是机房内容灾、同城容灾,还是跨城容灾,都是基于 MySQL 的无损半同步复制,只是物理部署方式不同,解决不同的问题)。</p>
|
||
<p>对于同城容灾,我看到很多这样的设计:</p>
|
||
<p><img src="assets/Cgp9HWDTCPmAbsNEAAD2UJugkKg920.png" alt="png" /></p>
|
||
<p>这种设计没有考虑到机房网络的抖动。如果机房 1 和机房 2 之间的网络发生抖动,那么因为事务提交需要机房 2 中的从服务器接收日志,所以会出现事务提交被 hang 住的问题。</p>
|
||
<p>而机房网络抖动非常常见,所以核心业务同城容灾务要采用三园区的架构,如下图所示:</p>
|
||
<p><img src="assets/CioPOWDTCQGADPvMAAEmUGR02eM950.png" alt="png" /></p>
|
||
<p>该架构称为“三园区的架构”,如果三个机房都在一个城市,则称为“ 一地三中心”,如果在相邻两个城市,那么就叫“两地三中心”。但这种同城/近城容灾,要求机房网络之间的延迟不超过 5ms。</p>
|
||
<p>在三园区架构中,一份数据被存放在了 3 个机房,机房之间根据半同步复制。这里将 MySQL 的半同步复制参数 rpl_semi_sync_master_wait_for_slave_count 设置为 1,表示只要有 1 个半同步备机接收到日志,主服务器上的事务就可以提交。</p>
|
||
<p>这样的设计,保证除主机房外,数据在其他机房至少一份完整的数据。</p>
|
||
<p>另外,即便机房 1 与机房 2 发生网络抖动,因为机房 1 与机房 3 之间的网络很好,不会影响事务在主服务器上的提交。如果机房 1 的出口交换机或光纤发生故障,那么这时高可用套件会 FAILOVER 到机房 2 或机房 3,因为至少有一份数据是完整的。</p>
|
||
<p>机房 2、机房 3 的数据用于保障数据一致性,但是如果要实现读写分离,或备份,还需要引入异步复制的备机节点。所以整体架构调整为:</p>
|
||
<p><img src="assets/Cgp9HWDTCQyAFtFCAAF7qJpeUuM426.png" alt="png" /></p>
|
||
<p>从图中可以看到,我们加入两个异步复制的节点,用于业务实现读写分离,另外再从机房 3 的备机中,引入一个异步复制的延迟备机,用于做数据误删除操作的恢复。</p>
|
||
<p><strong>当设计成类似上述的架构时,你才能认为自己的同城容灾架构是合格的!</strong></p>
|
||
<p>另一个重要的点:因为机房 1 中的主服务器要向四个从服务器发送日志,这时网卡有成为瓶颈的可能,所以请务必配置万兆网卡。</p>
|
||
<p><strong>在明白三园区架构后,要实现跨城容灾也就非常简单了,</strong> 只要把三个机房放在不同城市就行。但这样的设计,当主服务器发生宕机时,数据库就会切到跨城,而跨城之间的网络延迟超过了25 ms。所以,跨城容灾一般设计成“三地五中心”的架构,如下图所示:</p>
|
||
<p><img src="assets/CioPOWDTCROAOWeTAAF4gz8w6PY448.png" alt="png" /></p>
|
||
<p>在上图中:机房 1、机房 2 在城市 1 中;机房 3、机房 4 在城市 2 中;机房 5 在城市 3 中,三个城市之间的距离超过 200 公里,延迟超过 25ms。</p>
|
||
<p>由于有五个机房,所以 ACK 设置为 2,保证至少一份数据在两个机房有数据。这样当发生城市级故障,则城市 2 或城市 3 中,至少有一份完整的数据。</p>
|
||
<p>在真实的互联网业务场景中,“三地五中心”应用并不像“三园区”那样普遍。这是因为 25ms的延迟对业务的影响非常大,一般这种架构应用于读多写少的场景,比如用户中心。</p>
|
||
<p>另外,真实的互联网业务场景中,实现跨城容灾,一般基于同城容灾架构,然后再由业务层来保障跨城的数据一致性。</p>
|
||
<h3>兜底策略:数据核对</h3>
|
||
<p>到目前为止,我们的高可用是基于 MySQL 的复制技术。但你有没有想过这样几个问题:</p>
|
||
<ul>
|
||
<li>万一数据库的复制有 Bug 呢?导致最终的数据在逻辑上不一致呢?</li>
|
||
<li>主从的数据一定一致吗?你如何判断一定一致呢?</li>
|
||
</ul>
|
||
<p>所以,除了高可用的容灾架构设计,我们还要做一层兜底服务,用于判断数据的一致性。这里要引入数据核对,用来解决以下两方面的问题。</p>
|
||
<ul>
|
||
<li><strong>数据在业务逻辑上一致:</strong> 这个保障业务是对的;</li>
|
||
<li><strong>主从服务器之间的数据一致:</strong> 这个保障从服务器的数据是安全的、可切的。</li>
|
||
</ul>
|
||
<p><strong>业务逻辑核对由业务的同学负责编写,</strong> 从整个业务逻辑调度看账平不平。例如“今天库存的消耗”是否等于“订单明细表中的总和”,“在途快递” + “已收快递”是否等于“已下快递总和”。总之,这是个业务逻辑,用于对账。</p>
|
||
<p><strong>主从服务器之间的核对,是由数据库团队负责的。</strong> 需要额外写一个主从核对服务,用于保障主从数据的一致性。这个核对不依赖复制本身,也是一种逻辑核对。思路是:将最近一段时间内主服务器上变更过的记录与从服务器核对,从逻辑上验证是否一致。其实现如图所示:</p>
|
||
<p><img src="assets/CioPOWDTCR2AcmV6AAC1N5tCM7E109.png" alt="png" /></p>
|
||
<p>那么现在的难题是:如何判断最近一段时间内主服务器上变更过的记录?这里有两种思路:</p>
|
||
<ul>
|
||
<li>表结构设计规范中,有讲过每张表有一个 last_modify_date,用于记录每条记录的最后修改时间,按照这个条件过滤就能查出最近更新的记录,然后每条记录比较即可。</li>
|
||
<li>核对服务扫描最近的二进制日志,筛选出最近更新过记录的表和主键,然后核对数据。这种的实现难度会更大一些,但是不要求在数据库上进行查询。</li>
|
||
</ul>
|
||
<p>如果在核对过程中,记录又在主上发生了变化,但是还没有同步到从机,我们可以加入复核逻辑,按理来说多复核几次,主从数据应该就一致了。如果复核多次不一致,那么大概率,主从数据就已经是不一致的了。</p>
|
||
<p>核对服务的逻辑比较简单,但是要实现线上业务的数据核对,开发上还是有一些挑战,但这不就是我们 DBA 的价值所在吗?</p>
|
||
<h3>总结</h3>
|
||
<p>今天我们学习了金融级高可用的架构设计,内容非常干货,建议你反复阅读。其中涉及的内容在原理上并不复杂,但在实现细节上需要不断打磨,欢迎你在后续的架构设计过程中与我交流。总结来说:</p>
|
||
<ol>
|
||
<li>核心业务复制务必设置为无损半同步复制;</li>
|
||
<li>同城容灾使用三园区架构,一地三中心,或者两地三中心,机房见网络延迟不超过 5ms;</li>
|
||
<li>跨城容灾使用“三地五中心”,跨城机房距离超过 200KM,延迟超过 25ms;</li>
|
||
<li>跨城容灾架构由于网络耗时高,因此一般仅用于读多写少的业务,例如用户中心;</li>
|
||
<li>除了复制进行数据同步外,还需要额外的核对程序进行逻辑核对;</li>
|
||
<li>数据库层的逻辑核对,可以使用 last_modify_date 字段,取出最近修改的记录。</li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/MySQL实战宝典/17 高可用设计:你怎么活用三大架构方案?.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/MySQL实战宝典/19 高可用套件:选择这么多,你该如何选?.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":"709973229e4c3d60","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>
|