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

一、基于Session的短信登陆

黑马点评关键业务流程梳理一_第1张图片

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

二、基于Redis的短信登陆

登陆验证流程:

黑马点评关键业务流程梳理一_第2张图片

黑马点评关键业务流程梳理一_第3张图片

 拦截器优化:

黑马点评关键业务流程梳理一_第4张图片

三、商户查询

3.1 缓存

黑马点评关键业务流程梳理一_第5张图片

3.2 缓存更新策略

黑马点评关键业务流程梳理一_第6张图片

3.3 缓存穿透解决方案

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

常见的解决方案有两种:

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

黑马点评关键业务流程梳理一_第7张图片

解决方案:

黑马点评关键业务流程梳理一_第8张图片

3.4 缓存击穿解决方案

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

黑马点评关键业务流程梳理一_第9张图片

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

黑马点评关键业务流程梳理一_第10张图片

黑马点评关键业务流程梳理一_第11张图片

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

获取的锁是分布式锁

黑马点评关键业务流程梳理一_第12张图片

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

黑马点评关键业务流程梳理一_第13张图片

四、优惠券异步秒杀

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

黑马点评关键业务流程梳理一_第14张图片

 优化后:

黑马点评关键业务流程梳理一_第15张图片

 黑马点评关键业务流程梳理一_第16张图片

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

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

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

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

 4.1 Lua脚本

-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]

-- 2.数据key
-- 2.1.库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

4.2 秒杀代码

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

4.3 消费消息队列

   private class VoucherOrderHandler implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    // 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
                    List> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
                    );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有消息,继续下一次循环
                        continue;
                    }
                    // 解析数据
                    MapRecord record = list.get(0);
                    Map value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    // 3.创建订单
                    createVoucherOrder(voucherOrder);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                    handlePendingList();
                }
            }
        }

        private void handlePendingList() {
            while (true) {
                try {
                    // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
                    List> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create("stream.orders", ReadOffset.from("0"))
                    );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有异常消息,结束循环
                        break;
                    }
                    // 解析数据
                    MapRecord record = list.get(0);
                    Map value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    // 3.创建订单
                    createVoucherOrder(voucherOrder);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }
    }

4.4 创建订单

    private void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        // 创建锁对象
        RLock redisLock = redissonClient.getLock("lock:order:" + userId);
        // 尝试获取锁
        boolean isLock = redisLock.tryLock();
        // 判断
        if (!isLock) {
            // 获取锁失败,直接返回失败或者重试
            log.error("不允许重复下单!");
            return;
        }

        try {
            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判断是否存在
            if (count > 0) {
                // 用户已经购买过了
                log.error("不允许重复下单!");
                return;
            }

            // 6.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock - 1") // set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                    .update();
            if (!success) {
                // 扣减失败
                log.error("库存不足!");
                return;
            }

            // 7.创建订单
            save(voucherOrder);
        } finally {
            // 释放锁
            redisLock.unlock();
        }
    }

五、问题总结

5.1 为什么要释放分布式锁需要用Lua脚本

黑马点评关键业务流程梳理一_第17张图片

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

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

黑马点评关键业务流程梳理一_第18张图片

你可能感兴趣的:(Redis实战-黑马点评,java)