This commit is contained in:
周伟
2022-05-11 18:46:27 +08:00
commit 387f48277a
8634 changed files with 2579564 additions and 0 deletions

View File

@@ -0,0 +1,954 @@
<!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>23 分布式数据库表结构设计:如何正确地将数据分片?.md</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">00 开篇词 从业务出发,开启海量 MySQL 架构设计.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/01 数字类型:避免自增踩坑.md">01 数字类型:避免自增踩坑.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/02 字符串类型:不能忽略的 COLLATION.md">02 字符串类型:不能忽略的 COLLATION.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/03 日期类型TIMESTAMP 可能是巨坑.md">03 日期类型TIMESTAMP 可能是巨坑.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/04 非结构存储:用好 JSON 这张牌.md">04 非结构存储:用好 JSON 这张牌.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/05 表结构设计:忘记范式准则.md">05 表结构设计:忘记范式准则.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/06 表压缩:不仅仅是空间压缩.md">06 表压缩:不仅仅是空间压缩.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/07 表的访问设计:你该选择 SQL 还是 NoSQL.md">07 表的访问设计:你该选择 SQL 还是 NoSQL.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/08 索引:排序的艺术.md">08 索引:排序的艺术.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/09 索引组织表:万物皆索引.md">09 索引组织表:万物皆索引.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/10 组合索引:用好,性能提升 10 倍!.md">10 组合索引:用好,性能提升 10 倍!.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/11 索引出错:请理解 CBO 的工作原理.md">11 索引出错:请理解 CBO 的工作原理.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/12 JOIN 连接:到底能不能写 JOIN.md">12 JOIN 连接:到底能不能写 JOIN.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/13 子查询:放心地使用子查询功能吧!.md">13 子查询:放心地使用子查询功能吧!.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/14 分区表:哪些场景我不建议用分区表?.md">14 分区表:哪些场景我不建议用分区表?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/15 MySQL 复制:最简单也最容易配置出错.md">15 MySQL 复制:最简单也最容易配置出错.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/16 读写分离设计:复制延迟?其实是你用错了.md">16 读写分离设计:复制延迟?其实是你用错了.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/17 高可用设计:你怎么活用三大架构方案?.md">17 高可用设计:你怎么活用三大架构方案?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/18 金融级高可用架构:必不可少的数据核对.md">18 金融级高可用架构:必不可少的数据核对.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/19 高可用套件:选择这么多,你该如何选?.md">19 高可用套件:选择这么多,你该如何选?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/20 InnoDB Cluster改变历史的新产品.md">20 InnoDB Cluster改变历史的新产品.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/21 数据库备份:备份文件也要检查!.md">21 数据库备份:备份文件也要检查!.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/22 分布式数据库架构:彻底理解什么叫分布式数据库.md">22 分布式数据库架构:彻底理解什么叫分布式数据库.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/MySQL实战宝典/23 分布式数据库表结构设计:如何正确地将数据分片?.md">23 分布式数据库表结构设计:如何正确地将数据分片?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md">24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/25 分布式数据库架构选型:分库分表 or 中间件 .md">25 分布式数据库架构选型:分库分表 or 中间件 .md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/26 分布式设计之禅:全链路的条带化设计.md">26 分布式设计之禅:全链路的条带化设计.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战宝典/27 分布式事务:我们到底要不要使用 2PC.md">27 分布式事务:我们到底要不要使用 2PC.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>23 分布式数据库表结构设计:如何正确地将数据分片?</h1>
<p>前面 22 讲中,我们简单学习了分布式数据库的架构,知道各类分布式数据库都离不开计算层、存储层、元数据层这三层关系。</p>
<p>另外很重要的一点是知道分布式数据库是把数据打散存储在一个个分片中。在基于MySQL 的分布式数据库架构中,分片就存在于 MySQL 实例中。</p>
<p>今天这一讲,我们来学习分布式数据库中,一个非常重要的设计:正确地把数据分片,充分发挥分布式数据库架构的优势。</p>
<h3>选出分片键</h3>
<p>在对表中的数据进行分片时首先要选出一个分片键Shard Key即用户可以通过这个字段进行数据的水平拆分。</p>
<p>对于我们之前使用的电商业务的订单表orders其表结构如下所示</p>
<pre><code>CREATE TABLE `orders` (
`O_ORDERKEY` int NOT NULL,
`O_CUSTKEY` int NOT NULL,
`O_ORDERSTATUS` char(1) NOT NULL,
`O_TOTALPRICE` decimal(15,2) NOT NULL,
`O_ORDERDATE` date NOT NULL,
`O_ORDERPRIORITY` char(15) NOT NULL,
`O_CLERK` char(15) NOT NULL,
`O_SHIPPRIORITY` int NOT NULL,
`O_COMMENT` varchar(79) NOT NULL,
PRIMARY KEY (`O_ORDERKEY`),
KEY `idx_custkey_orderdate` (`O_CUSTKEY`,`O_ORDERDATE`),
KEY `ORDERS_FK1` (`O_CUSTKEY`),
KEY `idx_custkey_orderdate_totalprice` (`O_CUSTKEY`,`O_ORDERDATE`,`O_TOTALPRICE`),
KEY `idx_orderdate` (`O_ORDERDATE`),
KEY `idx_orderstatus` (`O_ORDERSTATUS`),
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`O_CUSTKEY`) REFERENCES `customer` (`C_CUSTKEY`)
) ENGINE=InnoDB
</code></pre>
<p>对于类似淘宝、京东、拼多多这样业务体量的应用来说,单实例 MySQL 数据库在性能和存储容量上肯定无法满足“双 11、618 ”大促的要求,所以要改造成分布式数据库架构。</p>
<p>而第一步就是要对表选出一个分片键,然后进行分布式架构的设计。</p>
<p>对于上面的表orders可以选择的分片键有o_orderkey、o_orderdate、也可以是o_custkey。在选出分片键后就要选择分片的算法比较常见的有 RANGE 和 HASH 算法。</p>
<p>比如,表 orders选择分片键 o_orderdate根据函数 YEAR 求出订单年份然后根据RANGE 算法进行分片,这样就能设计出基于 RANGE 分片算法的分布式数据库架构:</p>
<p><img src="assets/Cgp9HWDv4lmAI0tUAAFEb3P4_hc847.jpg" alt="image" /></p>
<p>从图中我们可以看到,采用 RANGE 算法进行分片后,表 orders 中1992 年的订单数据存放在分片 1 中、1993 年的订单数据存放在分片 2 中、1994 年的订单数据存放在分片 3中依次类推如果要存放新年份的订单数据追加新的分片即可。</p>
<p><strong>不过RANGE 分片算法在分布式数据库架构中,是一种非常糟糕的算法</strong>,因为对于分布式架构,通常希望能解决传统单实例数据库两个痛点:</p>
<ul>
<li>性能可扩展,通过增加分片节点,性能可以线性提升;</li>
<li>存储容量可扩展,通过增加分片节点,解决单点存储容量的数据瓶颈。</li>
</ul>
<p>那么对于订单表 orders 的 RANGE 分片算法来说,你会发现以上两点都无法实现,因为当年的数据依然存储在一个分片上(即热点还是存在于一个数据节点上)。</p>
<p>如果继续拆细呢?比如根据每天进行 RANGE 分片?这样的确会好一些,但是对“双 11、618”这样的大促来说依然是单分片在工作热点依然异常集中。</p>
<p>所以在分布式架构中RANGE 分区算法是一种比较糟糕的算法。但它也有好处可以方便数据在不同机器间进行迁移migrate比如要把分片 2 中 1992 年的数据迁移到分片 1直接将表进行迁移就行。</p>
<p><strong>而对海量并发的 OLTP 业务来说,一般推荐用 HASH 的分区算法</strong>。这样分片的每个节点都可以有实时的访问,每个节点负载都能相对平衡,从而实现性能和存储层的线性可扩展。</p>
<p>我们来看表 orders 根据 o_orderkey 进行 HASH 分片,分片算法如下:</p>
<p><img src="assets/Cgp9HWDv4muAGAAaAAEKa7KWUCA758.jpg" alt="image" /></p>
<p>在上述分片算法中,分片键是 o_orderkey总的分片数量是 4即把原来 1 份数据打散到 4 张表中),具体来讲,分片算法是将 o_orderkey 除以 4 进行取模操作。</p>
<p>最终将表orders 根据 HASH 算法进行分布式设计后的结果如下图所示:</p>
<p><img src="assets/CioPOWDv4oKAYT-OAAEs6WnCu9o191.jpg" alt="image" /></p>
<p>可以看到,对于订单号除以 4余数为 0 的数据存放在分片 1 中,余数为 1 的数据存放在分片 2 中,余数为 2 的数据存放在分片 3 中,以此类推。</p>
<p>这种基于 HASH 算法的分片设计才能较好地应用于大型互联网业务,真正做到分布式数据库架构弹性可扩展的设计要求。</p>
<p>但是,表 orders 分区键选择 o_orderkey 是最好地选择吗?并不是。</p>
<p>我们看一下库中的其他表,如表 customer、lineitem这三张表应该是经常一起使用的比如查询用户最近的订单明细。</p>
<p>如果用 o_orderkey 作分区键,那么 lineitem 可以用 l_orderkey 作为分区键但这时会发现表customer 并没有订单的相关信息,即无法使用订单作为分片键。</p>
<p>如果表 customer 选择另一个字段作为分片键那么业务数据无法做到单元化也就是对于表customer、orders、lineitem分片数据在同一数据库实例上。</p>
<p>所以,如果要实现分片数据的单元化,最好的选择是把用户字段作为分区键,在表 customer 中就是将 c_custkey 作为分片键表orders 中将 o_custkey 作为分片键,表 lineitem 中将 l_custkey 作为分片键:</p>
<p><img src="assets/Cgp9HWDv4pSAZulNAAFwBnC6gPU305.jpg" alt="image" /></p>
<p>这样做的好处是:根据用户维度进行查询时,可以在单个分片上完成所有的操作,不用涉及跨分片的访问,如下面的 SQL</p>
<pre><code>SELECT * FROM orders
INNER JOIN lineitem ON o_orderkey = l_orderkey
INNER JOIN customer ON o_custkey = c_custkey
WHERE o_custkey = 1
ORDER BY o_orderdate DESC LIMIT 10
</code></pre>
<p>所以,分布式数据库架构设计的原则是:<strong>选择一个适合的分片键和分片算法,把数据打散,并且业务的绝大部分查询都是根据分片键进行访问</strong></p>
<p>那为什么互联网业务这么适合进行分布式架构的设计呢?因为互联网业务大部分是 To C 业务,分片键就是用户的 ID业务的大部分访问都是根据用户 ID 进行查询,比如:</p>
<ul>
<li>查看某个用户下的微博/短视频;</li>
<li>查看某个用户的商品信息/购买记录;</li>
<li>查看某个用户自己的余额信息。</li>
</ul>
<p>学完分片键的选择后,接着就是规划分片,也就我们经常提到的分库分表。</p>
<h3>分库分表</h3>
<p>说了这么久分片,分片到底是什么呢?其实,前面说的分片本质是一张张表,而不是数据库实例,只是每个分片是在 MySQL 数据库实例中,严格来说:</p>
<pre><code>分片 = 实例 + 库 + 表 = <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ee879eae9e819c9a">[email&#160;protected]</a>:db_name:table_name
</code></pre>
<p>对于前面的表orders假设根据 HASH 算法进行分片,那么可以进行如下的分库分表设计:</p>
<ol>
<li>每个分片的表名库名都一样,如库 tpch表名 orders</li>
<li>每个分片的库名不一样,表名一样,如库名 tpch01、tpch02、tpch03、tpch04表名orders</li>
<li>每个分片的表名不一样,库名一样,如库名 tpch表名分别为 orders01、orders02、orders03、orders04</li>
<li>每个分片的库名不一样,表名也不一样,如分片 1 的表在库名 tpch01下表名为oders01分片 2 的表名在库名 tpch02表名为 orders02分片 3 的表名在库名tpch03表名为 orders03分片 3 的表名在库名 tpch04表名为 orders04。</li>
</ol>
<p>在这 4 种分库分表规则中,最推荐的是第 4 种,也是我们通常意义说的分库分表,这样做的好处有以下几点:</p>
<ul>
<li>不同分片的数据可以在同一 MySQL 数据库实例上,便于做容量的规划和后期的扩展;</li>
<li>同一分片键的表都在同一库下,方便做整体数据的迁移和扩容。</li>
</ul>
<p>如果根据第 4 种标准的分库分表规范,那么分布式 MySQL 数据库的架构可以是这样:
<img src="assets/Cgp9HWDv4qeAeLM7AAGBfl0Kr4I115.jpg" alt="image" /></p>
<p><strong>有没有发现,按上面这样的分布式设计,数据分片完成后,所有的库表依然是在同一个 MySQL实例上</strong></p>
<p>牢记,分布式数据库并不一定要求有很多个实例,最基本的要求是将数据进行打散分片。接着,用户可以根据自己的需要,进行扩缩容,以此实现数据库性能和容量的伸缩性。<strong>这才是分布式数据库真正的魅力所在</strong></p>
<p>对于上述的分布式数据库架构,一开始我们将 4 个分片数据存储在一个 MySQL 实例上,但是如果遇到一些大促活动,可以对其进行扩容,比如把 4 个分片扩容到 4 个MySQL实例上
<img src="assets/CioPOWDv4-uACx5SAAF0mbv_uqE895.jpg" alt="06.jpg" /></p>
<p>如果完成了大促活动,又可以对资源进行回收,将分片又都放到一台 MySQL 实例上,这就是对资源进行缩容。</p>
<p>总的来说,对分布式数据库进行扩缩容在互联网公司是一件常见的操作,比如对阿里来说,每年下半年 7 月开始,他们就要进行双 11 活动的容量评估,然后根据评估结果规划数据库的扩容。</p>
<p>一般来说,电商的双 11 活动后,还有双 12、新年、春节所以一般会持续到过完年再对数据库进行缩容。接下来我们来看看如何进行扩缩容。</p>
<p>扩缩容</p>
<p>在 HASH 分片的例子中,我们把数据分片到了 4 个节点,然而在生产环境中,为了方便之后的扩缩容操作,推荐一开始就把分片的数量设置为不少于 1000 个。</p>
<p>不用担心分片数量太多,因为分片 1 个还是 1000 个,管理方式都是一样的,但是 1000 个,意味着可以扩容到 1000 个实例上对于一般业务来说1000 个实例足够满足业务的需求了BTW网传阿里某核心业务的分布式数据库分片数量为 10000个</p>
<p>如果到了 1000 个分片依然无法满足业务的需求,这时能不能拆成 2000 个分片呢?从理论上来说是可以的,但是这意味着需要对一张表中的数据进行逻辑拆分,这个工作非常复杂,通常不推荐。</p>
<p>所以,<strong>一开始一定要设计足够多的分片</strong>。在实际工作中,我遇到很多次业务将分片数量从 32、64 拆成 256、512。每次这样的工作都是扒一层皮太不值得。所以做好分布式数据库设计的工作有多重要</p>
<p>那么扩容在 MySQL 数据库中如何操作呢?其实,本质是搭建一个复制架构,然后通过设置过滤复制,仅回放分片所在的数据库就行,这个数据库配置在从服务器上大致进行如下配置:</p>
<pre><code># 分片1从服务器配置
replicate_do_db =&quot;tpch01&quot;
</code></pre>
<p>所以在进行扩容时,首先根据下图的方式对扩容的分片进行过滤复制的配置:</p>
<p><img src="assets/Cgp9HWDv4zKAU0XpAAG23jW4LvA413.jpg" alt="image" /></p>
<p>然后再找一个业务低峰期,将业务的请求转向新的分片,完成最终的扩容操作:</p>
<p><img src="assets/Cgp9HWDv4x-ANZ8IAAHJ0OFzx-c573.jpg" alt="image" /></p>
<p>至于缩容操作,本质就是扩容操作的逆操作,这里就不再多说了。</p>
<h3>总结</h3>
<p>今天这一讲,我们学习了分布式数据库架构设计中的分片设计,也就是我们经常听说的分库分表设计。希望通过本讲,你能牢牢掌握以下内容:</p>
<ul>
<li>分布式数据库数据分片要先选择一个或多个字段作为分片键;</li>
<li>分片键的要求是业务经常访问的字段,且业务之间的表大多能根据这个分片键进行单元化;</li>
<li>如果选不出分片键,业务就无法进行分布式数据库的改造;</li>
<li>选择完分片键后,就要选择分片算法,通常是 RANGE 或 HASH 算法;</li>
<li>海量 OLTP 业务推荐使用 HASH 算法,强烈不推荐使用 RANGE 算法;</li>
<li>分片键和分片算法选择完后,就要进行分库分表设计,推荐不同库名表名的设计,这样能方便后续对分片数据进行扩缩容;</li>
<li>实际进行扩容时,可以使用过滤复制,仅复制需要的分片数据。</li>
</ul>
<p>今天的内容非常非常干货,希望你能反复阅读,掌握分布式数据库架构设计中最为基础和重要的知识点,我们下一讲见。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/MySQL实战宝典/22 分布式数据库架构:彻底理解什么叫分布式数据库.md">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/MySQL实战宝典/24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099732ec82a3d60","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>