mall
# ES 缓存商品信息
存入 spu 还是 sku?存入 sku
- spu 没有具体的价格
- spu 没有具体规格,不便于搜索和过滤
- spu 嵌套 sku,多层嵌套,不便于 es 搜索
每个 sku 都拥有相同的规格属性,要不要存入 es?要存
- 用空间换时间,如果不存就需要再查一次库,占用网络 IO
- 直接每个 sku 都存规格属性,冗余存储,直接在 es 返回页面所需的所有数据
# 规格参数
商品详情页展示不同规格(销售属性),选中不同规格如何知道对应的 spu?
返回的每个属性携带拥有此属性的 sku 集合,选中不同种类的属性,其中共同拥有的 sku 就是此 sku。
返回给前端的数据
销售属性 | 具体属性 | skuIds |
---|---|---|
颜色 | 白色 | 10,11 |
颜色 | 黑色 | 12,13 |
内存 | 4G | 10,12 |
内存 | 8G | 11,13 |
sku 对应属性
skuId | 颜色 | 内存 |
---|---|---|
10 | 白色 | 4G |
11 | 白色 | 8G |
12 | 黑色 | 4G |
13 | 黑色 | 8G |
界面展示
颜色 | 白色 | 黑色 |
内存 | 4G | 8G |
选中白色,对应 skuIds 为 10,11
- 内存 4G 对应 skuIds 为
10,12
,选中属性都有的 skuId 为 10,即:白色+4G 对应的 skuId 为 10 - 内存 8G 对应 skuIds 为
11,13
,选中属性都有的 skuId 为 11,即:白色+8G 对应的 skuId 为 11
# OAuth2
Web 网站的授权
- 网站跳转到三方授权界面,携带 client_id 和 redirect_url(回调地址)
- 用户授权成功,浏览器跳转到 redirect_url 并携带 code 参数
- redirect_url 收到 code 参数,通过认证服务器使用 code 来换取 access token
- 使用 access token 访问资源服务器来获取用户的开放信息 授权机制说明 - 微博API (opens new window)
# 单点登录 SSO
一个网站登录,其他网站(不用域名)也是登录状态。
网站A 和 网站B 使用同一个认证服务,认证成功将 token 存入认证服务 session(分布式 session)
- 网站A 使用认证服务时跳转到认证界面
- 认证成功在浏览器 session 中存入用户凭证(token)
- 跳转到 网站A 的 redirect_url 并携带 token
- 并将 token 存入网站A 的 session 中,网站A 通过 token 可以获取的身份信息
- 网站B 使用认证服务时跳转到 认证界面
- 由于 session 有当前用户的信息(token),直接跳转到网站B 的 redirect_url 并携带 token
- 并将 token 存入网站B 的 session 中,网站B 通过 token 可以获取的身份信息
- 网站A 和网站B 都有具体身份信息了。
# 购物车
需求:
- 未登录可选购商品加入(临时)购物车
- 登录成功将临时购物车中的商品合并到用户购物车 即: 未登录用户请求:
- 第一次使用,无 cookie,创建一个临时 cookie(uuid,30day)
- 有 cookie,购物项存入临时 cookie 登录用户:临时 cookie 中的购物项合并到用户购物车
购物车读多写多,使用 Redis 存储,类型为 Hash。
- key 为 userId 或 临时购物车 id(uuid,过期时间 30day)
- field 为 skuId
- value 为 sku 详情,包含 skuInfo 和 销售属性(Json 存储)
mall:cart:1
{
"check": true,
"count": 2,
"image": "https://mall-starry.oss-cn-shenzhen.aliyuncs.com/2023-08-05/50ff54ca-5d4a-4fb4-89d4-ff55b4731b8a_28f296629cca865e.jpg",
"price": 4388,
"skuAttrValues": [
"颜色:黑色",
"套装:8+128"
],
"skuId": 35,
"title": "华为/HUAWEI P60 超聚光XMAGE影像 双向北斗卫星消息 黑色 8+128",
"totalPrice": 8776
}
# 合并购物车
合并购物车时,sku 相同直接修改 sku 数量。sku 不同直接在 Redis 添加一个属性。合并完删临时 key。
# 查询购物项
Redis 中根据 userId 查询到对应的 hash,value 中的价格可能发送变化了,如何确保购物车价格是最新的。
- 修改价格时,同时更新缓存中的价格。
- 返回给前端购物项时,再查询一次最新的价格。(浪费性能)
更新缓存时,key 是以用户为维度的,如何更新所有的用户的购物车。
- 需要额外使用一个 key 来保存 sku 的价格。
skuId: price
# 订单
# 订单确认页(结算页)
需要数据:
- 收获地址列表
- 购物车为选中状态的购物项,购物项是否有库存
- 查询用户积分
- 总价格计算
- 额外携带一个 token 来防止重复提交
token 存入 redis,提交订单时删除 token,删除成功才创建订单;否则就是重复点击,不做处理。(token 需要设置一个有效期,防止一直占用内存)
# 提交订单
提交订单时把之前订单页的数据都提交到后台。
提交订单时需要再次计算对比价格,防止商家临时改变价格。 远程调用库存服务扣减库存,发送 mq 消息,防止后续步骤失败,通过 mq 消息进行回滚。 处理完成删除购物车中的数据。
# 分布式事务
失败数据回滚:
- 库存
- 积分
- 优惠券 通过 mq 保证数据最终一致性。
创建订单,远程调用
- 库存延迟队列 delay 2min
- 订单延迟队列 delay 1min
库存解锁 handler,wms 发出的 message,delay 2min
- 订单成功:
- 远程查询,订单状态不是取消状态,不做处理。
- 订单失败:
- 订单事务回滚,远程查询不到数据,回滚库存。
回滚时需要进行幂等处理,库存的状态必须为锁定状态。
订单失败 handler,order 发出的 message,delay 1min
- 订单失败:专门处理订单失败的 handler,不用远程查询,直接回滚库存。
# 秒杀
秒杀商品上架,库存预热,提交订单时直接从缓存中扣减库存,扣减成功发送商品信息到订单mq,扣减成功即秒杀成功,订单服务只用创建完订单即可。
库存扣减使用 redis 的 lua 脚本,或者 redisson 的 semaphore
local value = redis.call('get', KEYS[1])
if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then
local val = redis.call('decrby', KEYS[1], ARGV[1])
return 1
end
return 0
# 商品上架
定时任务,上架后2天内商品。 如何存储? 根据可秒杀的时段进行分类存储,1 小时内秒杀 和 全天秒杀。使用 hash 进行存储。
方案一 1 小时内秒杀:
- key:秒杀的整点开始时间(系统规定秒杀只能是整点开始)
- field:skuId
- value:skuInfo(Json) 全天秒杀:
- key:当天日期
- field:skuId
- value:skuInfo(Json)
方案二 使用 zset 来存储 score:秒杀开始的时间戳 value:skuInfo(Json)
如何查询当前时间秒杀有哪些秒杀商品?
- 查询当前时间的整点开始时间有哪些商品秒杀 + 查询全天秒杀的商品。
- 使用 zrange 来查询小于当前时间戳的数据。开始值当前时间戳,结束值 为当天时间的开始时间戳。
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
[WITHSCORES]
未在秒杀推荐列表展示,但是参与秒杀,需要根据 skuId 查询是否参与秒杀。
- 使用方案一时,直接从 hash 中判断是否存在 skuId。
HEXISTS key field
- 使用方案二时需要,上架秒杀商品时,设置一个缓存,缓存参与秒杀的 skuId。
# 优化
- 服务单一职责,可独立部署。扛不住,直接加机器;挂了不影响其他业务。
- 秒杀链接加密。防止恶意脚本,防止链接提前暴露,模拟请求。
- 库存预热,快速扣减。秒杀读多写少。无需每次实时校验库存。库存预热,放到 redis 中。信号量控制进来秒杀的请求。
- 动静分离。nginx 做好动静分离。保证秒杀和商品详情页的动态请求才打到后端的服务集群。使用 CDN 网络,分担集群压力。
- 恶意请求拦截。识别非法攻击请求并进行拦截,网关层。
- 流量错峰。使用各种手段,将流量分担到更大宽度的时间点。比如验证码,加入购物车。
- 限流、熔断、降级。前端限流 + 后端限流。限制次数,限制总量,快速失败降级运行,熔断隔离防止雪崩。
- 队列削峰。1 万个商品,每个 1000 件秒杀。双 11 所有秒杀成功的请求,进入队列,慢慢创建订单,扣减库存即可。