互联网大厂Java求职面试:高并发支付系统的幂等性设计

互联网大厂Java求职面试:高并发支付系统的幂等性设计

在一次充满挑战的面试中,技术总监级别的面试官遇到了一位有趣的候选人——郑薪苦。他虽然回答问题时常常东拉西扯,但偶尔也能歪打正着,说出一些关键点。这次,我们将重点关注高并发支付系统的幂等性设计。

第一轮提问:分布式事务在促销活动中的实现方案

面试官:假设我们正在进行双十一大促,系统需要处理大量的订单交易,请问你如何确保这些分布式事务的一致性?

郑薪苦:哦,这就像一场大型舞会,每个人都需要找到自己的舞伴。我们可以使用两阶段提交(2PC)来协调各个节点,确保事务要么全部成功,要么全部失败。

面试官:不错,那你能详细解释一下2PC的过程吗?

郑薪苦:当然可以。首先,事务协调者会向所有参与者发送准备请求,如果大家都同意,就会进入提交阶段,否则回滚。

面试官:很好,那在实际操作中,2PC有哪些常见问题?

郑薪苦:哈哈,这就像是舞会上有人突然摔倒了。主要问题是性能瓶颈和单点故障。所以,我们通常会考虑使用消息队列或者基于最终一致性的方案,比如TCC(Try-Confirm-Cancel)。

第二轮提问:千万级商品库存的实时更新与一致性保障

面试官:双十一期间,我们的商品库存可能达到千万级别,如何保证实时更新和一致性呢?

郑薪苦:嗯,这就像是一个巨大的糖果罐,每次拿糖果都要确保数量正确。我们可以使用Redis作为缓存层,并采用分布式锁来控制并发访问。

面试官:具体怎么实现分布式锁呢?

郑薪苦:可以通过Redis的SETNX命令来实现。当一个线程获取锁时,设置一个唯一的标识符,其他线程尝试获取锁时会失败,直到锁被释放。

面试官:那如果Redis挂了怎么办?

郑薪苦:啊,这就像是糖果罐被打翻了。我们可以引入Redlock算法,通过多个独立的Redis实例提高可靠性。

第三轮提问:高并发支付系统的幂等性设计与异常兜底

面试官:最后一个问题,关于高并发支付系统,如何设计幂等性以防止重复扣款?

郑薪苦:哦,这就像是给每笔交易贴上独一无二的标签。我们可以在数据库中为每笔交易生成唯一ID,并在每次请求时检查这个ID是否存在。

面试官:那具体的实现细节呢?

郑薪苦:可以用MySQL的唯一索引,或者使用Redis的SETNX命令。此外,还可以结合消息队列,将支付请求放入队列中,消费者负责处理并记录状态。

面试官:非常棒!那你对异常情况的处理有什么建议?

郑薪苦:这就像是提前准备好急救包。我们可以建立完善的监控和报警机制,及时发现并处理异常情况。同时,设计补偿机制,如定时任务扫描未完成的支付记录并进行补救。

面试官:今天的面试就到这里,感谢你的精彩回答,请回去等通知吧。

标准答案

分布式事务

  1. 两阶段提交(2PC):分为准备和提交两个阶段,适用于强一致性的场景。核心源码涉及事务管理器、资源管理器和事务日志。

    public class TransactionManager {
        public void prepare() {
            // 发送准备请求
        }
    
        public void commit() {
            // 提交事务
        }
    
        public void rollback() {
            // 回滚事务
        }
    }
    
  2. 常见问题:性能瓶颈和单点故障。可采用TCC模式,即Try(预留资源)、Confirm(确认操作)、Cancel(取消操作)。

  3. 应用场景:电商平台的大促活动、金融系统的转账操作。

商品库存的一致性保障

  1. Redis分布式锁:利用SETNX命令实现。

    public boolean acquireLock(String key, String value, long expireTime) {
        Jedis jedis = new Jedis("localhost");
        String result = jedis.set(key, value, "NX", "EX", expireTime);
        return "OK".equals(result);
    }
    
  2. Redlock算法:通过多个Redis实例提高可靠性。

  3. 优化方向:引入本地缓存、分片存储等技术。

幂等性设计

  1. 唯一ID生成:使用UUID或雪花算法。

    public class IdGenerator {
        private static final long twepoch = 1288834974657L;
        private static final long workerIdBits = 5L;
        private static final long datacenterIdBits = 5L;
        private static final long maxWorkerId = -1L ^ (-1L << workerIdBits);
        private static final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        private static final long sequenceBits = 12L;
        private static final long workerIdShift = sequenceBits;
        private static final long datacenterIdShift = sequenceBits + workerIdBits;
        private static final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private static final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private long workerId;
        private long datacenterId;
        private long sequence = 0L;
        private long lastTimestamp = -1L;
    
        public IdGenerator(long workerId, long datacenterId) {
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }
    
        public synchronized long nextId() {
            long timestamp = timeGen();
            if (timestamp < lastTimestamp) {
                throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }
            lastTimestamp = timestamp;
            return ((timestamp - twepoch) << timestampLeftShift) |
                   (datacenterId << datacenterIdShift) |
                   (workerId << workerIdShift) |
                   sequence;
        }
    
        protected long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        protected long timeGen() {
            return System.currentTimeMillis();
        }
    }
    
  2. 消息队列:将支付请求放入队列中,确保每笔交易只处理一次。

  3. 异常兜底:建立完善的监控和补偿机制。

总结

郑薪苦的幽默金句贯穿整个面试,让我们在严肃的技术讨论中增添了几分乐趣。希望这篇文章能帮助你在高并发支付系统的幂等性设计中游刃有余,无论是应对大厂面试还是实际工作中的技术挑战。

你可能感兴趣的:(Java场景面试宝典,Java,高并发,支付系统,幂等性,分布式事务,Redis,消息队列)