前言
在实际生活业务场景开发中,在我们的网站知名度越来越大的时候,随之而来的就是业务体量越来越大,用户群体越来越大,随之而来的技术要求也越来越高,其中核心点对网站的稳定性要求是硬性的。如果一个系统都无法正常提供访问了,那么对用户的体验感应该是极差的吧。本文总结了我所见所闻的知识,来提升网站吞吐能力,应对高迸发,高安全的实现方案。
业务痛点
数据库
一般来说,我们的系统崩溃,首先崩溃的是哪个设备?答案:肯定是数据库。
单机环境下,我们对数据库做 CURD 操作时,是需要占用 I/O 的,哪怕数据库提供了 I/O 缓冲,在大流量下也很容易把数据库干死,所以,我们首要目标是提升数据库的 吞吐 性能,保证 数据库 的稳定性。
读写分离
将 CURD 操作中,写操作(新增、更新、删除)交给 Master 主机操作,读操作(查询)交给 slave 从机操作。主从数据库通过 binlog 日志同步数据。将操作分开,大幅度提升数据的处理能力。其中从库(读库)可 横向扩展
可购买市面上 第三方服务商 提供的专业的数据库服务,如 阿里云RDS、阿里云PloarDb 底层架构中写库为 M-M 架构,采用ID 取模(%)判断是否等于0为基准处理数据是写入db1还是db2。双倍提升写库性能。
连接池
在程序中与 数据库 建立连接是需要消耗 资源 的,在某些后端开发框架的上下文全局环境中,哪怕我不对 数据库 做任何操作,一次请求也需要和 数据库 建立连接(未实现数据库连接 懒加载 功能),大量消耗了 数据库 的资源。以及某些业务场景下,对 数据库 的 I/O消耗不高,但对 数据库 连接的占用特别高(常见如根据主键 id查询某行数据,但流量太大,cpu持续飘高,但I/O指数不高)的场景。
由此的解决方案为建立数据库连接池。
1、基于 php swoole 可实现 数据库 连接池,相关内容可在百度中检索到。
2、采用 第三方服务商 提供的专业数据库服务,如数据库中间件默认就有连接池的实现。(推荐)
索引
索引对 数据库 是一个很重要的概念。它让原本很大数据体量的表能以较小的资源消耗,快速查询到目标数据(
如mysql 索引法:bree + 、hash 。类型:unique(唯一)、normal(普通)、fulltext(全文索引)、spatial(空间索引)
),但如何合理的使用索引是重要的点。
1、我们尽量让每一次查询都命中索引去查询,减少数据库的回表次数,达到最优查询的目的。
2、但不能无节制的去建立索引(如我给一张表每个字段都建立索引,这样是没有任何意义的)。
3、尽量避免子查询,子查询回会一次表,如果子查询表回的数据体量大,mysql需要开相应的内存区来存放回表数据。
4、避免索引失效(在where 中 字段使用表达式、隐式类型替换、不合理的(like "%x"),Or 条件中未使用索引查询,不合理的索引下推 均会导致全表扫描,降低查询速度,给 数据库 增加检索负担)
如何合理的建立索引说明可参考百度其它文档。
缓存
已 redis 为参照,缓存加入方案的目的有以下几点。
1、提升业务接口访问速度(部分业务数据的获取,从缓存中获取数据,因缓存是存放在内存中的,所以 I/O 处理极快)
2、对数据库进行减压出力(减少部分原是从数据库获取的数据途径,改为了从缓存中获取)
业务分析
分析业务场景,哪些数据是经常在读且不易发生变化的(如首页的Banner位显示、用户的基础数据:id、昵称、头像、性别等),这类数据可加入缓存中,在需要的直接从缓存中取出。不用从数据库中获取数据。达到降低数据库压力的目的。
热更新技术
这不是一个新技术,仅是一种设计模式。在后端开发技术中,他监听某个对象是否发生了变化,当发生变化后会把此对象数据同步至相关的缓存中来。
举例:商品库
某个商品的基本信息存放在 数据库 中,因业务需要和提升性能的目的,将商品的基本数据同样拷贝到了 缓存 中。在 业务场景 中,因业务需要,商品管理员在后端更改了商品的名称,而程序监听到这个动作(商品更新)发生后,抛出了商品更新 事件 ,由缓存数据中心监听到此 事件 的发生后,修改缓存中对应商品的信息。(事件机制原理详解,csdn 搜索-倾听岁月,其下有事件说明)
读写分离
没错,你没看错,redis 也支持 读写分离。类似 mysql 读写分离 的原理,M-S架构,依托binlog日志达到主从数据同步的目的。在redis 6.0 版本之后,工作 cpu 不再仅是单核的了。原理是命令的执行由一个cpu处理执行(单线程处理),但命令产生的I/O操作可以利用多核 cpu 的优势,由多核处理。性能进一步增强。
数据分片
如下图所示
我们将 业务数据 分摊给多个 redis 服务器存储,降低单台物理机或是单节点集群的压力。提升整体性能的设计。
注意事项
1、雪崩:单位时间内,缓存大量失效,且同时有大量的请求进来穿过了 redis 打在了数据库上执行查询操作。
对数据元不设置其过期时间,采用热更新技术+定时更新机制,维护热点数据
2、击穿:单位时间内,某个热点数据源因缓存时间过期失效,且同时有大量的请求进来穿过了 redis 打在了数据库上执行查询操作。
对数据元不设置其过期时间,采用 热更新技术 + 定时更新机制 ,维护热点数据
3、击穿:单位时间内,在缓存中未查找到目标数据,在数据库也没有查找到目标数据。如此大量请求循环。
常见如查询一个不存在的ID
服务器技术
单台服务器的处理能力总有其极限,我们不能说让一个1000W+访问量的系统交给一台机器去单独承受压力(服务器说:我承受了我不该承受的压力),既然是1000W+日活访问,单台服务器很难抗下来,那么我们就只有多加服务器来摊流了。
DNS 权重解析技术
这不是一个新技术,只要你买了域名,在一个域名下面挂载解析的ip后(多个ip),都会给你一个ip权重配比(权重你可以理解为就是解析的百分比)。 一个域名下最多可以解析10个ip(我试过,阿里云的域名解析最多允许我挂载10个ip,其它服务商的域名解析情况怎么样我还不清楚)。使用此技术,我们要处理session问题,将session数据的存储,放置在redis中,所有服务器关于session会话均连接同一台redis或是同一节点redis集群处理。
负载均衡技术(推荐)
核心技术:反向代理。 在负载均衡器下挂载多台服务器,当负载均衡器监听到有新的请求进来后会判断当前下面那些服务器的状态最好,然后优先将此请求交给目标服务器处理。基于 DNS 解析技术(解析的目标ip 地址是负载均衡地址),可达到 10 * 10 = 100 台服务器的挂载。
文件存储系统(OSS + CDN)
我们的系统中,总是有各种各样的静态资源,如css、js、image、mp3、mp4、其它静态资源等。针对这类资源,我们可以将其独立出去,不从业务服务器上供用户下载,将其放置在第三方文件服务系统上(阿里云的OSS 搭配 CDN加速(支持全球加速) 就不错),降低业务服务器的带宽和读写缓冲区的压力,让业务服务器专注于处理业务逻辑。
开发技术优化
以后端为php开发语言为例,我们采用lnmp 即是 linux + nginx + mysql + php 其中关于nginx和php-fpm的优化如下
nginx
1、keep-alive 长连接应该设置为开启,根据自己的业务需求分析,配置其长连接维持时间,过短或过长都不好,取一个能接受的居中值。
2、工作进程数一般设置为和 cpu 核数相同的工作进程数。 nginx-work 采用惊群模式和类似redis底层的epoll红黑树结构处理请求,具有超高的迸发性能。
php-fpm
1、php-fpm 的工作进程数一般设为 cpu 核数的2-8倍,具体几倍需要分析自己的业务。
进程序并不是越多越好,进程越多 cpu 就一直在不停的切换进程,结果反而消耗了不必要的资源(进程间的切换)。
2、分析自己的业务场景,是否有可以延时处理的操作。如 欢迎短信|邮件,excel导出,不需要即时反馈的数据统计,资金流转。
针对这类业务复杂但又不是必须立即给用户反馈的业务,可采用 消息队列 技术来处理,降低服务器压力
3、采用php8.x 版本,php8 的新特性 JIT 再一次加速了php的运行速度, JIT 依托扩展 opcode 所以要使用 JIT 一定要先开启 opcode,底层原理为多级缓存,原本是交给vm虚拟机运行的代码,直接编译为机器码缓存,之后在二次复用时直接从缓存获取机器码并在 cpu 上运行,速度极快,绕过了vm虚拟机,直接在 cpu 上执行。
架构设计高级进阶
经典案例 - 秒杀业务场景
说到秒杀,我们可以分析出秒杀业务场景的特点。
1、用户流量瞬时爆发。(大家都是在那1秒钟或是那一分钟来参与,在此时间段内,系统的流量暴涨)
2、库存的安全化。(如2W个库存,可能就在1-2秒就被秒没得了,单位时间内对数据库的压力极大,要保障数据库的安全性)
基于以上业务场景特点设计的架构图如下
思维
1、前端限制:当用户点击秒杀后,秒杀立即变灰(不可再次点击),考虑下用户,当看到一直在请求旋转的时候,我想是个用户都想再去点击一下,这样就产生了一个用户有多个秒杀请求
2、限流:一个用户在某一分钟内最多可请求n次,超出n次我们就提示排队中。
3、多库存记录:一个秒杀商品的库存,我们完全可以将这个库存摊到多个记录上,这样可以避免锁抢占,即是某一个时刻有n个请求在同时抢占一个记录的锁。如果只开一个库存记录,就好像几百万辆车依靠一条车道通信,速度自然很慢,容易产生堵塞,但如果我规划出1W条车道,那么解决这几百万的车流量不是轻轻松松么。
4、合理利用redis来限流:redis 是高性能的,我们可以利用这个特性,来帮助我们执行库存的查询操作。每次redis扣减库存事务执行成功,我们就认为这个用户拿到了通行证,可以执行后续的流程,未拿到通行证的用户将会返回获取通行证或是通知秒杀失败
5、乐观锁与死锁的联合使用,绝对保证商品库存不超出。
简易设计代码说明
step-1 : 先设计一个商品服务,用于管理商品
ShopServiceInterface
<?php
namespace App\Services\Shop;
use App\Models\Shop;
interface ShopServiceInterface
{
/**
* Notes:获取商品信息
* Author:tanyong
* DateTime:2022/7/13
* @param int $id 商品id
* @param array|null $fields 要查询的字段
* @param boolean 是否锁住记录(需配合事务使用)
* @return Shop | null
*/
public function get(int $id,array $fields=null,$lockForUpdate = false);
/**
* Notes:增加商品销售额
* Author:tanyong
* DateTime:2022/7/14
* @param Shop $shop 商品对象
* @param int $num 销售数量
* @return boolean
*/
public function incrShopDraw(Shop $shop,int $num);
/**
* Notes:增加商品销售额2
* Author:tanyong
* DateTime:2022/7/29
* @param array $where 查询条件
* @param int $num 销售数量
* @return int 受影响的行数
*/
public function incrShopDraw2(array $where, int $num);
/**
* Notes:更新商品信息
* Author:tanyong
* DateTime:2022/7/13
* @param Shop $shop 商品id
* @param array $data 需要更新的数据
* @return boolean 是否更新成功
*/
public function update(Shop $shop,array $data);
}
商品服务的实现
ShopService
<?php
namespace App\Services\Shop;
use App\Exceptions\AppException;
use App\Models\Shop;
class ShopService implements ShopServiceInterface
{
public function get(int $id, array $fields = null, $lockForUpdate = false)
{
$query = Shop::query();
if (!empty($fields)) {
$query->select($fields);
}
if ($lockForUpdate) {
$query->lockForUpdate();
}
return $query->where('id', $id)->first();
}
public function update(Shop $shop, array $data)
{
if (isset($data['id'])) {
throw new AppException('不可更新id');
}
foreach ($data as $k => $v) {
$shop->{$k} = $v;
}
return $shop->save();
}
public function incrShopDraw(Shop $shop, int $num)
{
$shop->increment('draw', $num);
}
public function incrShopDraw2(array $where, int $num)
{
$query = Shop::query();
return $query->where($where)->increment('draw',$num);
}
}
step2 - 秒杀会产生订单,所以我们再设计一个订单服务接口
OrderServiceInterface
<?php
namespace App\Services\Order;
use App\Models\Order;
use App\Models\Rely\CreateOrder;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
interface OrderServiceInterface
{
/**
* Notes:创建订单
* Author:tanyong
* DateTime:2022/7/13
* @param CreateOrder $order
* @return Order
*/
public function createOrder(CreateOrder $order);
/**
* Notes:获取订单 - 复杂搜索
* Author:tanyong
* DateTime:2022/7/13
* @param array $whereComplexSearch 复杂搜索,使用参见\App\Components\ORM\WhereUtil
* @param array|null $fields 要查询的字段
* @param null $type 返回类型 取值 App\V2\Components\ORM\OrmResultType
* @return Collection | LengthAwarePaginator | boolean | null | Order
*/
public function getComplexSearchOrder(array $whereComplexSearch, array $fields = null, $type = null);
}
制作一个订单服务的实现
<?php
namespace App\Services\Order;
use App\Components\ORM\OrmResultType;
use App\Components\ORM\WhereUtil;
use App\Models\Order;
use App\Models\Rely\CreateOrder;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
class OrderService implements OrderServiceInterface
{
public function createOrder(CreateOrder $order)
{
return Order::query()->create([
'orderno' => $order->orderNo,
'relationUid' => $order->relationUid,
'relationSid' => $order->relationSid,
'address' => json_encode($order->address,JSON_UNESCAPED_UNICODE),
'shopInfo' => json_encode($order->shopInfo,JSON_UNESCAPED_UNICODE),
'status' => $order->status
]);
}
public function getComplexSearchOrder(array $whereComplexSearch, array $fields = null, $type = null)
{
$query = Order::query();
WhereUtil::assemble($query,$whereComplexSearch);
return OrmResultType::returnResult($query,$type,$fields);
}
}
step3-因秒杀有瞬时爆炸的访问流量,所以我们要做好数据库的减压防护工作,所以在这里我们引入一个中间服务,用于缓存数据库中商品的库存,减小数据库的访问压力,数据缓存依赖redis服务
DataCacheServiceInterface
<?php
namespace App\Services\DataCache;
use App\Models\Shop;
/**
* 数据缓存服务
*/
interface DataCacheServiceInterface
{
/**
* Notes:获得商品缓存
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId
* @return Shop
*/
public function getShop(int $shopId);
/**
* Notes:设置商品缓存
* Author:tanyong
* DateTime:2022/7/13
* @param Shop $shop
* @return boolean
*/
public function setShop(Shop $shop);
/**
* Notes:清除商品缓存
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId
* @return mixed
*/
public function clearShop(int $shopId);
/**
* Notes:获取商品库存
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId 商品id
* @return int 商品库存
*/
public function getShopTotal(int $shopId);
/**
* Notes:设置商品库存
* Author:tanyong
* DateTime:2022/7/13
* @param Shop $shop
* @return boolean
*/
public function setShopTotal(int $shopId,$total);
/**
* Notes:商品库存自增
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId 商品id
* @return boolean
*/
public function incrShopTotal(int $shopId);
/**
* Notes:获得某商品所有的购买者(去重)
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId
* @return array
*/
public function getShopPurchaseUser(int $shopId);
/**
* Notes:向商品添加购买者
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId 商品id
* @param int $uids
* @return boolean
*/
public function addShopPurchaseUser(int $shopId,int ... $uids);
/**
* Notes:效验某个用户是否购买某商品
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId 商品id
* @param int $uid 用户uid
* @return boolean
*/
public function checkShopPurchaseUser(int $shopId,int $uid);
/**
* Notes:事务 - 扣减redis - 商品库存
* Author:tanyong
* DateTime:2022/7/13
* @param int $shopId 商品id
* @return boolean
*/
public function atomDecrShopTotal(int $shopId,int $operationNum = 1);
}
DataCacheService
<?php
namespace App\Services\DataCache;
use App\Components\ORM\OrmResultType;
use App\Enums\CacheKeyEnum;
use App\Enums\CacheTimeEnum;
use App\Exceptions\Codes;
use App\Exceptions\RedisException;
use App\Models\Shop;
use App\Services\Order\OrderServiceInterface;
use App\Services\Shop\ShopServiceInterface;
use Illuminate\Support\Facades\Redis;
class DataCacheService implements DataCacheServiceInterface
{
public $redis;
/**
* 商品服务
*/
public ShopServiceInterface $shopService;
/**
* 订单服务
*/
public OrderServiceInterface $orderService;
public function __construct(ShopServiceInterface $shopService,OrderServiceInterface $orderService)
{
$this->shopService = $shopService;
$this->orderService = $orderService;
$this->redis = Redis::connection()->client();
}
public function getShop(int $shopId)
{
$data = $this->redis->get(CacheKeyEnum::SHOP . $shopId);
if(empty($data))
{
$shop = $this->shopService->get($shopId);
$this->setShop($shop);
}else{
$shop = Shop::dispatchArray(json_decode($data,JSON_UNESCAPED_UNICODE));
}
return $shop;
}
public function setShop(Shop $shop)
{
return $this->redis->setex(
CacheKeyEnum::SHOP . $shop->id,
CacheTimeEnum::SHOP,
$shop->toJson(JSON_UNESCAPED_UNICODE)
);
}
public function clearShop(int $shopId)
{
return $this->redis->del(CacheKeyEnum::SHOP . $shopId);
}
public function getShopTotal(int $shopId)
{
$total = $this->redis->get(CacheKeyEnum::SHOP_TOTAL . $shopId);
if($total === null)
{
$shop = $this->getShop($shopId);
$total = $shop->getStock();
$this->setShopTotal($shop->id,$total);
}
return $total;
}
public function setShopTotal(int $shopId,$total)
{
return $this->redis->setex(
CacheKeyEnum::SHOP_TOTAL . $shopId,
CacheTimeEnum::SHOP_TOTAL,
$total
);
}
public function atomDecrShopTotal(int $shopId,int $operationNum = 1)
{
//反复执行3次均未通过则视为秒杀失败
if($operationNum >= 3)
return false;
$key = CacheKeyEnum::SHOP_TOTAL . $shopId;
$this->redis->watch($key);
$this->redis->multi();
$this->redis->decr($key);
$result = $this->redis->exec();
if(!$result)
{
$operationNum++;
return $this->atomDecrShopTotal($shopId,$operationNum);
}
return true;
}
public function incrShopTotal(int $shopId)
{
return $this->redis->incr(CacheKeyEnum::SHOP_TOTAL . $shopId);
}
public function getShopPurchaseUser(int $shopId)
{
return $this->redis->hGetAll(CacheKeyEnum::SHOP_PURCHASE_USERS . $shopId);
}
public function addShopPurchaseUser(int $shopId, int ... $uids)
{
$arr = [];
foreach($uids as $uid)
$arr[$uid] = $uid;
return $this->redis->hMSet(CacheKeyEnum::SHOP_PURCHASE_USERS . $shopId,$arr);
}
public function checkShopPurchaseUser(int $shopId, int $uid)
{
$key = CacheKeyEnum::SHOP_PURCHASE_USERS . $shopId;
if(!$this->redis->exists($key)) {
$wheres = [
['relationSid', 'eq', $shopId]
];
$orders = $this->orderService->getComplexSearchOrder($wheres, null, OrmResultType::GET);
if (!empty($orders))
{
$relationUids = $orders->map(function($order){
return $order->relationUid;
})->toArray();
if(!empty($relationUids))
$this->addShopPurchaseUser($shopId,...$relationUids);
}
}
return $this->redis->hExists($key,$uid);
}
}
step4-我们认为秒杀商品是一个业务动作行为,我们将秒杀商品抽象成一个接口
BusinessServiceInterface
<?php
namespace App\Services\Business;
use App\Exceptions\AppException;
use App\Models\Order;
/**
* 业务服务
*
*/
interface BusinessServiceInterface
{
/**
* Notes:商品购买
* Author:tanyong
* DateTime:2022/7/13
* @param int $memberId 会员id
* @param int $shopId 商品id
* @return Order 商品订单
* @throws AppException
*/
public function purchaseShop(int $memberId,int $shopId);
}
BusinessService
双锁实现
悲观锁:锁住商品记录,不允许同时操作一个商品记录
乐观锁:最后的保证商品库存的真实性
<?php
namespace App\Services\Business;
use App\Components\ORM\OrmResultType;
use App\Components\Util\StringUtil;
use App\Enums\OrderStatusEnum;
use App\Exceptions\BusinessException;
use App\Exceptions\Codes;
use App\Models\Rely\CreateOrder;
use App\Services\Order\OrderServiceInterface;
use App\Services\Shop\ShopServiceInterface;
use Illuminate\Support\Facades\DB;
class BusinessService implements BusinessServiceInterface
{
public function __construct(
/**
* 商品服务
*/
public ShopServiceInterface $shopService,
/**
* 订单服务
*/
public OrderServiceInterface $orderService,
)
{}
public function purchaseShop(int $memberId, int $shopId)
{
return DB::transaction(function() use($memberId,$shopId){
//获取商品信息并锁住记录 死锁
$shop = $this->shopService->get($shopId,null,true);
if($shop->total <= $shop->draw)
throw new BusinessException("库存不足",Codes::BUSINESS_ERROR);
//最终效验 - 用户是否已购买此商品
$wheres = [
['relationSid','eq',$shopId],
['relationUid','eq',$memberId]
];
$count = $this->orderService->getComplexSearchOrder($wheres,null,OrmResultType::COUNT);
if($count > 0)
throw new BusinessException("您已参与本次秒杀",Codes::BUSINESS_ERROR);
//创建订单
$createOrder = new CreateOrder();
$createOrder->orderNo = time() . StringUtil::randStr(8);
$createOrder->relationUid = $memberId;
$createOrder->relationSid = $shopId;
$createOrder->address = ['address'=>'测试地址'];
$createOrder->shopInfo = $shop->getSimple();
$createOrder->status = OrderStatusEnum::CREATE;
$order = $this->orderService->createOrder($createOrder);
//增加销售数量
//$this->shopService->incrShopDraw($shop,1);
//乐观锁
$result = $this->shopService->incrShopDraw2([
'id' => $shop->id,
'draw' => $shop->draw
],1);
if(!$result)
throw new BusinessException("秒杀失败",Codes::BUSINESS_ERROR);
// $shopService->update($shop,[
// 'draw' => ($shop->draw + 1)
// ]);
return $order;
},3);
}
}
step5-效验服务
CheckServiceInterface
<?php
namespace App\Services\Check;
interface CheckServiceInterface
{
/**
* Notes:效验用户是否购买某商品 (redis 队列效验)
* Author:tanyong
* DateTime:2022/7/13
* @param int $memberId 用户id
* @param int $shopId 商品id
* @return boolean
*/
public function checkMemberPurchaseShop(int $memberId,int $shopId);
}
CheckService
<?php
namespace App\Services\Check;
use App\Services\DataCache\DataCacheServiceInterface;
class CheckService implements CheckServiceInterface
{
public function checkMemberPurchaseShop(int $memberId, int $shopId)
{
/**
* 数据缓存服务
* @var DataCacheServiceInterface $dataCacheService
*/
$dataCacheService = app()->get(DataCacheServiceInterface::class);
return $dataCacheService->checkShopPurchaseUser($shopId,$memberId);
}
}
step6-控制层:业务逻辑封装,流程控制
<?php
namespace App\Http\Controllers;
use App\Components\ORM\OrmResultType;
use App\Exceptions\AppException;
use App\Exceptions\Codes;
use App\Exceptions\RedisException;
use App\Http\Requests\OrderCreateOrderRequest;
use App\Services\Business\BusinessServiceInterface;
use App\Services\Check\CheckServiceInterface;
use App\Services\DataCache\DataCacheServiceInterface;
use App\Services\Order\OrderServiceInterface;
use App\Services\Shop\ShopServiceInterface;
use App\Services\User\UserServiceInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
public function createOrder(OrderCreateOrderRequest $request)
{
try{
/**
* 会员服务
* @var UserServiceInterface $userService
*/
$userService = app()->get(UserServiceInterface::class);
/**
* 数据缓存服务
* @var DataCacheServiceInterface $dataCacheService
*/
$dataCacheService = app()->get(DataCacheServiceInterface::class);
/**
* 效验服务
* @var CheckServiceInterface $checkService
*/
$checkService = app()->get(CheckServiceInterface::class);
/**
* 业务服务
* @var BusinessServiceInterface $businessService
*/
$businessService = app()->get(BusinessServiceInterface::class);
//获取用户信息
$user = $userService->get($request->memberId);
if(empty($user))
throw new AppException('not get userinfo',Codes::USER_ERROR);
//获取商品库存
$shopTotal = $dataCacheService->getShopTotal($request->shopId);
if($shopTotal <= 0)
throw new AppException('库存不足',Codes::BUSINESS_ERROR);
//用户是否已成功参与秒杀
if($checkService->checkMemberPurchaseShop(shopId: $request->shopId,memberId: $request->memberId))
throw new AppException('您已成功参与秒杀 - 2',Codes::BUSINESS_ERROR);
//原子操作 - 自减redis商品库存
$result = $dataCacheService->atomDecrShopTotal($request->shopId);
if(!$result)
throw new AppException('抢购失败,请重新参与',Codes::BUSINESS_ERROR);
try{
//开始秒杀
$result = $businessService->purchaseShop($request->memberId,$request->shopId);
//加入队列
$dataCacheService->addShopPurchaseUser($request->shopId,$request->memberId);
}catch(\Exception $e)
{
//将分配的库存补充回redis
$dataCacheService->incrShopTotal($request->shopId);
throw $e;
}
return $result;
}catch(RedisException $e)
{
return [
'code' => 101,
'message' => "database error"
];
}
}
}
设计完毕。