场景:
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(39;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不同,但是其他字段一样的记录。
1. 对付高并发数据添加,可以利用Redis的setnx。
2. setnx是设置键并且在有效期内有值时,再次对该键名进行重复赋值无法进行,会返回0。
3. 可以代码在对某些条件查询是否存在时,可以将条件组成键名赋值。添加记录时再次对键名重新赋值,返回null则表示已经存在。
4. 以下代码是项目中的一个测试方法,利用的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 "添加数据成功"; }
库存超卖是一个很常见的秒杀或者其他高并发场景下的数据更新问题。网络上的办理方法也是多种多样,对该问题延伸的数据库乐不雅观锁,悲观锁的知识点也是数不胜数。
以是,这里我也不再先容数据库的存储引擎机制,事务,表锁等观点。直接以代码展现,以下因此乐不雅观锁实现的数据库更新问题。
1. 高并发下,对单条记录的修正。一样平常修正前会对某字段进行判断,但是并发情形下,拿查询的结果进行拦截是极其的不靠谱。不过也可以对查询进行加锁,但是须要在同一事务中。
2. 库存字段添加无符号的字段约束,以是再大的并发在修正为0之后也不会涌现负数了,在修正的操作时捕捉修正为负数时的数据库非常。
3. 表中添加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 '购买成功了'; }