二十一、Swoole 基础学习笔记 - Swoole 高性能共享内存 Table

作者: 温新

分类: 【Swoole 系列】

阅读: 1824

时间: 2023-03-13 11:45:27

hi,我是温新,一名PHPer

文章基于 Swoole 5.0.1 版本编写。

**学习目标:学习并了解 table 的使用 **

说明:本篇文章结合官方文档编写及参考网络资料编写,虽非全部原创,但也是结合了自己的理解,若转载请附带本文 URL,编写不易,持续编写更不易,谢谢!

什么是 Swoole\Table

Swoole\Table 是一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程 / 多线程数据共享和同步加锁问题。

Swoole\Table 解决了什么问题

Swoole\Table 主要解决进程之间共享内存数据问题。由于 PHP 不支持多线程,因此 Swoole 使用多进程模式,在多进程模式下存在进程内存隔离,在工作进程内修改 global 全局变量和超全局变量时,在其他进程是无效的。

进程之间要操作同一份数据,可以使用外部存储服务:

  • 相间库,如 MySQL;
  • 缓存服务器,如 Redis、Memcache;
  • 磁盘文件,多进程并发读写时需要加锁。

普通的数据库和磁盘文件操作,存在较多 IO 等待时间。因此推荐使用:

  • Redis 内存数据库,读写速度非常快,但是有 TCP 连接等问题,性能也不是最高的。
  • /dev/shm 内存文件系统,读写操作全部在内存中完成,无 IO 消耗,性能极高,但是数据不是格式化的,还有数据同步的问题。

Swoole\Table 优势

  • 性能强悍,单线程每秒可读写 200 万次;
  • 应用代码无需加锁,Table 内置行锁自旋锁,所有操作均是多线程 / 多进程安全。用户层完全不需要考虑数据同步问题;
  • 支持多进程,Table 可以用于多进程之间共享数据;
  • 使用行锁,而不是全局锁,仅当 2 个进程在同一 CPU 时间,并发读取同一条数据才会进行发生抢锁。

Swoole\table 创建一个 table

创建一个 Table 仅需三步:

  • 1、实例化 Table;
  • 2、设置表格字段及类型;
  • 3、创建表格。

三步就可以创建出一个高性能 Table,table 创建后,其余操作都是围绕这个 table 来操作。

不要使用数组方式读写 Table,一定要使用文档中提供的 API 来进行操作。

<?php
// 21-swoole-table.php

// 1、实例化 table 并设置最大行数
$table = new Swoole\Table(1024);

// 2、指定表格字段并设置表格类型与长度
$table->column('id', $table::TYPE_INT);
$table->column('name', Swoole\Table::TYPE_STRING, 64);
$table->column('salary', $table::TYPE_FLOAT);

// 3、创建表格
$table->create();

表格已经创建完成,那么现在对这些方法进行学习:

__construct()

Swoole\Table::__construct(int $size, float $conflict_proportion = 0.2);

参数:

  • size:指定表格的最大行数;
  • conflict_proportion:哈希冲突的最大比例。

由于 Table 底层是建立在共享内存之上,所以无法动态扩容。所以 $size 必须在创建前自己计算设置好,Table 能存储的最大行数与 $size 正相关,但不完全一致,如 $size1024 实际可存储的行数小于1024,如果 $size 过大,机器内存不足 Table 会创建失败。

size 容量计算:

  • 如果 $size 不是为 2N 次方,如 1024819265536 等,底层会自动调整为接近的一个数字,如果小于 1024 则默认成 1024,即 1024 是最小值。从 v4.4.6 版本开始最小值为 64
  • Table 占用的内存总数为 (HashTable结构体长度 + KEY长度64字节 + $size值) * (1 + $conflict_proportion值作为hash冲突) * (列尺寸)。
  • 如果你的数据 Key 和 Hash 冲突率超过 20%,预留的冲突内存块容量不足,set 新的数据就会报 Unable to allocate memory 错误,并返回 false,存储失败,此时需要调大 $size 值并重启服务。
  • 在内存足够的情况下尽量将此值设置的大一些。

column

含义:内存表增加一列。

Swoole\Table->column(string $name, int $type, int $size = 0);

参数:

  • name:指定字段的名称;

  • type:指定字段类型;

  • size:指定字符串字段的最大长度【字符串类型的字段必须指定 $size】。

  • $type 类型说明

类型 说明
Table::TYPE_INT 默认为 8 个字节
Table::TYPE_STRING 设置后,设置的字符串不能超过 $size 指定的最大长度
Table::TYPE_FLOAT 会占用 8 个字节的内存

create

含义:创建内存表。定义好表的结构后,执行 create 向操作系统申请内存,创建表。

Swoole\Table->create(): bool

Swoole\Table 操作

下面来看看 table 相关的操作方法。

<?php

// 21-swoole-table-2.php

$table = new Swoole\Table(1024);

$table->column('id', $table::TYPE_INT);
$table->column('name', Swoole\Table::TYPE_STRING, 64);
$table->column('salary', $table::TYPE_FLOAT);
$table->column('age', Swoole\Table::TYPE_INT, 1);

$table->create();

/**
 * set() 方法: 添加数据
 *
 * @param string $key 数据的 key
 * @param array  $value 数据
 *
 * @return bool
 */
$table->set('user_1', ['id'=>1, 'name'=>'Lucy','age'=>18,'salary'=>1829.12]);
$table->set('user_2', ['id'=>2, 'name'=>'Jack','age'=>19,'salary'=>2302.39]);


/**
 * get() 方法:获取一行数据
 *
 * @param string $key 数据的 key【必须为字符串类型】
 * @param string $field 当指定了 $field 时仅返回该字段的值,而不是整个记录
 */
$lucy     = $table->get('user_1');
$lucyName = $table->get('user_1', 'name');
print_r($lucy);
echo $lucyName . PHP_EOL;

/**
 * exists() 方法:检查 table 中是否存在某一个 key
 *
 * @return bool
 */
$exists = $table->exists('user_1');
var_dump($exists);

/**
 * count() 方法:返回 table 中存在的条目数
 *
 * @return int 
 */
$count = $table->count();
echo $count . PHP_EOL;


/**
 * stats() 获取 Swoole\Table 状态。
 *
 * @return array
 */
$stats = $table->stats();

/**
 * incr() 方法:原子自增操作
 *
 * @param string $key 数据的 key【如果 $key 对应的行不存在,默认列的值为 0
 * @param string $column 指定列名【仅支持浮点型和整型字段】
 * @param string $incrby 增量 【如果列为 int,$incrby 必须为 int 型,如果列为 float 型,$incrby 必须为 float 类型】
 */
$table->incr('user_1', 'age');
print_r($table->get('user_1'));

$table->decr('user_1', 'age');

Swoole\Table 综合案例

案例使用官方文档中的案例来演示

<?php

// 21-swoole-table-3.php

$table = new Swoole\Table(1024);
$table->column('fd', Swoole\Table::TYPE_INT);
$table->column('reactor_id', Swoole\Table::TYPE_INT);
$table->column('data', Swoole\Table::TYPE_STRING, 64);
$table->create();

$serv = new Swoole\Server('127.0.0.1', 9501);
$serv->set(['dispatch_mode' => 1]);
// 设置 table 实例为 server 的属性
$serv->table = $table;

$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {

    $cmd = explode(" ", trim($data));

    if ($cmd[0] == 'get')
    {
        // 获取 table 数据
        if (count($cmd) < 2) {
            $cmd[1] = $fd;
        }
        $getFd = intval($cmd[1]);
        $info  = $serv->table->get($getFd);
        // 将数据返回给客户端
        $serv->send($fd, var_export($info, true)."\n");
    } elseif ($cmd[0] == 'set') {
        // 设置 Table 数据
        $ret = $serv->table->set($fd, ['reactor_id' => $data, 'fd' => $fd, 'data' => $cmd[1]]);
        if ($ret === false) {
            $serv->send($fd, "ERROR\n");
        } else {
            $serv->send($fd, "OK\n");
        }
    } else {
        $serv->send($fd, "command error.\n");
    }
});

$serv->start();

客户端使用 telnet 进行连接:

$telnet 127.0.0.1 9501
telnet> 
# 添加数据
set Lucy
OK
# 获取数据
get 
array (
  'fd' => 1,
  'reactor_id' => 0,
  'data' => 'Lucy',
)

代码解析:

服务启动后,输入字符串 set Lcuy 表示添加数据;

输入 get 表示获取数据

我是温新,本篇文章到此结束,下一篇文章继续学习。

请登录后再评论