• 黑马点评关键业务流程梳理


    一、基于Session的短信登陆

    session 共享问题:多台 Tomcat 并不共享 session 存储空间,当请求切换到不同 tomcat 服务时导致数据丢失的问题。

    二、基于Redis的短信登陆

    登陆验证流程:

     拦截器优化:

    三、商户查询

    3.1 缓存

    3.2 缓存更新策略

    3.3 缓存穿透解决方案

    缓存穿透是指客户端的数据在缓存中和数据中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

    常见的解决方案有两种:

    • 缓存空对象
      • 优点:实现简单,维护方便
      • 缺点:额外的内存消耗、可能造成短期的不一致(若之后有该数据插入到数据库,会造成缓存和数据库不一致问题)
    • 布隆过滤
      • 优点:内存占用较少,没有多余 key
      • 缺点:实现复杂,存在误判
         

    解决方案:

    3.4 缓存击穿解决方案

    缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问并且缓存冲击就按业务比较复杂的 Key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

    常见的解决方案有两种:

    • 互斥锁
    • 逻辑过期

     3.4.1 基于互斥锁方式解决缓存击穿

    获取的锁是分布式锁

    3.4.2 基于逻辑过期方式解决缓存击穿 

    四、优惠券异步秒杀

    优化前:查询MySQL速度慢,而且下一步操作需要等待上一步执行完成

     优化后:

     

    (1)新增秒杀优惠券的同时,将优惠券信息保存到Redis中

    (2)基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

    (3)如果抢购成功,将优惠券id和用户id封装后发送到消息队列

    (4)开启线程任务,不断从消息队列中获取信息,实现异步下单功能

     4.1 Lua脚本

    1. -- 1.参数列表
    2. -- 1.1.优惠券id
    3. local voucherId = ARGV[1]
    4. -- 1.2.用户id
    5. local userId = ARGV[2]
    6. -- 1.3.订单id
    7. local orderId = ARGV[3]
    8. -- 2.数据key
    9. -- 2.1.库存key
    10. local stockKey = 'seckill:stock:' .. voucherId
    11. -- 2.2.订单key
    12. local orderKey = 'seckill:order:' .. voucherId
    13. -- 3.脚本业务
    14. -- 3.1.判断库存是否充足 get stockKey
    15. if(tonumber(redis.call('get', stockKey)) <= 0) then
    16. -- 3.2.库存不足,返回1
    17. return 1
    18. end
    19. -- 3.2.判断用户是否下单 SISMEMBER orderKey userId
    20. if(redis.call('sismember', orderKey, userId) == 1) then
    21. -- 3.3.存在,说明是重复下单,返回2
    22. return 2
    23. end
    24. -- 3.4.扣库存 incrby stockKey -1
    25. redis.call('incrby', stockKey, -1)
    26. -- 3.5.下单(保存用户)sadd orderKey userId
    27. redis.call('sadd', orderKey, userId)
    28. -- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
    29. redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
    30. return 0

    4.2 秒杀代码

    1. public Result seckillVoucher(Long voucherId) {
    2. Long userId = UserHolder.getUser().getId();
    3. long orderId = redisIdWorker.nextId("order");
    4. // 1.执行lua脚本
    5. Long result = stringRedisTemplate.execute(
    6. SECKILL_SCRIPT,
    7. Collections.emptyList(),
    8. voucherId.toString(), userId.toString(), String.valueOf(orderId)
    9. );
    10. int r = result.intValue();
    11. // 2.判断结果是否为0
    12. if (r != 0) {
    13. // 2.1.不为0 ,代表没有购买资格
    14. return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
    15. }
    16. // 3.返回订单id
    17. return Result.ok(orderId);
    18. }

    4.3 消费消息队列

    1. private class VoucherOrderHandler implements Runnable {
    2. @Override
    3. public void run() {
    4. while (true) {
    5. try {
    6. // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
    7. List> list = stringRedisTemplate.opsForStream().read(
    8. Consumer.from("g1", "c1"),
    9. StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
    10. StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
    11. );
    12. // 2.判断订单信息是否为空
    13. if (list == null || list.isEmpty()) {
    14. // 如果为null,说明没有消息,继续下一次循环
    15. continue;
    16. }
    17. // 解析数据
    18. MapRecord record = list.get(0);
    19. Map value = record.getValue();
    20. VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
    21. // 3.创建订单
    22. createVoucherOrder(voucherOrder);
    23. // 4.确认消息 XACK
    24. stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
    25. } catch (Exception e) {
    26. log.error("处理订单异常", e);
    27. handlePendingList();
    28. }
    29. }
    30. }
    31. private void handlePendingList() {
    32. while (true) {
    33. try {
    34. // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
    35. List> list = stringRedisTemplate.opsForStream().read(
    36. Consumer.from("g1", "c1"),
    37. StreamReadOptions.empty().count(1),
    38. StreamOffset.create("stream.orders", ReadOffset.from("0"))
    39. );
    40. // 2.判断订单信息是否为空
    41. if (list == null || list.isEmpty()) {
    42. // 如果为null,说明没有异常消息,结束循环
    43. break;
    44. }
    45. // 解析数据
    46. MapRecord record = list.get(0);
    47. Map value = record.getValue();
    48. VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
    49. // 3.创建订单
    50. createVoucherOrder(voucherOrder);
    51. // 4.确认消息 XACK
    52. stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
    53. } catch (Exception e) {
    54. log.error("处理订单异常", e);
    55. }
    56. }
    57. }
    58. }

    4.4 创建订单

    1. private void createVoucherOrder(VoucherOrder voucherOrder) {
    2. Long userId = voucherOrder.getUserId();
    3. Long voucherId = voucherOrder.getVoucherId();
    4. // 创建锁对象
    5. RLock redisLock = redissonClient.getLock("lock:order:" + userId);
    6. // 尝试获取锁
    7. boolean isLock = redisLock.tryLock();
    8. // 判断
    9. if (!isLock) {
    10. // 获取锁失败,直接返回失败或者重试
    11. log.error("不允许重复下单!");
    12. return;
    13. }
    14. try {
    15. // 5.1.查询订单
    16. int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    17. // 5.2.判断是否存在
    18. if (count > 0) {
    19. // 用户已经购买过了
    20. log.error("不允许重复下单!");
    21. return;
    22. }
    23. // 6.扣减库存
    24. boolean success = seckillVoucherService.update()
    25. .setSql("stock = stock - 1") // set stock = stock - 1
    26. .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
    27. .update();
    28. if (!success) {
    29. // 扣减失败
    30. log.error("库存不足!");
    31. return;
    32. }
    33. // 7.创建订单
    34. save(voucherOrder);
    35. } finally {
    36. // 释放锁
    37. redisLock.unlock();
    38. }
    39. }

    五、问题总结

    5.1 为什么要用Lua脚本

    场景:线程1执行完业务,需要释放分布式锁时,先判断锁的标识为自己,但是释放锁的操作阻塞,此时锁被超时释放,被另外线程2获取之后,线程1已经认为是自己的锁,然后执行释放操作。因此使用Redis提供的Lua脚本,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性

    5.2 Redis 中 List、PubSub 和 Stream三者的特点

  • 相关阅读:
    赞奇科技出席江苏828 B2B企业服务峰会,助力企业数字化转型
    Linux修改SSH连接的默认端口
    聚类基本概念及常见聚类算法和EM算法
    Python编程基础 | Python编程基础面向对象编程
    蓝牙核心规范(V5.4)12.4-深入详解之广播编码选择
    MySQL InnoDB引擎优势以及共享表空间扩容和日志文件详解
    Linux 内存泄漏检测的基本原理
    免漫(安卓)
    Python while循环语句语法格式
    技术岗/算法岗面试如何准备?5000字长文、6个角度以2023秋招经历分享面试经验
  • 原文地址:https://blog.csdn.net/sfklyqh/article/details/126004884