博客记录-day133-力扣+项目库存扣减问题+Redis面试题

142 阅读20分钟

一、力扣

1、最深叶节点的最近公共祖先

1123.最深叶节点的最近公共祖先

image.png

思路:dfs返回子节点的深度,进而判断是否为最深的叶子结点

深度为需要dfs的返回值

class Solution {
    int dep=-1;
    TreeNode ans;
    public TreeNode lcaDeepestLeaves(TreeNode root) {
        dfs(root,0);
        return ans;
    }
    public int dfs(TreeNode root,int len){
        if(root==null){
            dep=Math.max(dep,len);
            return len;
        } 
        int left=dfs(root.left,len+1);
        int right=dfs(root.right,len+1);
        if(left==right&&left==dep){
            ans=root;
        }
        return Math.max(left,right);
    }
}

2、二叉树的最近公共祖先

236. 二叉树的最近公共祖先

image.png

image.png

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null) return null;
        if(root==p||root==q) return root;
        TreeNode left=lowestCommonAncestor(root.left,p,q);
        TreeNode right=lowestCommonAncestor(root.right,p,q);
        if(left!=null&&right!=null) return root;
        if(left==null) return right;
        else return left;
    }
}

3、链表相交

面试题 02.07. 链表相交

image.png

image.png

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode A = headA, B = headB;
        while (A != B) {
            A = A != null ? A.next : headB;
            B = B != null ? B.next : headA;
        }
        return A;
    }
}

4、两两交换链表中的节点

24. 两两交换链表中的节点

image.png

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy=new ListNode(0);
        dummy.next=head;
        ListNode fir=head;
        ListNode pre=dummy;
        while(fir!=null&&fir.next!=null){
            ListNode sec=fir.next;
            ListNode nextfir=sec.next;
            pre.next=sec;
            sec.next=fir;
            fir.next=nextfir;
            pre=fir;
            fir=nextfir;
        }
        return dummy.next;
    }
}

5、LRU 缓存

146. LRU 缓存

image.png

class LRUCache {
    class ListNode{
        int key,value;
        ListNode prev,next;
        public ListNode(){};
        public ListNode(int key,int value){
            this.key=key;
            this.value=value;
        }
    }
    ListNode dummy;
    int capacity;
    Map<Integer,ListNode> map;
    public LRUCache(int capacity) {
        dummy=new ListNode();
        dummy.next=dummy;
        dummy.prev=dummy;
        this.capacity=capacity;
        map=new HashMap<>();
    }
    
    public int get(int key) {
        if(map.containsKey(key)){
            ListNode target=map.get(key);
            remove(target);
            pushFirst(target);
            return target.value;
        }else{
            return -1;
        }
    }
    
    public void put(int key, int value) {
        if(map.containsKey(key)){
            ListNode target=map.get(key);
            target.value=value;
            remove(target);
            pushFirst(target);
        }else{
            ListNode target=new ListNode(key,value);
            pushFirst(target);
            map.put(key,target);
            if(map.size()>capacity){
                ListNode last=dummy.prev;
                remove(last);
                map.remove(last.key);
            }
        }
    }
    public void remove(ListNode target){
        target.prev.next=target.next;
        target.next.prev=target.prev;
    }
    public void pushFirst(ListNode target){
        ListNode temp=dummy.next;
        dummy.next=target;
        target.prev=dummy;
        temp.prev=target;
        target.next=temp;
    }
}

二、项目-库存扣减问题

1、奖品库存在redis预扣减核心代码

《大营销平台系统设计实现》 - 营销服务 第10节:不超卖库存规则实现

奖品库存是指还有几个奖品可以抽取。

扣减的是strategy_award表中的award_count_surplus字段,是实际奖品库存的扣减,在SKU扣减之后。

image.png

  1. 如果在前面章节所实现的规则树中,对于库存节点的操作,开发 decr 方式扣减库存。decr 是原子操作,效率非常高。这样要注意,setnx 加锁是一种兜底手段,避免后续有库存的恢复,导致库存从96消耗后又回到了98重复消费。所以对于每个key加锁,98、97、96... 即使有恢复库存也不会导致超卖。【setnx 在 redisson 是用 trySet 实现】
  2. 库存消耗完以后,还需要更新库表的数据量。但这会也不能随着用户消耗奖品库存的速率,对数据库表执行扣减操作。所以这里可以通过 Redisson 延迟队列 + 定时任务的方式,缓慢消耗队列数据来更新库表数据变化。

1. 库存扣减节点

@Override
public DefaultTreeFactory.TreeActionEntity logic(String userId, Long strategyId, Integer awardId, String ruleValue, Date endDateTime) {
    log.info("规则过滤-库存扣减 userId:{} strategyId:{} awardId:{}", userId, strategyId, awardId);
    // 扣减库存
    Boolean status = strategyDispatch.subtractionAwardStock(strategyId, awardId, endDateTime);
    // true;库存扣减成功,TAKE_OVER 规则节点接管,返回奖品ID,奖品规则配置
    if (status) {
        log.info("规则过滤-库存扣减-成功 userId:{} strategyId:{} awardId:{}", userId, strategyId, awardId);

        // 写入延迟队列,延迟消费更新数据库记录。【在trigger的job;UpdateAwardStockJob 下消费队列,更新数据库记录】
        strategyRepository.awardStockConsumeSendQueue(StrategyAwardStockKeyVO.builder()
                .strategyId(strategyId)
                .awardId(awardId)
                .build());

        return DefaultTreeFactory.TreeActionEntity.builder()
                .ruleLogicCheckType(RuleLogicCheckTypeVO.TAKE_OVER)
                .strategyAwardVO(DefaultTreeFactory.StrategyAwardVO.builder()
                        .awardId(awardId)
                        .awardRuleValue(ruleValue)
                        .build())
                .build();
    }

    // 如果库存不足,则直接返回放行
    log.warn("规则过滤-库存扣减-告警,库存不足。userId:{} strategyId:{} awardId:{}", userId, strategyId, awardId);
    return DefaultTreeFactory.TreeActionEntity.builder()
            .ruleLogicCheckType(RuleLogicCheckTypeVO.ALLOW)
            .build();
}

2. 底层方法

2.1 库存扣减
String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;

策略奖品的扣减cacheKey使用的是 strategyId +awardId。

@Override
public Boolean subtractionAwardStock(String cacheKey, Date endDateTime) {
    long surplus = redisService.decr(cacheKey);
    if (surplus < 0) {
        // 库存小于0,恢复为0个
        redisService.setAtomicLong(cacheKey, 0);
        return false;
    }
    // 1. 按照cacheKey decr 后的值,如 99、98、97 和 key 组成为库存锁的key进行使用。
    // 2. 加锁为了兜底,如果后续有恢复库存,手动处理等,也不会超卖。因为所有的可用库存key,都被加锁了。
    String lockKey = cacheKey + Constants.UNDERLINE + surplus;
    Boolean lock = false;
    if (null != endDateTime) {
        long expireMillis = endDateTime.getTime() - System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
        lock = redisService.setNx(lockKey, expireMillis, TimeUnit.MILLISECONDS);
    } else {
        lock = redisService.setNx(lockKey);
    }
    if (!lock) {
        log.info("策略奖品库存加锁失败 {}", lockKey);
    }
    return lock;
}
2.2 延迟队列

延迟队列通过​​异步处理​​和​​削峰填谷​​机制降低数据库压力:它将非实时任务(如库存回滚、数据同步)暂存队列,避免高频直接操作数据库;在低负载时段逐步处理积压任务,减少瞬时数据库连接数和锁竞争,同时通过批量操作进一步降低单次 I/O 开销,从而提升系统吞吐量并保障数据库稳定性。

@Override
// 定义发放奖励库存的消费入延迟队列方法,接收策略奖品库存键值对象作为参数
public void awardStockConsumeSendQueue(StrategyAwardStockKeyVO strategyAwardStockKeyVO) {
    
    // 从常量类获取Redis中存储策略奖励库存查询的键值(用于标识业务队列)
    String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_QUERY_KEY;
    
    // 通过Redisson客户端获取指定键的阻塞队列实例(RBlockingQueue是Redisson提供的阻塞队列实现)
    RBlockingQueue<StrategyAwardStockKeyVO> blockingQueue = redisService.getBlockingQueue(cacheKey);
    
    // 基于阻塞队列创建关联的延迟队列(RDelayedQueue,消息需延迟指定时间后才能被消费者获取)
    RDelayedQueue<StrategyAwardStockKeyVO> delayedQueue = redisService.getDelayedQueue(blockingQueue);
    
    // 将库存消耗任务发送至延迟队列,设置延迟时间为3秒(注意:此处延迟时间应考虑业务实际需求)
    delayedQueue.offer(strategyAwardStockKeyVO, 3, TimeUnit.SECONDS);
    
}

3. 异步队列定时任务更新奖品库存

/**
 * @description 更新奖品库存任务;为了不让更新库存的压力打到数据库中,
 * 这里采用了redis更新缓存库存,异步队列更新数据库,数据库表最终一致即可。 
 */

@Scheduled(cron = "0/5 * * * * ?")
public void exec() {
    try {
        StrategyAwardStockKeyVO strategyAwardStockKeyVO = raffleStock.takeQueueValue();
        if (null == strategyAwardStockKeyVO) return;
        log.info("定时任务,更新奖品消耗库存 strategyId:{} awardId:{}", strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
        raffleStock.updateStrategyAwardStock(strategyAwardStockKeyVO.getStrategyId(), strategyAwardStockKeyVO.getAwardId());
    } catch (Exception e) {
        log.error("定时任务,更新奖品消耗库存失败", e);
    }
}

2、 活动sku在redis预扣减核心代码

《大营销平台系统设计实现》 - 营销服务 第16节:引入MQ处理活动SKU库存一致性

活动的类似给你个人账户充值,让你多少次可以参与活动。

活动SKU扣减的是raffle_activity_sku表中的stock_count_surplus字段,在策略奖品扣减之前。

image.png

  1. 第一步;完成责任链的活动校验,时间、状态、库存。
  2. 第二步;对库存的扣减,使用 decr + lock 锁的方式(兜底)进行处理。
  3. 第三步;做完库存扣减后,发送延迟队列,由任务调度更新趋势库存,满足最终一致。
  4. 第四步;库存消耗为0后,发送MQ消息,驱动变更数据库库存为0

1. 库存扣减结点

@Override
public boolean action(ActivitySkuEntity activitySkuEntity, ActivityEntity activityEntity, ActivityCountEntity activityCountEntity) {
    log.info("活动责任链-商品库存处理【有效期、状态、库存(sku)】开始。sku:{} activityId:{}", activitySkuEntity.getSku(), activityEntity.getActivityId());
    // 扣减库存
    boolean status = activityDispatch.subtractionActivitySkuStock(activitySkuEntity.getSku(), activityEntity.getEndDateTime());
    // true;库存扣减成功
    if (status) {
        log.info("活动责任链-商品库存处理【有效期、状态、库存(sku)】成功。sku:{} activityId:{}", activitySkuEntity.getSku(), activityEntity.getActivityId());

        // 写入延迟队列,延迟消费更新库存记录
        activityRepository.activitySkuStockConsumeSendQueue(ActivitySkuStockKeyVO.builder()
                .sku(activitySkuEntity.getSku())
                .activityId(activityEntity.getActivityId())
                .build());

        return true;
    }

    throw new AppException(ResponseCode.ACTIVITY_SKU_STOCK_ERROR.getCode(), ResponseCode.ACTIVITY_SKU_STOCK_ERROR.getInfo());
}

2. 底层方法

2.1 库存扣减

/**
 * 根据策略ID和奖品ID,扣减奖品缓存库存
 *
 * @param sku 互动SKU
 * @param endDateTime 活动结束时间,根据结束时间设置加锁的key为结束时间
 * @return 扣减结果
 */

@Override
public boolean subtractionActivitySkuStock(Long sku, String cacheKey, Date endDateTime) {
    long surplus = redisService.decr(cacheKey);
    if (surplus == 0) {
        // 库存消耗没了以后,发送MQ消息,更新数据库库存
        eventPublisher.publish(activitySkuStockZeroMessageEvent.topic(), activitySkuStockZeroMessageEvent.buildEventMessage(sku));
    } else if (surplus < 0) {
        // 库存小于0,恢复为0个
        redisService.setAtomicLong(cacheKey, 0);
        return false;
    }

    // 1. 按照cacheKey decr 后的值,如 99、98、97 和 key 组成为库存锁的key进行使用。
    // 2. 加锁为了兜底,如果后续有恢复库存,手动处理等【运营是人来操作,会有这种情况发放,系统要做防护】,也不会超卖。因为所有的可用库存key,都被加锁了。
    // 3. 设置加锁时间为活动到期 + 延迟1天
    String lockKey = cacheKey + Constants.UNDERLINE + surplus;
    long expireMillis = endDateTime.getTime() - System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
    Boolean lock = redisService.setNx(lockKey, expireMillis, TimeUnit.MILLISECONDS);
    if (!lock) {
        log.info("活动sku库存加锁失败 {}", lockKey);
    }
    return lock;
}

该代码通过Redis原子操作高效管理库存扣减,具备以下核心优势:

  1. ​防超卖机制​​:采用decr原子命令确保库存扣减线程安全,结合库存为0时触发MQ异步更新数据库,实现最终一致性。
  2. ​负库存兜底​​:当库存异常为负时自动归零,阻断错误扩散。
  3. ​分布式锁防护​​:基于剩余库存值动态生成锁Key,配合活动到期时间+1天动态过期策略,既规避并发冲突,又防止人工恢复库存时的超卖风险。
  4. ​弹性扩展设计​​:通过缓存与数据库双写校验,支撑高并发场景,同时保留人工干预能力,平衡系统性能与数据可靠性。
  5. 为什么可以防超卖:该代码通过为每次库存扣减后的剩余值动态生成唯一锁Key(如cacheKey_99),并设置锁过期时间为活动到期后延迟1天,从而在人工恢复库存时强制串行化操作:即使多线程/多操作同时触发库存回补,同一库存状态的恢复请求会被同一锁Key竞争拦截,仅允许单次操作生效(需等待锁释放),确保人工干预时库存更新按顺序执行,避免并发恢复导致超卖。

decr是保证扣减时候的瞬时一致性。

setnx是使用用户的ID加上decr扣减后的库存值来加锁是为了保证不会超卖。

2.2 延迟队列

延迟队列通过​​异步处理​​和​​削峰填谷​​机制降低数据库压力:它将非实时任务(如库存回滚、数据同步)暂存队列,避免高频直接操作数据库;在低负载时段逐步处理积压任务,减少瞬时数据库连接数和锁竞争,同时通过批量操作进一步降低单次 I/O 开销,从而提升系统吞吐量并保障数据库稳定性。

@Override
// 定义活动SKU库存消费的延迟队列发送方法,接收活动SKU库存键值对象作为参数
public void activitySkuStockConsumeSendQueue(ActivitySkuStockKeyVO activitySkuStockKeyVO) {

    // 从常量类获取Redis中存储活动SKU库存查询的键值(用于标识具体业务队列,例如:"activity:sku:count:query")
    String cacheKey = Constants.RedisKey.ACTIVITY_SKU_COUNT_QUERY_KEY;

    // 通过Redisson客户端获取指定键的阻塞队列实例(RBlockingQueue基于Redis List实现,支持分布式阻塞操作)
    RBlockingQueue<ActivitySkuStockKeyVO> blockingQueue = redisService.getBlockingQueue(cacheKey);

    // 创建关联的延迟队列(RDelayedQueue会将消息延迟指定时间后,再转移到RBlockingQueue供消费者消费)
    RDelayedQueue<ActivitySkuStockKeyVO> delayedQueue = redisService.getDelayedQueue(blockingQueue);

    // 将库存消费任务发送至延迟队列,设置延迟时间为3秒(需确认是否符合业务场景,如是否需等待用户点击事件)
    delayedQueue.offer(activitySkuStockKeyVO, 3, TimeUnit.SECONDS);
}

这里是延迟的方法消息到 Redis 队列中。以此来减缓消费。(这里是一个双重减缓,一个是延迟队列,一个是定时的任务调度)

3. 定时任务更新活动sku库存

@Scheduled(cron = "0/5 * * * * ?")
public void exec() {
    try {
        ActivitySkuStockKeyVO activitySkuStockKeyVO = skuStock.takeQueueValue();
        if (null == activitySkuStockKeyVO) return;
        log.info("定时任务,更新活动sku库存 sku:{} activityId:{}", activitySkuStockKeyVO.getSku(), activitySkuStockKeyVO.getActivityId());
        skuStock.updateActivitySkuStock(activitySkuStockKeyVO.getSku());
    } catch (Exception e) {
        log.error("定时任务,更新活动sku库存失败", e);
    }
}

4. RabbitMQ监听消费

@RabbitListener(queuesToDeclare = @Queue(value = "${spring.rabbitmq.topic.activity_sku_stock_zero}"))
public void listener(String message) {
    try {
        log.info("监听活动sku库存消耗为0消息 topic: {} message: {}", topic, message);
        // 转换对象
        BaseEvent.EventMessage<Long> eventMessage = JSON.parseObject(message, new TypeReference<BaseEvent.EventMessage<Long>>() {
        }.getType());
        Long sku = eventMessage.getData();
        // 更新库存
        skuStock.clearActivitySkuStock(sku);
        // 清空队列 「此时就不需要延迟更新数据库记录了」
        skuStock.clearQueueValue();
    } catch (Exception e) {
        log.error("监听活动sku库存消耗为0消息,消费失败 topic: {} message: {}", topic, message);
        throw e;
    }
}

3、库存扣减

1. 使用decr+setnx原因

核心目标

在高并发场景下,通过 SETNX 结合用户 ID 和 DECR 扣减后的库存值加锁,核心目标是确保同一用户对特定资源的操作是串行化的,避免重复扣减或重复操作,同时防止库存超卖或数据竞争问题。例如,在秒杀场景中,用户可能快速重复提交请求,而 SETNX 的排他性锁能强制同一用户的请求按顺序处理,而 DECR 的原子性扣减则保证库存变化的准确性。


具体场景与逻辑

假设用户抢购商品时,系统需处理两大核心问题:一是库存扣减的准确性(避免超卖),二是同一用户不能重复抢购同一商品。通过 DECR 原子性扣减库存(如 DECR stock:123),可以直接减少库存并返回剩余值,若结果为负则说明库存不足。但若用户多次点击提交,可能导致重复扣减,因此需用 SETNXlock:user:123:stock:456 加锁,确保同一用户对同一商品的扣减操作串行执行。若锁已存在(SETNX 返回 0),则拒绝重复请求;若锁不存在(SETNX 返回 1),则加锁并执行扣减。


为何需要这种组合

DECR 提供无锁化的原子操作,适合高并发场景下的快速扣减,但无法防止用户重复提交或并发后置校验问题(如扣减后库存不足需回滚)。而 SETNX 的排他性锁能强制串行化用户操作,但会引入性能瓶颈。两者的组合通过分工解决不同问题:DECR 保证扣减效率,SETNX 解决用户维度的并发冲突。锁的粒度设计为用户 ID+商品 ID 的组合键(而非全局锁),既能限制同一用户的重复操作,又避免全局锁竞争,提升系统吞吐量。


完整流程示例
  1. 生成锁键:根据用户 ID 和商品 ID 生成唯一锁键(如 lock:user:123:stock:456)。
  2. 尝试加锁:通过 SETNX 尝试加锁,若成功则继续执行,否则返回“重试”。
  3. 扣减库存:使用 DECR 原子性扣减库存,若结果非负则生成订单,否则回滚(通过 INCR 恢复库存)。
  4. 释放锁:无论成功与否,最终通过 DEL 释放锁,避免死锁。整个过程需在 try-finally 中确保锁释放,即使发生异常也能正确清理资源。

关键优势

防重复操作SETNX 确保同一用户的请求按顺序处理,避免重复扣减。
原子性保障DECR 直接操作 Redis,无需依赖数据库事务,性能更高。
细粒度锁:用户+商品的锁粒度减少竞争,避免全局锁的性能损耗。


总结

SETNX + DECR 的组合通过分工协作解决高并发场景的核心矛盾:DECR 提供高效的库存扣减能力,SETNX 则通过用户维度的锁机制避免重复操作。其设计核心是平衡性能与一致性,通过细粒度锁降低系统竞争,同时依赖 Redis 的原子性操作保障数据可靠性。这一模式适用于需严格限制用户重复操作的场景(如秒杀、限购),但对性能要求极高时需进一步优化锁策略。

4、策略奖品和活动SKU的区别

1. 策略奖品(Strategy Award)

  1. 定义与作用

    • 策略奖品是与抽奖策略(Strategy)相关联的奖品
    • 策略奖品主要由策略ID和奖品ID组成
    • 关注的是奖品在策略中的配置和分配规则
  2. 核心属性

    // 策略ID
    private Long strategyId;
    // 奖品ID
    private Integer awardId;
    
  3. 相关服务

    • 接口负责处理策略奖品的库存相关操作
    • 提供了获取奖品库存消耗队列和更新奖品库存消耗记录的功能

活动SKU(Activity SKU)

  1. 定义与作用

    • 活动SKU是与具体活动相关联的商品单元
    • 在中可以看到,活动SKU主要由商品sku和活动ID组成
    • 关注的是活动中具体商品的库存和状态管理
  2. 核心属性

    /** 商品sku */
    private Long sku;
    /** 活动ID */
    private Long activityId;
    
  3. 相关服务

    • 接口负责处理活动SKU的库存相关操作
    • 提供了获取活动sku库存消耗队列、清空队列、更新活动sku库存和清空活动sku库存的功能

3. 主要区别

  1. 关注点不同

    • 策略奖品:关注的是抽奖策略中奖品的配置和分配规则
    • 活动SKU:关注的是活动中具体商品的库存和状态管理
  2. 使用场景不同

    • 策略奖品:用于定义抽奖策略中各个奖品的概率、数量等配置
    • 活动SKU:用于管理活动中实际商品的库存和状态
  3. 处理流程不同

    • 策略奖品:在中可以看到,系统通过定时任务更新活动SKU库存
    • 活动SKU:在中可以看到,系统通过监听消息来处理活动SKU库存为0的情况

总结来说,策略奖品更多地关注抽奖策略的配置和规则,而活动SKU更多地关注具体活动中商品的库存和状态管理。它们共同构成了抽奖系统中奖品管理的完整体系。

5、库存扣减加锁的原因

因为redis可能出现网络波动,或主从同步的问题。如果有一个分区内的库存是99个,另外一个分区因为网络波动,库存为101个,又因为redis是AP的系统,假设没有setnx锁的话,会产生超卖的现象。 有setnx锁,那么用户就无法再对第101库存进行消费。进而避免了超卖的发生。

使用 setNx 在库存扣减中的核心价值是:

  1. 保证库存操作的原子性和一致性
  2. 防止超卖问题
  3. 应对人为干预的兜底保障
  4. 与异步更新机制配合,提高系统性能

这种设计体现了分布式系统中对数据一致性和高并发处理的专业考量,是保证库存准确性的关键机制。

6、如何增加库存

1. 为什么扣减用decr,不使用incr。

因为 decr 命令是Redis提供的原子性递减操作,它能确保在分布式环境下,多个客户端同时请求时不会出现竞态条件。decr 命令会立即返回递减后的值,这使得系统可以立即判断库存是否充足,无需额外查询。

incr的核心是增加库存,再与总库存比较来判断是否超卖,这个只能用lua来保证原子性,进而lua的性能不如decr,所以扣减使用decr。

  • 原子操作优化 : decr 是Redis的内置命令,经过高度优化,直接在C语言层面实现,而Lua脚本需要经过Lua解释器执行,增加了额外的解释开销。
  • 执行路径更短 : decr 命令的执行路径非常短,Redis收到命令后直接执行对应的C函数;而Lua脚本需要加载脚本、解析脚本、执行脚本,执行路径更长。

2. 为什么补充库存使用incr或lua

补充库存应该重新设计数据库表,变为总的库存数和已经消费的库存数。可以先调整数据库库存,之后使用 incrby 给缓存加库存。这样的话加锁就不会出现问题。

使用decr+setnx在库存恢复中出现的潜在问题:库存从10扣减到5,10 9 8 7 6都已经被加锁了,现在补充5个库存,库存恢复到10,那接下来10个库存都卖不出去了。

因此使用incr不会存在上述问题。

7、如果中奖记录这个事务失败,但是发送了mq怎么办呢?

写入中奖记录,写task和更新用户抽奖单是在一个事务里,之后又发送的mq,作为补偿机制,但是如果中奖记录这个事务失败,但是发送了mq怎么办呢?中奖记录这些没写入,但是会做发放奖品的操作?

答: 不会的,事务执行失败后会调用status.setRollbackOnly()进行回滚。

三、语雀-Redis面试题

1、如何基于Redisson实现一个延迟队列

✅如何基于Redisson实现一个延迟队列

可以看上文的延迟队列,是一样的。

image.png

image.png 基于Redisson实现延迟队列的核心步骤如下:首先通过RedissonClient创建分布式连接,定义普通队列RQueue<T>和延迟队列RDelayedQueue<T>(绑定普通队列),生产者调用RDelayedQueue.offer(object, delay, timeUnit)方法发送带延迟的消息,消费者则通过RQueue.take()poll()方法监听并消费到期消息,Redisson内部基于有序集合(Sorted Set)和看门狗机制自动管理消息的延迟到期与转移,确保消息在指定时间后进入可消费队列。

2、介绍下Redis集群的脑裂问题?

✅介绍下Redis集群的脑裂问题?

1. 产生原因

image.png

2. 解决办法

image.png

3、Redis中key过期了一定会立即删除吗

✅Redis中key过期了一定会立即删除吗

image.png

4、Redis中有一批key瞬间过期,为什么其它key的读写效率会降低?

✅Redis中有一批key瞬间过期,为什么其它key的读写效率会降低?

image.png

5、如何基于Redis实现滑动窗口限流?

✅如何基于Redis实现滑动窗口限流?

image.png

image.png

6、Redis的Key和Value的设计原则有哪些?

✅Redis的Key和Value的设计原则有哪些?

1. key原则

image.png

2. value原则

image.png

7、Redis的事务和Lua之间有哪些区别?

✅Redis的事务和Lua之间有哪些区别?

image.png

image.png

8、Redisson的lock和tryLock有什么区别?

✅Redisson的lock和tryLock有什么区别?

image.png

RLock lock = redisson.getLock("myLock");
boolean isLocked = lock.tryLock(); // 非阻塞方法,立即返回获取结果
if (isLocked) {
    try {
    // 执行临界区代码
    } finally {
    lock.unlock();
    }
} else {
// 获取锁失败,处理逻辑
}

image.png

RLock lock = redisson.getLock("myLock");
lock.lock(); // 阻塞方法,直到获取到锁
try {
    // 执行代码
} finally {
    lock.unlock();
}

所以,我们可以认为,lock实现的是一个阻塞锁,而tryLock实现的是一个非阻塞锁(在没有指定waitTime的情况下)。

9、为什么Redis不支持回滚?

✅为什么Redis不支持回滚?

image.png

10、如何用Redis实现乐观锁?

✅如何用Redis实现乐观锁?

image.png

11、watchdog一直续期,那客户端挂了怎么办?

✅watchdog一直续期,那客户端挂了怎么办?

image.png 当客户端挂掉而watchdog持续续期时,需结合多层级容错机制:1. 心跳双向验证:客户端需主动上报状态,若服务端长期未收到心跳则判定失效;2. 超时阈值动态调整:根据历史稳定性动态延长超时时间,避免误判瞬时故障;3. 级联守护进程:部署监控代理(如systemd/supervisor)作为第二道防线,在应用层watchdog失效时触发进程重启;4. 健康检查接口:暴露HTTP/TCP探活端点,结合外部监控系统(如Prometheus)进行主动探测;5. 熔断与降级:客户端挂起超时后自动切断非核心功能,保障基础服务可用性。最终需通过日志溯源+全链路监控定位根因,而非单纯依赖续期机制。

12、如何用setnx实现一个可重入锁?

✅如何用setnx实现一个可重入锁?

image.png

以上方式用synchronized来解决的并发,其实这里性能并不好,可以直接借助lua脚本的原子性来实现这个可重入的功能。

13、Redis实现分布锁的时候,哪些问题需要考虑?

✅Redis实现分布锁的时候,哪些问题需要考虑?

1. 锁的基本要求

image.png

2. 误解锁问题

image.png

3. 锁的有效时间

image.png

4. 单点故障问题

image.png

5. 网络分区问题

image.png

14、Redisson解锁失败,watchdog会不会一直续期下去?

✅Redisson解锁失败,watchdog会不会一直续期下去?

image.png

15、Redis Cluster 中使用事务和 lua 有什么限制?

✅Redis Cluster 中使用事务和 lua 有什么限制?

image.png

16、Redisson 的 watchdog 什么情况下可能会失效?

✅Redisson 的 watchdog 什么情况下可能会失效?

image.png

17、Redisson如何保证解锁的线程一定是加锁的线程

✅Redisson如何保证解锁的线程一定是加锁的线程?

image.png

18、RDB和AOF的写回策略分别是什么?

✅RDB和AOF的写回策略分别是什么?

1. RDB的写回策略

image.png

2. AOF的写回策略

image.png

19、Redis的事务和MySQL的事务区别?

✅Redis的事务和MySQL的事务区别?

1. Redis

image.png

2. MySQL

image.png

20、Redis中hash结构比string的好处有哪些?

✅Redis中hash结构比string的好处有哪些?

image.png