使用本地的Redis来加速应用程序(Linux+PHP+DB+Redis)

首先

使用本地Redis可以减少与外部数据库和缓存的连接次数、通信次数和通信数据量。我认为特别适用于具有自动缩放功能的Web服务器系统。虽然使用Memcached也可以实现相同的效果,但如果要进行排名等操作,可能会用到Redis的有序集合,而且统一使用本地和外部数据库可能更好,所以这次我选择了Redis。

这是对之前发布的《某SP社交游戏的调优备忘录》中缓存处理和加速的更详细总结。
当谈到加速时,很容易变得像精神论一样,所以我尽量写了具体的代码示例。
(但精神论也很重要,希望你能全都读完…)
这次我使用了phpredis来进行简单的编写,应该更易懂!!

Redis的使用案例说明

    • ローカルRedis

 

    • 主にマスターデータ(今回の本題)

 

    • 外部Redis

 

    セッション管理、ランキング、マルチバトル、ユーザー固有のデータなど

在使用缓存时,很多情况下会使用外部缓存服务器。
如果规模较小,可能只用它就足够应付了。
当Web服务器数量超过几十台时,缓存服务器可能会由于负载而崩溃。
这种情况下,我想推荐本地缓存。
如果你对如何在本地共享缓存感到困惑,请再次参考上述用法示例。
我们会将类似主数据这样不会动态更改的数据存储在本地缓存中。
不要犹豫,可以在所有Web服务器上都缓存,并且没有问题。
即使在各个服务器上缓存的时间不一致也没有问题。
也许会对多个服务器拥有相同的缓存感到不舒服或者觉得不高效。
让我们暂时忘掉这种固定概念吧。
即使整洁有序,如果不能正常工作,也没有意义。

环境设置

    • WebサーバにRedisをインストールする。

 

    • RedisはUnixドメインソケットで接続する。

 

    • Redisに接続する際には持続的接続(pconnect)を使う。

 

    Redisには主にマスターデータをキャッシュする。

安装和确认

简单来说,由于我所使用的环境是CentOS7和PHP7.0,所以情况如下。

$ sudo yum install epel-release
$ rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
$ sudo yum install redis
$ sudo yum --enablerepo=remi-php70 install php
$ sudo yum --enablerepo=remi-php70 install php-pecl-redis
$ sudo vi /usr/lib/tmpfiles.d/redis.conf
d /run/redis 775 root redis
$ sudo vi /etc/redis.conf
unixsocket /var/run/redis/redis.sock
unixsocketperm 777
$ php -v
PHP 7.0.25 (cli) (built: Oct 24 2017 18:17:05) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
$ sudo systemctl start redis.service
$ redis-cli get test
(nil)
$ redis-cli set test 10
OK
$ redis-cli get test
"10"

调整Redis的配置或设置

我趁机尝试了各种转化为恶魔的方法。
我会根据服务器规格来调整内存容量。

daemonize yes
pidfile /var/run/redis/redis.pid
maxmemory 1gb
maxmemory-policy volatile-lru
supervised systemd
$ sudo vi /etc/systemd/system/multi-user.target.wants/redis.service
After=network.target syslog.target
Type=notify
ExecStart=/usr/bin/redis-server /etc/redis.conf
Restart=on-failure

$ sudo systemctl daemon-reload
$ sudo systemctl start redis.service
$ sudo systemctl enable redis.service

由于在Redis启动时启用透明大页(THP)会产生警告,因此需要将其禁用。

警告:您的内核支持透明巨页面(THP)。这会导致Redis出现延迟和内存使用问题。为了解决这个问题,请作为root运行命令’echo never > /sys/kernel/mm/transparent_hugepage/enabled’,并将其添加到/etc/rc.local中,以在重新启动后保留此设置。在禁用THP后,必须重新启动Redis。

由于根据消息进行操作后仍然出现警告,所以进行了以下设置更改。2018/03/07删除追加transparent_hugepage=never以应用设置
$ sudo vi /etc/default/grub
GRUB_CMDLINE_LINUX=”transparent_hugepage=never …..” #保留原设置,只追加

$ sudo grub2-mkconfig -o /etc/grub2.cfg

if test -f /sys/kernel/mm/transparent_hugepage/enabled; then
   echo never > /sys/kernel/mm/transparent_hugepage/enabled
fi
if test -f /sys/kernel/mm/transparent_hugepage/defrag; then
   echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi

因为没有执行权限,所以无法在启动时执行操作,需要更改权限。

$ chmod u+x /etc/rc.local

如果操作系统重新启动后成功地设置为never,那就没问题了。Redis启动时也不应该再出现警告。

$ sudo cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]

使用Redis进行缓存

当您在某种计算过程中使用IN子句的SELECT语句来获取多个记录时,您会更容易地体会到其效果。这与RPG游戏或卡片游戏中的战斗计算相似(通过组合角色、属性、技能等多个数据进行计算)。

样本源

https://github.com/Fearinota/RedisWrapper的代码是使用了这个包装器类。

Wrapper/Redis.php:主体
Class/xxx.php:示例代码包装使用主体
Batch/Redis.php:批量处理类
Sql/create_schema.sql:用于示例脚本的表创建DDL(使用MySQL创建的DDL)
Sample/sample.php:用于逐个记录缓存的示例脚本(PHP+MySQL+Redis)
※示例代码是为MySQL编写的。如果要在其他数据库中使用,请修改DSN等参数。

使用phpredis可以实现这个功能。

根据用途进行设置修改可能更方便。

由于这只是为了说明而随意制作的,所以在实际使用中可能需要做调整。

每个记录都进行缓存

事先通过批处理将缓存数据(哈希类型)存储在本地Redis中。
键名使用表名和唯一ID。(使用自己方便的格式即可)
对于存在大量主数据且需要一次获取多条记录(使用IN子句)的情况,它非常有效。

$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$cache = [];
$sql = 'SELECT * FROM character';
$rows = $pdo->query($sql, \PDO::FETCH_ASSOC);
foreach ($rows as $row) {
    $cache[$row['id']] = $row;
}
$redis->hMSet('character', $cache);

首先,为了使用缓存,需要从数据库中获取用户拥有的列表(例如物品、角色的ID),并创建一个KEY数组。
使用KEY数组一次性获取主数据(hMGet)。
(循环逐个获取是不可行的)。

使用缓存,需要先从数据库中获取用户的所持列表(如物品、角色的ID),并创建一个KEY的数组。
利用这个KEY的数组一次性获取主数据(hMGet)。
(循环逐个获取是不可取的)。

$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$keys = [];
$sql = 'SELECT * FROM user_character_list'; //ユーザーの所持リストを取得
$rows = $pdo->query($sql, \PDO::FETCH_ASSOC);
foreach ($rows as $row) {
    $keys[] = $row['id'];
}
$data = $redis->hMGet('character', $keys);

想定中的流程:
1.在战斗开始时从数据库中获取角色和怪物的ID
2.从本地Redis获取角色和怪物的主数据
3.从数据库中获取用户特定的数据(如角色的状态和已掌握的技能等)
4.进行战斗计算

如果主要数据越多,效果会更好。
如果有时需要重新获取用户特定数据,可以将其缓存在外部Redis中,可能能够实现加速。(选项1和3)
我曾经参与的社交游戏中有些地方需要多次使用相同的卡组进行战斗,因此将整个卡组(相当于选项1和3的数据)缓存起来,就能加速。

批量缓存多个记录

桌子上的所有记录

如果记录数量很少,几乎每次都要进行全扫描才能使用的表是目标表。
如果数据量较小,可以将其一起缓存在一个数组中。
方法与缓存单个记录几乎相同。
将表名和”ALL”作为KEY。
如果可能,最好事先通过批处理创建缓存。
由于目标是处理数据量较小的表,我认为可以动态地创建缓存。

$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$rows = $redis->hGet('table', 'ALL');
if (empty($rows)) {
    $sql = "SELECT * FROM table";
    $rows = $pdo->query($sql, \PDO::FETCH_ASSOC);
    $redis->hSet('table', 'ALL', $rows);
}

将每个记录都缓存并进行hGetAll在某种程度上可能更具通用性,但与其多次提取所需记录,不如一次性全部获取并保存在变量中更好。
如果将KEY反转(’ALL’,’table’),您可以使用一个哈希类型来管理多个缓存。
这样就可以使用hGetAll一次性获取它们。(请注意不要过度整理以避免膨胀)
建议与后续的静态变量缓存一起使用。

查询结果

将经常执行的通用查询结果进行缓存。
如果有缓存,则返回缓存,否则从数据库中获取并放入本地Redis中。
这是在优化现有系统时经常使用的方法。

$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$rows = $redis->hGet('table', $id);
if (empty($rows)) {
    $sql = "SELECT * FROM table WHERE id={$id}";
    $rows = $pdo->query($sql, \PDO::FETCH_ASSOC);
    $redis->hSet('table', $id, $rows);
}

不要过度缓存!高频更新的数据进行缓存也没有意义,所以要停止这样做。盲目缓存会导致缓存成为瓶颈。

缓存用户特定的数据(使用外部Redis)

COUNT()等属性较重的汇总型SQL适合用于缓存。
由于需要与数据更新同步,所以我们可以在外部使用Redis进行操作。
由于不希望频繁进行重量级汇总操作,可以设置较长的有效期,例如1小时。
在数据更新时,如果更新(或删除)缓存,可以保持一致性。
有效期可以根据系统进行调整。
虽然可以使用Redis的incr进行计数,但是考虑到在恢复后重新创建缓存等情况,最好是将获取的数据存入缓存。

$expire = 3600;  //1h
$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$count = $redis->get('user:'.$user_id.':message_count');
if (empty($count)) {
    $sql = "SELECT COUNT(*) AS count FROM message WHERE user_id={$user_id} AND new_flag=1";
    $row = $pdo->query($sql, \PDO::FETCH_ASSOC);
    $redis->setex('user:'.$user_id.':message_count', $expire, $row[0]['count']);
}
$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$sql = "INSERT INTO message VALUES({$user_id}, 'message')";
$pdo->query($sql);
$redis->del('user:'.$user_id.':message_count');
$pdo = new \PDO('dsn', 'user', 'password');
$redis = new Redis();
$sql = "UPDATE message SET new_flag=0 WHERE user_id={$user_id}";
$pdo->query($sql);
$redis->del('user:'.$user_id.':message_count');

例如,如果将KEY设置为user:id:xxxx,就可以按用户汇总进行获取和删除。

$redis = new Redis();
$redis->del($redis->keys('user:'.$user_id.':*'));

需要根据系统的特点来考虑适合的KEY格式,最好是能减少获取、更新和删除的次数。

使用静态变量进行缓存

如果在处理过程中多次执行完全相同的查询,可以将查询结果全部缓存在静态变量中,这样以后只需要引用变量的内容即可。
(如果从Redis中多次获取完全相同的缓存,也是同样的原理)

在使用ORM的系统中,可能会特别有效。
在处理过程中要注意数据可能会被更改的情况。
基本上,不需要对可能发生更改的内容进行缓存。
但如果在数据更改时清除变量能够提高速度的话,建议还是尽管麻烦但进行缓存。
(为了避免误解)由于变量的性质,一旦PHP处理结束,变量会消失。

/**
 * データを1行取得する
 * @param   PDO $pdo   PDOオブジェクト
 * @param   int $id    プライマリID
 * @return  array      結果配列
 */
function select($pdo, $id) {
    $sql = "SELECT * FROM table WHERE id={$id}";
    return $pdo->query($sql, \PDO::FETCH_ASSOC);
}
static $_cacheSelect = array();
function select($pdo, $id) {
    $cacheKey = 'table_'.$id;
    if (isset(self::$_cacheSelect[$cacheKey])) {
        $rows = self::$_cacheSelect[$cacheKey];
    } else {
        $redis = new Redis();
        $rows = $redis->hGet($key, $cacheKey);
        if (empty($rows)) {
            $sql = "SELECT * FROM table WHERE id={$id}";
            $rows = $pdo->query($sql, \PDO::FETCH_ASSOC);
            $redis->hSet($key, $cacheKey, $rows);
        }
        self::$_cacheSelect[$cacheKey] = $rows;
    }
    return $rows;
}

如果同一查询被执行3次或以上,变量缓存可能会使其更快。结合使用Redis缓存可能会产生更好的效果。请根据情况灵活选择。

关于缓存的有效期限

基本上,主数据可以无限期使用。
由于所有用户都会使用,设置期限并不太有意义,因为重新缓存的成本是浪费的。
如果要缓存除主数据以外的其他数据,则可以将它们的缓存时间设置为1至5分钟左右。
如果在1分钟内执行了100次,那么通过5分钟的缓存,可以减少499次与外部通信。
这已经足够了。
即使延长到1小时,也只有11次的差异。
为了避免无谓地扩大缓存,请确认使用频率。

要想实现高速化

    • なるべく接続回数(DB接続…etc)を減らす

 

    • なるべく通信回数(クエリ実行…etc)を減らす

 

    なるべく通信データ量(クエリの結果データ量…etc)を減らす

这是一种看似乏味的工作的积累。
由于是一连串乏味的工作,如果进行速度测量并确保达到成就感,可能会变得更有趣!
如果你还没有实际体验过,可以尝试测量外部连接和查询执行的速度。
当你测量的时候,会发现相比简单的计算,它需要更多的时间。(也许进行100到1000次循环来进行比较更容易)

顺便说一下,最近在SQL的select语句中经常见到没有指定列名的情况。
这里为了方便解释,我使用了“*”,但是尽量只指定必要的列。
如果只取一行记录的话,查询语句会变短,可能也是好事吧…
总之,更新日期和创建日期肯定是不需要的!
仅这两个日期字段的话,就是Datetime类型×2=16B,以字符串形式返回的话则是38B。
比如说38B×1万(次访问)×10(次select执行),就是大约3MB吧。
要传输3MB的数据需要多少秒呢?

考虑这些事情是加快速度的第一步。
你不认为需要编写复杂的机制或代码才能做到吗……?
最重要的是热情和毅力!

请提供下列内容的中文释义,仅需一个选项:

参考

phpredis/phpredis(GitHub)是一个Redis客户端库。

附加内容:将缓存创建批处理设置为systemd。

创建配置文件。
这次我们将其命名为rediscache.service。(文件名可任意)

[Unit]
Description=Create Redis Cache
After=network.target redis.service

[Service]
Type=simple
ExecStart=/usr/bin/php /path/to/batch.php
User=root
Group=root
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable rediscache.service

如果执行文件位于Vagrant的config.vm.synced_folder配置的挂载目录中,
则在挂载完成之前进行执行会产生错误。

本地主机php:无法打开输入文件:/path/to/batch.php

尝试了各种设置更改后仍然不行,所以我设置了RestartSec。在我的环境中只需要重新启动一次就能成功缓存注册。总之,就先这样吧。(实际在服务器上设置时应该不会发生这种情况)
另外,将清除缓存的批处理程序设置为ExecStop可能也是个不错的选择。

如果在Jenkins中进行自动化,可以批量执行所有Web服务器,那么在操作中就变得容易更新和删除缓存了。
如果正在进行自动扩容,那么需要提取当前正在运行的Web服务器并执行。

编辑后记

只是轻巧地将Memcached替换成Redis,结果却发现需要做很多调查和确认。
在与主题无关的地方,如在CentOS7上的设置中遇到了困难…
花费了很长时间,但通过查阅资料并尝试,我学到了很多东西。
最初只是大概写完花了大约10小时,然后不断修正修改,已经无法计算总共花了多少时间了…哈哈
日语真难啊(远视的眼神

如果Web服务器的内存有剩余的话,可以尝试使用本地的Redis缓存来加快速度!同时还可以加入静态变量缓存。

广告
将在 10 秒后关闭
bannerAds