对于一个数据库写操作频繁的站点来说,通过引入写缓存来减少写数据库的次数显得至关重要。我们知道,通常的数据写操 作包括插入、更新、删除,这些操作又同时可能伴随着条件查找和索引的更新,所以它们的开销往往会令人望而生畏
直接更新
下面我们来看一个有趣的例子,就拿站点访问量统计功能来说,我们需要记录每个 URL 的累计访问量,所以每次页面刷新都 会伴随着一次访问量的增加,我们将访问量数据保存在数据库中,这毫无疑问,因为我们要长久地保存它。 为此,我们编写了一段有代表性的代码,它可以让某个页面的访问量加 1,代码如下所示
<?php $page = 'article_090222.htm'; $sql = "update page_view set view_count=view_count+1 where page='" . $page . "'"; $conn = mysql_connect('localhost', 'root', '', db_page); mysql_select_db('db_map_main', $conn); mysql_query($sql, $conn); ?>
这段代码很简单,虽然我不知道 article_090222.htm 这个页面目前的访问量是多少,但是我知道它的访问量增加了 1,而且结 果将保存在数据库中。我们可以称它为“直接更新” ,这来源于之前介绍的文件访问中的直接 I/O 标记(O_Direct),它可以跳 过内核写缓存,将数据毫无延迟地直接写入磁盘。
我们对上面的动态程序进行压力测试,结果如下所示
Server Software: lighttpd/1.4.20 Server Hostname: www.liveinmap.com Server Port: 8001 Document Path: /book_writecache_mysql.php Document Length: 0 bytes Concurrency Level: 500 Time taken for tests: 5.239 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 1690000 bytes HTML transferred: 0 bytes Requests per second: 1908.89 [#/sec] (mean) Time per request: 261.933 [ms] (mean) Time per request: 0.524 [ms] (mean, across all concurrent requests) Transfer rate: 315.04 [Kbytes/sec] received
从测试结果可以看出,我们对这个动态程序一共请求了 10000 次,这也意味着 article_090222.htm 这个页面的访问量被增加了 10000。下面我们引入 memcached 作为写缓存,它也许会使得数据更新出现延迟,但是我们可以接受,因为我们并不需要访 问量数据实时更新。
线程安全和锁竞争
在此之前,我们先来看一种传统的分布式加运算,以下的代码只是例子中的一个片段,它先从缓存服务器上取回一个数值, 然后在本地加 1,接下来写回缓存服务器。
<?php $count = $memcache->get($key); $count++; $memcache->set($key, $count, false, 0); ?>
看起来没有任何问题,但是,别忘了可能会有多个用户同时触发这样的计算,你一定能想象的到会有什么糟糕的后果,最后的累计访问量总是小于实际访问量。
事实上,这并不涉及 memcached 本身线程安全的问题,而是以上这种加运算的方式不是线程安全的。如果要保证这种加运算可以正常无误地同时进行,那就要考虑一定的事务隔离机制,简单的办法是使用锁竞争,并且将锁保存在 memcached 上,存在竞争关系的动态内容可以争夺这个锁,一旦某个会话抢到锁,那么其他的会话必须等待。
这里要说的不是如何实现这种分布式锁机制,而是并不鼓励这样做,因为锁竞争带来的等待时间是无法容忍的,这将使得引入 memcached 作为写缓存的唯一优势立刻烟消云散.
原子加法
幸运的是,memcached 提供了原子递增操作,事实上,也正是因为它,我们才考虑在访问量递增更新的应用中引入写缓存。 我们再来修改代码,加入 memcached 的支持,如下所示:
<?php $page = 'article_090222.htm'; $memcache = memcache_connect('10.0.1.12', 11711); $count = $memcache->increment($page, 1); if ($count === false) { $memcache->add($page, 1, false, 0); exit(1); } if ($count == 1000) { $memcache->set($page, 0, false, 0); $sql = "update page_view set view_count=view_count+" . $count . " where page='" . $page . "'"; $conn = mysql_connect('localhost', 'root', '', 'db_page'); mysql_query($sql, $conn); } ?>
在新的代码中,完全改变了之前的“直接更新”方式,当需要增加一次访问量的时候,它做了以下工作: 1.为 memcached 缓存中的对应数据项加 1,如果该数据项不存在,则创建该数据项,并且赋值为 1,代表这个页面是第 一次被访问;
2.如果 memcached 缓存中存在对应数据项,并且累加后的数值为 1000,则将这个数据项置 0,同时更新数据库,将数 据库中的对应数值加 1000。 也就是说,改造后的程序每经历 1000 次递增后才写一次数据库,究竟效果如何呢?我们再来进行压力测试,结果如下所示:
Server Software: lighttpd/1.4.20 Server Hostname: www.liveinmap.com Server Port: 8001 Document Path: /writecache_memcache.php Document Length: 0 bytes Concurrency Level: 500 Time taken for tests: 3.599 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 1690000 bytes HTML transferred: 0 bytes Requests per second: 2778.24 [#/sec] (mean) Time per request: 179.970 [ms] (mean) Time per request: 0.360 [ms] (mean, across all concurrent requests) Transfer rate: 458.52 [Kbytes/sec] received
感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程