update to 2.5.0-b1 (build 408)

This commit is contained in:
jerry
2021-06-16 00:17:30 +08:00
parent 59d614a24e
commit 4ee16d4fc6
92 changed files with 2286 additions and 1342 deletions

View File

@@ -0,0 +1,65 @@
# ZMAtomic 原子计数器
原子计数器是用于多进程间跨进程使用的原子计数使用的,比如统计入站请求数量等。此功能基于 Swoole 的 Atomic详情见 [Swoole - 文档]([进程间无锁计数器(Atomic) (swoole.com)](http://wiki.swoole.com/#/memory/atomic))。
## 配置和初始化
见配置文件:`config/global.php` 中的 `init_atomics` 字段:
```php
/** zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
'foo' => 0,
'bar' => 4,
];
```
这时我们就成功初始化两个原子计数器,名字分别为 `foo``bar`
!!! warning "注意"
初始化的值必须是不小于 0 的 int32 值!
## 使用
定义和命名空间:`ZM\Store\ZMAtomic`
连接计数示例:
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Store\ZMAtomic;
class Hello {
/**
* @OnRequestEvent()
*/
public function onRequest() {
$cnt = ZMAtomic::get("foo")->add(1);
ctx()->getResponse()->end("当前已访问:".$cnt."");
}
}
```
### ZMAtomic::get()->get()
获取计数的数字:`dump(ZMAtomic::get("bar")->get());` 返回 4。
### ZMAtomic::get()->add($num)
加上一定的数并返回结果:`dump(ZMAtomic::get("bar")->add(5));` 返回 9。
### ZMAtomic::get()->sub($num)
要减少的数值(必须为正整数):`dump(ZMAtomic::get("bar")->sub(5));` 返回 5。
### ZMAtomic::get()->set($num)
设置计数的数字:`ZMAtomic::get("bar")->set(77);`
!!! note "提示"
还有一些不常用的方法,可以看 Swoole 官方的文档,这里就不一一列举了。

View File

@@ -0,0 +1,64 @@
# 存储管理(文件)
DataProvider 是框架内提供的一个简易的文件管理类。
定义:`\ZM\Utils\DataProvider`
## DataProvider::getWorkingDir()
`working_dir()`
## DataProvider::getFrameworkLink()
`ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
## DataProvider::getDataFolder()
获取配置项 `zm_data` 指定的目录。
如果指定参数 `$second`,则返回二级目录地址,如果二级目录不存在则自动创建。
```php
DataProvider::getDataFolder("TestModule"); // 例如返回 /root/zhamao-framework/zm_data/TestModule/
```
## DataProvider::getFrameworkRootDir()
返回框架本体的根目录。
## DataProvider::saveToJson()
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
定义:`saveToJson($filename, $file_array)`
`$filename` 是文件名,不需要加后缀,比如你想保存成 `foo/bar.json`,这里写 `foo/bar` 就好。如果不想要二级目录,就直接写 `bar`,不需要加 `.json` 后缀。
这里只支持二级目录,不支持更多级的子目录。
`$file_array` 为内容,一般是数组,比如你缓存了一个 API 接口返回的数据,然后直接解析成数组后丢给它就好了。
## DataProvider::loadFromJson()
从 json 文件加载内容至变量。
定义:`loadFromJson($filename)`
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
## 其他文件读取
框架比较贴近原生的 PHP所以推荐直接使用原生的方法来读写文件`file_get_contents``file_put_contents`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。
```php
// 读取框架工作目录的文件 composer.json 文件
$r = file_get_contents(working_dir() . "/composer.json");
// 写入 Linux 临时目录下的文件
file_put_contents("/tmp/test.txt", "hello world");
```
!!! warning "注意"
在默认的情况里,框架的根目录均为可写可读的,在读写文件时务必要注意目录的位置和权限。使用 `working_dir()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。

View File

@@ -0,0 +1,353 @@
# LightCache 轻量缓存
在炸毛框架 1.x 时代,框架里有非常方便使用的 ZMBuf 缓存,但是由于 2.x 版本框架加入了多进程模式所以不能再以传统的存到全局变量的方式来构建和管理缓存了LightCache 就是替代方案。LightCache 依旧是 key-value 键值对形式的存储,支持多种类型的变量。
定义:`ZM\Store\LightCache`
## 与 ZMBuf 的不同
从存储内容角度LightCache 存入的是 Swoole 初始化的共享内存,基于 Swoole/Table 编写。优势在于多进程下的性能极佳,而且没有数据同步问题;劣势在于它需要在启动框架前就声明总大小,不能根据存储数据的大小来划定,需提前指定最大能存储的容量。而 ZMBuf 基于直接把变量存到静态成员中 `public static $data` 类似这样,且 1.x 框架基于单进程单线程,无任何数据同步的问题。
总之来说LightCache 是让用户在涉及多进程编程时,一个折中的解决方案,提出和解决了很多多进程开发时存储数据遇到的问题:数据同步、进程间通信效率、数据是否需要上锁等。
- 数据同步:多进程下因为是固定的内存大小区域,所以每个进程读取和写入都是只有一份数据的,不存在数据不同步的问题。
- 进程间通信:因为多个进程共享一片区域的内存,所以不需要进程间通信,无协程切换。
- 镀锡是否需要上锁:看情况。一般情况下 Swoole/Table 模块自带一个行锁,只有两个进程在两个 CPU 上同时读取一行数据时才会发生抢锁,作为框架的使用者,如果只写或只读,是无需手动上任何锁的。只有在先 `get()``set()` 这样的情况才需要上自旋锁。后面的段会详细讲述。
使用体验上,基本和 ZMBuf 无差,如果没有用过 1.x 的版本,可无视此段话。
## 使用
### 配置和初始化
配置文件还是在 `config/global.php` 文件里,字段是 `light_cache`
```php
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 512, //最多允许储存的条数需要2的倍数
'max_strlen' => 32768, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
];
```
其中 `$size` 是最多保存的键值对数目,填写非 2 的倍数时底层会自动修正为 2 的倍数值。
`$max_strlen` 为单条值最长保存的长度。因为 Swoole/Table 只能存数字、字符串所以在存取数组等变量时会先将其序列化为字符串形式保存get 时自动反序列化回来。在存数组等非字符串变量时,请先自行计算你要存取的内容序列化后的最大长度。如果长度超出最大长度,则无法保存,`set()` 将返回 false。
`hash_conflict_proportion`Table 模块底层使用 hash 表,会存在 hash 冲突,调大 Hash 冲突率会提升 `size` 指定条目数的准确性,但也将增加物理内存的使用。这里单位是百分比,`0.6``60%`
`persistence_path` 是持久化保存变量的文件保存位置,默认在 `zm_data/_cache.json` 文件。
`auto_save_interval` 是持久化保存变量的自动保存时间,单位秒。
### LightCache::set()
设置内容。
定义:`LightCache::set($key, $value, $expire = -1)`
返回值:`bool`。当 value 超出了最大长度或内存不足时,返回 false其余 true。
参数:
`$key` 的长度不能超过 64 字节,且不能存入二进制内容。
`$value` 可存入 `bool``string``int``array` 等可被 `json_encode()` 的变量,闭包函数和对象不可存入。
`$expire``int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除(框架中途停止不受影响)。如果为 -1 则什么都不做。框架停止后自动被清除。
**注意:如果前面使用了 set() ,后面再次使用 set() 会重置 expire 过期时间为 -1-1 是框架运行时不过期,关闭框架删除的状态),如果只需要更新值,请使用 update()。**
```php
// use ZM\Store\LightCache;
/**
* @CQCommand("store")
*/
public function store() {
LightCache::set("key1", ["value1" => "strOrInt", "value2" => 123]);
return "OK!";
}
/**
* @CQCommand("storeAfterRemove")
*/
public function storeAfterRemove() {
LightCache::set("store1", "remove1", 30);
ctx()->reply(LightCache::get("store1") !== null ? "内容存在!" : "内容不存在!");
zm_sleep(30);
ctx()->reply(LightCache::get("store1") !== null ? "内容存在!" : "内容不存在!");
}
```
<chat-box>
) store
( OK
) storeAfterRemove
( 内容存在!
^ 等待 30 秒
( 内容不存在!
</chat-box>
### LightCache::update()
更新值而不更新状态。如果键值对不存在,则返回 false。
定义:`LightCache::update(string $key, $value)`
参数同 `set()`,可参考。
### LightCache::get()
获取内容。
返回值:`mixed|null`。当无内容或过期时返回 null剩余情况返回原数据。
### LightCache::getExpire()
获取存储项剩余过期时间(秒)。
定义:`LightCache::getExpire(string $key)`
```php
$s = LightCache::set("test", "hello", 20);
zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 10
```
### LightCache::getExpireTS()
获取存储项要过期的时间戳。2.4.3 起可用)
定义:`LightCache::getExpireTS(string $key)`
```php
$s = LightCache::set("test", "hello", 20); //假设这条代码执行时时间戳是 1616838482
zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 1616838502
zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 null
```
### LightCache::getMemoryUsage()
获取轻量缓存使用的总空间大小(字节)
```php
LightCache::getMemoryUsage();
```
轻量缓存的内存手工计算方式:(Table 结构体长度` + `KEY 长度 64 字节 + `$size`) * (1 + `$conflict_proportion`) * 列尺寸。
Table 结构体长度根据你所设定的 `max_strlen` 会变化。
> 框架默认配置下的轻量缓存启动后大约占用内存 25MB 左右。
### LightCache::isset()
判断某项是否存在。
```php
LightCache::set("foo", "bar");
dump(LightCache::isset("foo")); // true
```
### LightCache::unset()
删除某项。
```php
LightCache::set("foo", "bar");
LightCache::unset("foo");
dump(LightCache::isset("foo")); // false
```
### LightCache::getAll()
获取所有项。
```php
LightCache::set("k1", ["I", "am", "array"]);
LightCache::set("k2", "v2");
LightCache::set("k3", 20001);
dump(LightCache::getAll());
/*
{
"k1": ["I", "am", "array"],
"k2": "v2",
"k3": 20001
}
*/
```
### LightCache::addPersistence()
添加持久化存储的键。
用法:`LightCache::addPersistence($key)`
注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为 -2 了。2.4.2 起可用此方法)。
详见下方 **持久化**
### LightCache::removePersistence()
删除持久化的键。
用法:`LightCache::removePersistence($key)`
注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为非 -2 了。2.4.2 起可用此方法)。
### 持久化
使用 `LightCache::addPersistence($key)` 添加对应需要持久化的键名即可。
```php
/**
* @OnStart()
*/
public function onStart() {
LightCache::addPersistence("msg_time");
}
/**
* @CQCommand("getStore")
*/
public function getStore() {
return "存储时间:".date("Y-m-d H:i:s", LightCache::get("msg_time"));
}
```
<chat-box>
^ 我在 2021-01-05 15:21:00 发送这条消息
) getStore
( 2021-01-05 15:20:00
^ 这时我用 Ctrl+C 停止框架,过一会儿再启动
) getStore
( 存储时间2021-01-05 15:20:00
</chat-box>
### 数据加锁
在特定情况下,使用 LightCache 必须配合锁使用,否则会出现数据错乱。我们来看下面的例子:
```php
/**
* @RequestMapping("/test")
*/
public function test() {
$s = LightCache::get("web_count");
if($s === null) $s = 1;
else $s += 1;
LightCache::set("web_count", $s);
return "<h1>It works!</h1>";
}
```
我们使用压测工具,例如 `ab`,对此路由接口开很多很多线程进行测试,假设我们设置请求总数为 200000 次,框架的工作进程数为 8我用的是 2020 年末的 i5 MacBook Pro 13 inch
> 懒得再测了,下面就口述过程吧。
在运行完测试后,通过 `LightCache::get("web_count")`,获取到的数你会发现不是 200000。怎么回事呢请自行翻阅多进程开发相关的书籍哦或者简单理解为有一些情况下进程 1 执行到了 `if-else` 语句,另一个进程也执行到了这里,两次在代码层面加的数是相同的,则虽然请求了两次,但是后执行 set 的那个进程又覆盖了前一个进程执行的值,导致最终结果加了 1 而不是 2
!!! note "提示"
同样的场景,使用 ZMAtomic 就不需要使用锁了。Atomic 是一句话:`add(1)` 立即加值的。而 LightCache 需要加锁的情况一般都是 `get->改值->set` 这样的代码。
解决这一问题,就需要用到锁。这种情况下,我们首先考虑的是自旋锁,框架也因此内置了一个方便使用的自旋锁组件。详见下一章:自旋锁。
## 如何临时缓存大变量
由于 LightCache 需要提前声明最大大小,所以在某些情况下,比如第三方 API 接口结果临时缓存,可能不太适合使用,这时对于 2.x 版本的多进程炸毛框架是一个新的问题。
解决方案有三种:
-`global.php` 中的 `swoole.worker_num` 调整为 `1` 即可,所有除所有主 handler 事件的用户类外其他类均可使用如 `Hello::$store` 类似的静态变量全局存取
- 使用 WorkerCache需要 2.2.0 以上版本)
- 使用 Redis需要安装 `redis` 扩展)
以上WorkerCache 是为了弥补 LightCache 的不足而诞生的,以下就是 WorkerCache 的具体内容。
### WorkerCache 跨进程大缓存
WorkerCache 和 LightCache 几乎完全不同WorkerCache 存储的方式说白了就是 PHP 的静态变量,不过框架支持使用封装好的进程间通信进行跨进程读取。但由于需要设置一个存储变量的进程,所以配置文件必须先指定要将数据存到哪个 Worker/TaskWorker 进程中。关于框架内多进程的说明,请见 [进阶 - 多进程 Hack](/advanced/multi-process/)。
定义:`ZM\Store\WorkerCache`
#### 配置
见 [基本配置](/guide/basic-config/)。
#### WorkerCache::get()
定义:`get($key)`
`$key` 为指定要获取的键值对的值,如果不存在则返回 null。
#### WorkerCache::set()
定义:`set($key, $value, $async = false)`
设置变量,你懂的。
注意,`$value` 可以是被无损 `json_encode``json_decode` 的变量闭包Closure、资源resource等类型不支持存储。
`$async` 默认为 false当为 true 时候,不会返回是否成功设置与否,否则会协程等待是否目标进程存储成功。
#### WorkerCache::unset()
定义:`unset($key, $async = false)`
删除键对应的值。`$async` 的意义同上。
#### WorkerCache::add()
定义:`add($key, int $value, $async = false)`
给 int 类型的值加一,如果值不存在,则默认为 0 且加上目标的 `$value`
#### WorkerCache::sub()
定义:`sub($key, int $value, $async = false)`
给 int 类型的值减一,如果值不存在,则默认为 0 且减去目标的 `$value`
```php
<?php
namespace Module\Example;
use ZM\Store\WorkerCache;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand("set_store")
*/
public function setStorage() {
$arg1 = ctx()->getNextArg("请输入要设置的内容名称");
$arg2 = ctx()->getFullArg("请输入要设置的内容");
WorkerCache::set($arg1, $arg2);
return "成功!";
}
/**
* @CQCommand("get_store")
*/
public function getStorage() {
$arg1 = ctx()->getFullArg("请输入要获取的内容名称");
$data = WorkerCache::get($arg1);
return $data ?? "内容不存在!";
}
}
```
<chat-box>
) set_store hello world
( 成功!
) get_store hello
( world
) get_store foo
( 内容不存在!
</chat-box>

View File

@@ -0,0 +1,97 @@
# MySQL 数据库
## 配置
炸毛框架的数据库组件支持原生 SQL、查询构造器去掉了复杂的对象模型关联同时默认为数据库连接池使开发变得简单。
数据库的配置位于 `config/global.php` 文件的 `sql_config` 段。
数据库操作的唯一核心工具类和功能类为 `\ZM\DB\DB`,使用前需要配置 host 和 use 此类。
## 查询构造器
在 炸毛框架 中,数据库查询构造器为创建和执行数据库查询提供了一个方便的接口,它可用于执行应用程序中大部分数据库操作。同时,查询构造器使用 `prepare` 预处理来保护程序免受 SQL 注入攻击,因此没有必要转义任何传入的字符串。
### 新增数据
```php
DB::table("admin")->insert(['admin_name', 'admin_password'])->save();
// INSERT INTO admin VALUES ('admin_name', 'admin_password')
```
其中 `insert` 的参数是插入条目的数据列表。假设 admin 表有 `name``password` 两列。
> 自增 ID 插入 0 即可。
### 删除数据
```php
DB::table("admin")->delete()->where("name", "admin_name")->save();
// DELETE FROM admin WHERE name = 'admin_name'
```
其中 `where` 语句的第一个参数为列名,当只有两个参数时,第二个参数为值,效果等同于 SQL 语句:`WHERE name = 'admin_name'`,当含有第三个参数且第二个参数为 `=``!=``LIKE` 的时候,效果就是 `WHERE 第一个参数 第二个参数的操作符 第三个参数`
### 更新数据
```php
DB::table("admin")->update(["name" => "fake_admin"])->where("name", "admin_name")->save();
// UPDATE admin SET name = 'fake_admin' WHERE name = 'admin_name'
```
`update()` 方法中是要 SET 的内容的键值对,例如上面把 `name` 列的值改为 `fake_admin`
### 查询数据
```php
$r = DB::table("admin")->select(["name"])->where("name", "fake_admin")->fetchFirst();
// SELECT name FROM admin WHERE name = 'fake_admin'
echo $r["name"];
echo DB::table("admin")->where("name", "fake_admin")->value("name");
// SELECT * FROM admin WHERE name = 'fake_admin'
```
`select()` 方法的参数是要查询的列,默认留空为 `["*"]`,也就是所有列都获取,也可以在 table 后直接 where 查询。
其中 `fetchFirst()` 获取第一行数据。
如果只需获取一行中的一个字段值,也可以通过上面所示的 `value()` 方法直接获取。
多列数据获取需要使用 `fetchAll()`
```php
$r = DB::table("admin")->select()->fetchAll();
// SELECT * FROM admin WHERE 1
foreach($r as $k => $v) {
echo $v["name"].PHP_EOL;
}
```
### 查询条数
```php
DB::table("admin")->where("name", "fake_admin")->count();
//SELECT count(*) FROM admin WHERE name = 'fake_admin'
```
## 直接执行 SQL
> 在查询器外执行的 SQL 语句都不会被缓存,都是一定会请求数据库的。
### DB::rawQuery()
- 用途:直接执行模板查询的裸 SQL 语句。
- 参数:`$line``$params`
- 返回:查到的行的数组
`$line` 为请求的 SQL 语句,`$params` 为模板参数。
```php
$r = DB::rawQuery("SELECT * FROM admin WHERE name = ?", ["fake_admin"]);
//SELECT * FROM admin WHERE name = 'fake_admin'
echo $r[0]["password"];
```
> 参数查询已经从根本上杜绝了 SQL 注入的问题。

View File

@@ -0,0 +1,62 @@
# Redis
炸毛框架内置了 Redis 连接池,供开发者使用。使用前需要先安装 `redis` 扩展:
```bash
pecl install redis
```
> 如果是 Docker 环境,则默认已安装。
## 配置
配置文件在 `config/global.php` 的全局配置文件下,详情见 [配置](/guide/basic-config/#redis_config)。
示例配置(假设 Redis Server 开到了本地):
```php
/** Redis连接信息host留空则启动时不创建Redis连接池 */
$config['redis_config'] = [
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 1,
'db_index' => 0,
'auth' => ''
];
```
## 使用
当写好配置文件后,不可以使用 reload 进行重载,因为连接池需要在主进程中声明配置,才可以应用到多个工作进程中。所以必须输入 `stop` 或 Ctrl+C 停止后再启动框架。
定义:`ZM\Store\Redis\ZMRedis`
因为使用的是连接池,所以每次使用完一个连接需要归还连接给连接池。框架封装了两种方式自动归还,你可以选择下面其中的任意一种。
以下的方式获取的 `$redis` 都是 `redis` 扩展的对象 `\Redis`,关于 redis 扩展的方法文档,详情见:[Redis 文档](https://www.php.cn/course/49.html)。
### 对象模式
```php
$obj = new ZMRedis();
$redis = $obj->get();
ctx()->reply($redis->ping("123"));
```
### 回调模式
```php
// 前面的代码
ZMRedis::call(function($redis) {
$redis->set("key1", "hello world");
$result = $redis->get("key1");
ctx()->reply($result);
});
// 后面的代码
```
### 二者的区别
选一个喜欢的就好。硬要是说区别的话,对象模式是在 PHP 自动回收这个 `ZMRedis` 对象时会归还连接,也可以通过手动 `unset($obj)` 进行回收,否则就会执行到函数结尾自动回收。切记不可将 `$obj` 对象持久化存到静态或全局变量等。
回调模式看似是回调,但是是同步执行的,不会发生顺序错乱。也就是说到了 `ZMRedis::call()` 方法里面的时候,后面的代码不会提前执行,是顺序执行的。回调的作用仅仅是用作自动回收连接对象。

View File

@@ -0,0 +1,73 @@
# SpinLock 自旋锁
前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。
## 配置
自旋锁使用无需配置,和 LightCache 同源。
## 使用
定义:`ZM\Store\Lock\SpinLock`
### SpinLock::lock($key)
给信号量 `$key` 上锁。如果该信号量已经被上锁,则原地等待直到其他资源释放锁。
```php
SpinLock::lock("foo");
```
### SpinLock::unlock($key)
给信号量 `$key` 释放锁。
```php
SpinLock::unlock("foo");
```
### SpinLock::tryLock($key)
给信号量 `$key` 上锁。如果该信号量已经被上锁,则立刻返回 false。
```php
SpinLock::trylock("foo");
```
## 综合实例
我们这里以之前在 LightCache 中的实例进行继续讲解,如何给之前那样的情况加锁:
```php
/**
* @RequestMapping("/test")
*/
public function test() {
SpinLock::lock("web_count"); // 加上这行
$s = LightCache::get("web_count");
if($s === null) $s = 1;
else $s += 1;
LightCache::set("web_count", $s);
SpinLock::unlock("web_count"); // 再加上这行
return "<h1>It works!</h1>";
}
```
加两行就 OK。这时再使用压测工具请求 200000 次,值就会是 200000 了!
原理剖析:在 LightCache 获取前,先对此内容上锁,这时如果其他进程有同时也在执行这个代码的时候,就会在 `SpinLock::lock()` 这行代码处原地等待,防止继续执行。等前面的那个进程执行到 `SpinLock::unlock()` 释放锁时,其他进程才可继续执行,从而避免了多个进程并行执行这段代码导致的数据错乱。
!!! error "警告"
使用锁时务必谨慎,如果不按照下面的规则使用自旋锁可能导致 CPU 占用率上升。
自旋锁使用约定:
- 使用 `SpinLock::lock()` 指定信号量名称时必须指定为字符串,且最好与你的 LightCache 缓存名称相同。
- 使用 `lock()` 时最好紧跟在 `LightCache::get()` 代码前。
- 使用自旋锁后,`LightCache::get()``LightCache::set()` 之间的代码段一定不能有 **读写文件、数据库操作和网络请求** 等代码,最好为纯 PHP 逻辑代码,且越短越好,如示例代码。
-`LightCache::set()` 后最好紧跟 `SpinLock::unlock()`
## 性能
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下使用自旋锁和不适用自旋锁的测试成绩时间分别为7.4s 和 6.9s。