共计 4427 个字符,预计需要花费 12 分钟才能阅读完成。
前言:
在理论开发我的项目中,产品一旦推广开来,总能遇到一些小问题。比方某个接口忽然就申请崩掉了,某个提交接口明明做了限度为什么就多出了好多反复的记录。还有是某个记录超过限度进行批改了,以下就以这几个小问题总结一下平时采取的解决办法。
场景:
1. 缓存生效场景,就比方某个接口做了数据缓存,缓存过期导致忽然某个时刻大量申请间接读数据库。解决办法设置 redis 缓存回调事件,订阅生效频道。所以这个也能够用来解决某些业务场景到期解决形式。
2. 接口幂等性场景,就比方注册接口,通过手机号查问是否存在记录。但有时呈现网络提早用户连点等状况,会呈现数据库呈现几条一样的用户数据记录。
3. 商品库存超卖场景,比方某个流动商品下单,多个用户同时下一个商品的订单,从而导致库存超卖的景象。解决办法能够应用乐观锁或者乐观锁解决此问题。
场景一,缓存生效回调。
1. 设置 Redis 回调事件办法。
(1). 关上 Redis 客户终端,输出命令非持久性的回调事件设置
config set notify-keyspace-events Ex
(2). windows 平台关上 Redis 装置目录中找到 ”redis.windows-service.conf”,而后关上编辑找到 notify-keyspace-events 那一行,去掉 ”#”,改为 notify-keyspace-events“Ex”。
(3). 其中 Redis 还能够设置订阅键名的回调,比方订阅某个键名的 del 操作等,能够在 conf 中设置不同的,办法网上也有的。
2. 订阅 redis 某个库的键生效的频道名。
能够在命令测试,也能够通过 PHP 代码订阅而后 cli 环境下运行脚本。
命令:subscribe __keyevent@0__:expired
3. 设置有限期
从新关上一个新的 redis 客户终端输出一个带有效期的键值对,如下(键名 test_key_name, 工夫 30s,值 ceshi)
命令:setex test_key_name 30 ceshi
4. 查看键生效回调订阅的命令窗口是否呈现生效的键名。
5. 代码实现键名的生效事件订阅。
<?php | |
// 设置 php 脚本执行工夫 | |
set_time_limit(0); | |
// 设置 socket 连贯超时工夫 | |
ini_set('default_socket_timeout', -1); | |
class redisSubscribe | |
{ | |
protected $config = [ | |
"host" => "127.0.0.1", | |
"password" => "6379" | |
]; | |
protected $redis; | |
public function __construct() | |
{ | |
try {$this->redis = new \Redis(); | |
$this->redis->pconnect($this->config['host'],$this->config['password']); | |
} catch(\Exception $e) {echo "redis 谬误:".$e->getMessage().PHP_EOL; | |
} | |
} | |
// 一般音讯订阅 | |
public function normal() | |
{ | |
// 申明频道名称 | |
$channelName = "test"; | |
try {$this->redis->subscribe([$channelName], function ($redis, $channel, $msg) | |
{ | |
echo 'channel:' . $channel . ',message:' . $msg . PHP_EOL; | |
file_put_contents('subscribe.log',"\n-".$msg."-\n",FILE_APPEND); | |
}); | |
} catch (\Exception $e) {echo $e->getMessage(); | |
} | |
} | |
// 订阅 Key 生效事件的频道 | |
public function keyNotify() | |
{ | |
echo "wathc keyNotify start~~".PHP_EOL; | |
// Key 事件回调 | |
//$channel = "__keyevent@0__:expired"; // 0 号库的 Key 过期事件频道名 | |
$channel = "__keyevent@*__:expired"; // 所有库的 Key 过期事件频道名 | |
try {$this->redis->subscribe([$channel], function ($redis, $channel, $msg) | |
{ | |
echo 'channel:' . $channel . '===========' . ',message:' . $msg . PHP_EOL; | |
file_put_contents('subscribe.log',"\n-".$msg."-\n",FILE_APPEND); | |
}); | |
} catch (\Exception $e) {echo $e->getMessage(); | |
} | |
} | |
} | |
(new redisSubscribe())->keyNotify(); | |
?> |
6. 通过 PHP-cli 运行该脚本
而后也能够 setex 一个短时间的键,而后查看命令是否输入该生效的键名。
7. 缓存生效利用开展。
(1). 代码中设置的所有键名都配置到我的项目的全局配置文件中。
(2). 服务器中开一个守护过程(继续运行订阅某个库或者所有库的键生效回调事件脚本)。
(3). 当该脚本有回调时,取出键名去全局缓存键名数组中匹配。
(4). 规定业务能够自行设计。
(5). 比方取出一个 ”cate5″ 的键名,则能够取资讯表中查问分类 ID 为 5 的所有数据而后再进行缓存。
(6). 缓存生效事件还一个高端玩法,就是取代某些定时工作。比方能够将某个订单作为键名缓存,当该键名生效就能够取出键名拿到 ID 去数据库中将订单状态批改为生效。
场景二,接口幂等性。
接口反复数据也就是在高并发下的数据增加场景。最典型的是注册接口,用户在网络提早大或者信号不稳固的状况下。并且同时大量用户在进行注册操作,用户点击了一次没反馈而后再次点击多个。
在没有做幂等性解决只是拿到手机号查询数据库是否存在,用户表又没分库分表,查问迟缓,查问进去后,多条并发的申请都绕过了手机号曾经存在的条件判断,所以就呈现了 ID 不同,然而其余字段一样的记录。
- 对于高并发数据增加,能够应用 Redis 的 setnx。
- setnx 是设置键并且在有效期内有值时,再次对该键名进行反复赋值无奈进行,会返回 0。
- 能够代码在对某些条件查问是否存在时,能够将条件组成键名赋值。增加记录时再次对键名从新赋值,返回 null 则示意曾经存在。
- 以下代码是我的项目中的一个测试方法,应用的 redis 是封装的,借鉴须要批改。
/** | |
* @Notes: 高并发避免反复提交(插入数据)【保障接口的幂等性】* @Interface preventRepeatSubmit | |
* @return mixed | |
* @author: bqs | |
* @Time: 2020/6/19 14:56 | |
*/ | |
public function preventRepeatSubmit() | |
{/* 比方查问某条 (什么条件) 记录是否存在,分布式锁机制[redis 的原子性 setnx] | |
* 1. 通过条件拼接为惟一的键名,将键名 setnx 设置一个 30s 有效期的值 | |
* 2. setnx 设置键名不胜利 (返回 0) 示意曾经存在,接口则间接返回记录曾经存在 | |
* 3. 依据该条件查询数据库记录,如果存在,接口再返回记录曾经存在 | |
*【只有增加记录前须要查问什么是否存在则都须要思考高并发状况,则通过此计划】*/ | |
$redis = Redis::db(0); | |
$no = date('YmdHis',time()).mt_rand(1000,9999); | |
//$no = 202006191537447811; | |
// 是否增加锁表 | |
$addLock = false; | |
if ($redis->setnx($no,1)) {$redis->expire($no,30); // 设置 30s 过期工夫 | |
} else {$addLock = true; // 订单曾经存在则锁住} | |
// 数据库查问是否存在 | |
$isExist = Db::name('ztest')->where(['no'=>$no])->find(); | |
if ($isExist) {$addLock = true;} | |
if (!$addLock) { | |
$data = [ | |
"no" => $no, | |
"tab_num" => 2, | |
"stock" => 20, | |
"create_time" => time()]; | |
$res = Db::name('ztest')->insertGetId($data); | |
} | |
return "增加数据胜利"; | |
} |
场景三,库存超卖。
库存超卖是一个很常见的秒杀或者其余高并发场景下的数据更新问题。网络上的解决办法也是多种多样,对该问题延长的数据库乐观锁,乐观锁的知识点也是不可胜数。
所以,这里我也不再介绍数据库的存储引擎机制,事务,表锁等概念。间接以代码展示,以下是以乐观锁实现的数据库更新问题。
- 高并发下,对单条记录的批改。个别批改前会对某字段进行判断,然而并发状况下,拿查问的后果进行拦挡是极其的不靠谱。不过也能够对查问进行加锁,然而须要在同一事务中。
- 库存字段增加无符号的字段束缚,所以再大的并发在批改为 0 之后也不会呈现正数了,在批改的操作时捕获批改为正数时的数据库异样。
- 表中增加 version 字段,这个也是网上盛传的乐观锁经典实例了,前面的原理和流程我就不介绍了,代码也是这样写的,所以间接贴代码了。
/** | |
* @Notes: 高并发乐观锁 -(更新数据)* @Interface testConcurrence | |
* @return mixed | |
* @author: bqs | |
* @Time: 2020/6/19 14:25 | |
*/ | |
public function testConcurrence() | |
{ | |
// 开启事务 | |
Db::startTrans(); | |
// 查问 ID25 以后的库存和版本号 | |
$curr = Db::name('ztest')->field('stock,version')->where(['id'=>25])->find(); | |
// 判断库存是否小于 0 | |
if ($curr && $curr['stock'] <= 0) {throw new \Exception('物品已售罄',302); | |
} | |
try { | |
// 批改库存 - 获取 ID25 的行琐 | |
$updateRes = Db::name('ztest')->where(['id'=>25,'version'=>$curr['version']])->update(['stock'=>$curr['stock']-1,'version'=>$curr['version']+1]); | |
// 标识并发过去批改的,拿到的 version 太旧,事务回滚从新回到查问再走一遍 | |
if (!$updateRes) {Db::rollback(); | |
} | |
} catch(\Exception $e) {Db::rollback(); | |
// 记录日志,或者返回 | |
} | |
// 事务提交 | |
Db::commit(); | |
return '购买胜利了'; | |
} |