虽然黑色星期五有惊无险的过去了, 但是 Magento 2 社区版无法读写分离这个限制, 始终是悬在整个网站上的一把利剑。

我之前尝试过给 Magento 2 写一个 MySQL 读写分离的插件, 在深入研究了 Magento 2 的数据库访问层后, 发现通过一个简单的插件, 想做到读写分离基本上是不可能的。Magento 2 社区版读写数据库的逻辑里, 混杂着大量的 Magento 1的代码和逻辑, 无法在修改少量代码的前提下做到读写分离, 后来忙着做网站上的各种需求, 于是读写分离就搁置了。

这次黑五, 整个项目的性能瓶颈就是 MySQL, 流量上来之后, 应用服务器负载基本保持不变, 而数据库服务器负载却翻了3倍多, 而且是在数据库服务器提前升级了硬件配置的基础上。所以我觉得 Magento 2 的数据库层必须要优化一下, 既然没法做读写分离, 那能不能加个缓存层呢?将绝大多数读取操作转移到缓存层去, 理论上数据库的负载会相应下降。

要想改的代码最少, 就得找对地方。 Magento 2 的数据库 Adapter 是 Magento\Framework\DB\Adapter\Pdo\Mysql 类, 该类继承自 Zend_Db_Adapter_Abstract

所有获取数据的方法如下:

Zend_Db_Adapter_Abstract::fetchAll($sql, $bind = array(), $fetchMode = null)

Zend_Db_Adapter_Abstract::fetchAssoc($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchCol($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchPairs($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchOne($sql, $bind = array())

Zend_Db_Adapter_Abstract::fetchRow($sql, $bind = array(), $fetchMode = null)

其中, fetchAll() 和 fetchRow() 是用的最多的两个。

下面以 fetchRow() 为例, 分析该方案的可行性以及实现方法。

/**
* Fetches the first row of the SQL result.
* Uses the current fetchMode for the adapter.
*
* @param string|Zend_Db_Select $sql An SQL SELECT statement.
* @param mixed $bind Data to bind into SELECT placeholders.
* @param mixed $fetchMode Override current fetch mode.
* @return mixed Array, object, or scalar depending on fetch mode.
*/
public function fetchRow($sql, $bind = array(), $fetchMode = null)

通过解析 $sql 对象和 $bind 数组, 可以得到精确的、格式化的数据, 包含
1. 数据库表名
2. 字段键值对

通过这些数据,可以构建缓存的键(key)和标签(tag), 例如:
$cacheKey = table_name::主键键值对
或者
$cacheKey = table_name::唯一键索引键值对

$cacheTags = [
table_name,
table_name::主键键值对
table_name::唯一键索引键值对组1,
table_name::唯一键索引键值对组2,

]

cacheTags 的作用是给缓存分类, 方便后续清理。

有了 $cacheKey, $cacheTags 之后, 就可以将数据库查询的结果保存到缓存中去;

下次再有查询过来, 先在缓存中查找有无对应的数据, 如果有就直接返回给数据调用方了;

那么如果数据更新了呢?

数据更新分为三种: 1. UPDATE, 2. INSERT, 3 DELETE

对于 UPDATE:

/**
* Updates table rows with specified data based on a WHERE clause.
*
* @param mixed $table The table to update.
* @param array $bind Column-value pairs.
* @param mixed $where UPDATE WHERE clause(s).
* @return int The number of affected rows.
* @throws Zend_Db_Adapter_Exception
*/
public function update($table, array $bind, $where = '')

update() 方法接收 3 个参数, 分别是 table_name, 待更新数据键值对, where 条件子句。
刚才我们在构建 $cacheTags 时, 分别有 table_name、table_name::主键键值对、table_name::唯一键索引键值对, table_name 是现成的, 其余两种tag 需要从 where 子句中解析。 通过解析,最坏情况是 where 子句未解析到任何键值对, 最好情况是解析到了所有 filed 键值对。最坏情况下, 需要清除 table_name 下的所有缓存数据, 而最好情况下, 只需要清除一条缓存数据。

对于 INSERT:

/**
* Inserts a table row with specified data.
*
* @param mixed $table The table to insert data into.
* @param array $bind Column-value pairs.
* @return int The number of affected rows.
* @throws Zend_Db_Adapter_Exception
*/
public function insert($table, array $bind)

insert() 方法接收 2 个参数, 分别是 table_name, 待插入数据键值对。 由于新插入的数据根本不存在与缓存中, 所以不需要对缓存进行操作

对于 DELETE:

/**
* Deletes table rows based on a WHERE clause.
*
* @param mixed $table The table to update.
* @param mixed $where DELETE WHERE clause(s).
* @return int The number of affected rows.
*/
public function delete($table, $where = '')

delete() 方法接收 2 个参数, table_name 和 where 子句, 假如能从 where 子句中解析到主键键值对 或 唯一键索引键值对, 就只需要清除一条缓存记录, 否则需要清除该 table_name 下的所有缓存记录。

优化效果:
我暂时只是用 ab 测试了 Magento 2 的购物车:

ab -C PHPSESSID=acmsj8q8ld1tvdo77lm5t0dr9b -n 40 -c 5  http://localhost/checkout/cart/

没有缓存的时候:
test-No-Cache-1:

Requests per second:    1.79 [#/sec] (mean)
Time per request: 2786.478 [ms] (mean)
Time per request: 557.296 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms)
50% 756
66% 2064
75% 5635
80% 6150
90% 7632
95% 8530
98% 8563
99% 8563
100% 8563 (longest request) MySQL 进程的 CPU 占用率保持在 20% ~ 24%

test-No-Cache-2:

Requests per second:    1.84 [#/sec] (mean)
Time per request: 2720.852 [ms] (mean)
Time per request: 544.170 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms)
50% 586
66% 1523
75% 4036
80% 5667
90% 10228
95% 11621
98% 12098
99% 12098
100% 12098 (longest request) MySQL 进程的 CPU 占用率保持在 20% ~ 24%

有缓存的时候:
test-With-Cache-1:

Requests per second:    1.99 [#/sec] (mean)
Time per request: 2509.273 [ms] (mean)
Time per request: 501.854 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms)
50% 489
66% 511
75% 574
80% 637
90% 19073
95% 19553
98% 20063
99% 20063
100% 20063 (longest request) MySQL 进程的 CPU 占用率保持在 5% 左右

test-With-Cache-2:

Requests per second:    2.10 [#/sec] (mean)
Time per request: 2384.145 [ms] (mean)
Time per request: 476.829 [ms] (mean, across all concurrent requests) Percentage of the requests served within a certain time (ms)
50% 465
66% 472
75% 565
80% 620
90% 9509
95% 18374
98% 18588
99% 18588
100% 18588 (longest request) MySQL 进程的 CPU 占用率保持在 5% ~ 7 %

通过上面两组数据的对比, 很明显 MySQL 的 CPU 占用率有了大幅度下降(从 20% 下降到 5%), 可见增加一个缓存层对降低 MySQL 负载是有效果的。

但是有一个小问题, 在不使用缓存的情况下, Percentage of the requests served within a certain time 这个值,在 90% 这个点之后, 表现要比有缓存的情况好, 我猜是大量 unserialize() 操作造成 CPU 资源不够导致响应缓慢。

经过修改后的 vendor/magento/framework/DB/Adapter/Pdo/Mysql.php:

class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface
{ protected $_cache; public function fetchAll($sql, $bind = array(), $fetchMode = null)
{
if ($sql instanceof \Zend_Db_Select) {
/** @var array $from */
$from = $sql->getPart('from');
$tableName = current($from)['tableName'];
$cacheKey = 'FETCH_ALL::' . $tableName . '::' . md5((string)$sql);
$cache = $this->getCache();
$data = $cache->load($cacheKey);
if ($data === false) {
$data = parent::fetchAll($sql, $bind, $fetchMode);
$cache->save(serialize($data), $cacheKey, ['FETCH_ALL::' . $tableName], 3600);
} else {
$data = @unserialize($data);
}
} else {
$data = parent::fetchAll($sql, $bind, $fetchMode);
}
return $data;
} public function fetchRow($sql, $bind = [], $fetchMode = null)
{
$cacheIdentifiers = $this->resolveSql($sql, $bind);
if ($cacheIdentifiers !== false) {
$cache = $this->getCache()->getFrontend();
$data = $cache->load($cacheIdentifiers['cacheKey']); if ($data === false) {
$data = parent::fetchRow($sql, $bind, $fetchMode);
if ($data) {
$cache->save(serialize($data), $cacheIdentifiers['cacheKey'], $cacheIdentifiers['cacheTags'], 3600);
}
} else {
$data = @unserialize($data);
}
} else {
$data = parent::fetchRow($sql, $bind, $fetchMode);
}
return $data;
} public function update($table, array $bind, $where = '')
{
parent::update($table, $bind, $where);
$cacheKey = $this->resolveUpdate($table, $bind, $where);
if ($cacheKey === false) {
$cacheKey = $table;
}
$this->getCache()->clean([$cacheKey, 'FETCH_ALL::' . $table]);
} /**
* @return \Magento\Framework\App\CacheInterface
*/
private function getCache()
{
if ($this->_cache === null) {
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$this->_cache = $objectManager->get(\Magento\Framework\App\CacheInterface::class);
}
return $this->_cache;
} /**
* @param string|\Zend_Db_Select $sql An SQL SELECT statement.
* @param mixed $bind Data to bind into SELECT placeholders.
* @return array
*/
protected function resolveSql($sql, $bind = array())
{
$result = false;
if ($sql instanceof \Zend_Db_Select) {
try {
/** @var array $from */
$from = $sql->getPart('from');
$tableName = current($from)['tableName'];
$where = $sql->getPart('where'); foreach ($this->getIndexFields($tableName) as $indexFields) {
$kv = $this->getKv($indexFields, $where, $bind);
if ($kv !== false) {
$cacheKey = $tableName . '::' . implode('|', $kv);
$cacheTags = [
$tableName,
$cacheKey
];
$result = ['cacheKey' => $cacheKey, 'cacheTags' => $cacheTags];
}
}
}catch (\Zend_Db_Select_Exception $e) { }
}
return $result;
} protected function resolveUpdate($tableName, array $bind, $where = '')
{
$cacheKey = false;
if (is_string($where)) {
$where = [$where];
}
foreach ($this->getIndexFields($tableName) as $indexFields) {
$kv = $this->getKv($indexFields, $where, $bind);
if ($kv !== false) {
$cacheKey = $tableName . '::' . implode('|', $kv);
}
}
return $cacheKey;
} protected function getIndexFields($tableName)
{
$indexes = $this->getIndexList($tableName); $indexFields = [];
foreach ($indexes as $data) {
if ($data['INDEX_TYPE'] == 'primary') {
$indexFields[] = $data['COLUMNS_LIST'];
} elseif ($data['INDEX_TYPE'] == 'unique') {
$indexFields[] = $data['COLUMNS_LIST'];
}
}
return $indexFields;
} protected function getKv($fields, $where, $bind)
{
$found = true;
$kv = [];
foreach ($fields as $field) {
$_found = false; if (isset($bind[':' . $field])) { // 在 bind 数组中查找 filed value
$kv[$field] = $field . '=' .$bind[':' . $field];
$_found = true;
} elseif (is_array($where)) {
foreach ($where as $case) { // 遍历 where 条件子句, 查找 filed value
$matches = [];
$preg = sprintf('#%s.*=(.*)#', $field);
$_result = preg_match($preg, $case, $matches);
if ($_result) {
$kv[$field] = $field . '=' .trim($matches[1], ' \')');
$_found = true;
}
}
} if (!$_found) { // 其中任一 field 没找到,
$found = false;
break;
}
}
return $found ? $kv : false;
}
}

给 Magento 2 添加缓存层的分析与尝试的更多相关文章

  1. .net缓存应用与分析

    在 ASP.NET 提供的许多特性中,相比 ASP.NET 的所有其他特性,缓存对应用程序的性能具有最大的潜在影响,利用缓存和其他机制,ASP.NET 开发人员可以接受使用开销很大的控件(例如,Dat ...

  2. Mcrouter-基于Memcached协议的缓存层流量管理工具(Memcached集群的另一个选择)(转)

    Mcrouter 是一个基于Memcached协议的路由器,它是 Facebook缓存架构的核心组件,在峰值的时候,它能够处理每秒50亿次的请求.近日,Facebook开放了Mcrouter的源代码, ...

  3. 史上最全的CSS hack方式一览 jQuery 图片轮播的代码分离 JQuery中的动画 C#中Trim()、TrimStart()、TrimEnd()的用法 marquee 标签的使用详情 js鼠标事件 js添加遮罩层 页面上通过地址栏传值时出现乱码的两种解决方法 ref和out的区别在c#中 总结

    史上最全的CSS hack方式一览 2013年09月28日 15:57:08 阅读数:175473 做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我 ...

  4. Tapdata 肖贝贝:实时数据引擎系列(六)-从 PostgreSQL 实时数据集成看增量数据缓存层的必要性

      摘要:对于 PostgreSQL 的实时数据采集, 业界经常遇到了包括:对源库性能/存储影响较大, 采集性能受限, 时间回退重新同步不支持, 数据类型较复杂等等问题.Tapdata 在解决 Pos ...

  5. Caffe学习系列(15):添加新层

    如何在Caffe中增加一层新的Layer呢?主要分为四步: (1)在./src/caffe/proto/caffe.proto 中增加对应layer的paramter message: (2)在./i ...

  6. 在ubuntu12.04下编译android4.1.2添加JNI层出现问题

    tiny4412学习者,在ubuntu12.04下编译android4.1.2添加JNI层出现问题: (虚心请教解决方法) trouble writing output: Too many metho ...

  7. js添加遮罩层

    直接用代码来说明 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="MaskT ...

  8. 10.hibernate缓存机制详细分析(转自xiaoluo501395377)

    hibernate缓存机制详细分析   在本篇随笔里将会分析一下hibernate的缓存机制,包括一级缓存(session级别).二级缓存(sessionFactory级别)以及查询缓存,当然还要讨论 ...

  9. Android Hal层简要分析

    Android Hal层简要分析 Android Hal层(即 Hardware Abstraction Layer)是Google开发的Android系统里上层应用对底层硬件操作屏蔽的一个软件层次, ...

随机推荐

  1. BAT小米奇虎美团迅雷携程等等各大企业校招,笔试面试题。

    类似在线测试的方式展示题目. 历年在线笔试试卷: 百度 http://www.nowcoder.com/paper/search?query=%E7%99%BE%E5%BA%A6  腾讯http:// ...

  2. Http协议以及模拟http请求发送数据

    1 为什么要使用http协议 假设我现在有两个客户端浏览器,一个是google,一个是IE浏览器:我现在有两个服务器,一个是tomcat,一个是JBoss;在最初的情况下是:如果google要往tom ...

  3. sql语句去重 最后部分没看 看1 有用

    一 数据库 1.常问数据库查询.修改(SQL查询包含筛选查询.聚合查询和链接查询和优化问题,手写SQL语句,例如四个球队比赛,用SQL显示所有比赛组合:举例2:选择重复项,然后去掉重复项:) 数据库里 ...

  4. 深入学习JavaScript: apply 方法 详解

    我在一开始看到javascript的函数apply和call时,非常的模糊,看也看不懂,最近在网上看到一些文章对apply方法和call的一些示例,总算是看的有点眉目了,在这里我做如下笔记,希望和大家 ...

  5. HTML5程序开发范例宝典 完整版 (韩旭等著) 中文pdf扫描版

    HTML5程序开发范例宝典紧密围绕编程者在编程中遇到的实际问题和开发中应该掌握的技术,全面介绍了利用HTML进行程序开发的各方面技术和技巧.全书共16章,内容包括HTML网页布局.HTML基本元素.H ...

  6. kafka 配置及常用命令

    1. 主要配置 config/server.properties (1)  broker.id=0  # 集群中,每个 kafka 实例的值都不一样 (2) log.dirs=/tmp/kafka-l ...

  7. 【转】如何解决C盘根目录无法创建或写入文件

    源地址:http://blog.csdn.net/xinke453/article/details/7496545

  8. 关于SqlDataReader使用的一点疑惑

    C#中的SqlDataReader类(System.Data.SqlClient)是用来在保持打开数据库连接的状态下取数据用的 用法如下图: “保持与数据库的连接”这个特性也是SqlDataReade ...

  9. C#网络编程学习(5)---Tcp连接中出现的粘包、拆包问题

    本文参考于CSDN博客wxy941011 1.疑问 我们使用第四个博客中的项目. 修改客户端为:连接成功后循环向服务器发送从1-100的数字.看看服务器会不会正常的接收100次数据. 可是我们发现服务 ...

  10. ORM(一)

    ORM常识: 1.一对多,多的一方设置外键字段,有外键字段的表叫做子表.没有外键字段的表叫做主表. 2.主表放到子表的下面,否则子表找不到主表,写数据要先往主表中写. 数据库:(1)不创建主键,会自动 ...