二十一、Swoole 基础学习笔记 - Swoole 高性能共享内存 Table
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
正相关,但不完全一致,如$size
为1024
实际可存储的行数小于1024
,如果$size
过大,机器内存不足Table
会创建失败。
size 容量计算:
- 如果
$size
不是为2
的N
次方,如1024
、8192
、65536
等,底层会自动调整为接近的一个数字,如果小于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
表示获取数据
我是温新,本篇文章到此结束,下一篇文章继续学习。