某电商平台秒杀活动曾遭遇重大故障:并发量突破 10 万 / 秒时,分布式锁失效导致超卖 1200 件商品,直接损失超 50 万元。事后复盘发现,传统 Redis 分布式锁因未处理超时续期、锁释放异常等问题,导致锁机制失效。本文将以电商秒杀场景为核心,从分布式锁原理、主流实现方案、性能优化到一致性保障,提供一套可落地的高并发解决方案。

一、分布式锁核心:解决集群环境下的资源竞争

1. 为什么需要分布式锁?单机锁的局限性

在单体应用中,synchronizedReentrantLock可解决多线程资源竞争问题,但在分布式集群环境下:

  • JVM 隔离:不同节点的线程无法共享 JVM 锁,导致分布式事务冲突;
  • 数据一致性风险:多节点同时操作同一数据(如库存扣减),引发超卖、重复下单等问题;
  • 高可用挑战:单点锁服务故障会导致整个系统瘫痪。

电商秒杀场景的核心痛点:

  • 商品库存是有限资源,需保证全局唯一扣减;
  • 并发量极高(每秒数万请求),锁机制需高性能;
  • 业务逻辑需原子性(扣库存 + 创建订单 + 记录日志)。

2. 分布式锁的核心特性

一个可靠的分布式锁需满足以下特性:

  • 互斥性:同一时刻只能有一个客户端持有锁;
  • 可重入性:同一客户端可重复获取同一把锁;
  • 超时释放:防止客户端宕机导致锁永久占用;
  • 高可用:锁服务集群部署,避免单点故障;
  • 公平性:按请求顺序获取锁(可选);
  • 性能:加锁 / 解锁操作延迟低,支持高并发。

3. 主流分布式锁方案对比

方案 实现方式 优点 缺点 适用场景
Redis 分布式锁 基于 SETNX 命令 性能高、部署简单 需处理超时续期、主从同步延迟 高并发秒杀、缓存更新
ZooKeeper 分布式锁 基于临时有序节点 可靠性高、支持公平锁 性能较低、部署复杂 数据一致性要求高的场景
etcd 分布式锁 基于 CAS 机制 + Lease 高可用、支持 TTL 自动过期 生态不如 Redis 成熟 Kubernetes 生态下的服务
数据库分布式锁 基于行锁或乐观锁 无需额外组件 性能差、易造成数据库瓶颈 低并发场景

二、实战实现:三种主流分布式锁的代码落地

1. Redis 分布式锁:高性能秒杀场景首选

Redis 分布式锁基于SETNX(SET if Not Exists)命令,结合 Lua 脚本保证原子性。

1.1 基础实现(Redisson 客户端)

Redisson 是 Redis 官方推荐的 Java 客户端,内置分布式锁实现:

java

运行

@Service
public class RedisLockStockService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private OrderMapper orderMapper;

    // 秒杀下单(Redis分布式锁)
    public boolean seckill(Long productId, Long userId) {
        // 1. 构建锁对象(锁名:product_stock_1001)
        RLock lock = redissonClient.getLock("product_stock_" + productId);
        
        try {
            // 2. 尝试获取锁(最多等待3秒,持有锁10秒后自动释放)
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                return false; // 获取锁失败,秒杀失败
            }
            
            // 3. 业务逻辑(原子性操作)
            // 3.1 查库存
            Stock stock = stockMapper.selectByProductId(productId);
            if (stock == null || stock.getQuantity() <= 0) {
                return false; // 库存不足
            }
            
            // 3.2 扣库存
            int rows = stockMapper.deductStock(productId);
            if (rows == 0) {
                return false; // 扣减失败
            }
            
            // 3.3 创建订单
            Order order = new Order();
            order.setProductId(productId);
            order.setUserId(userId);
            order.setStatus(OrderStatus.CREATED);
            orderMapper.insert(order);
            
            return true; // 秒杀成功
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            // 4. 释放锁(确保只有持有锁的客户端释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
1.2 高级特性:超时续期(Watch Dog)

Redisson 自动实现锁超时续期:

java

运行

// 配置Redisson(application.yml)
redisson:
  singleServerConfig:
    address: redis://localhost:6379
    password: 123456
  lockWatchdogTimeout: 30000 # 看门狗超时时间(30秒),默认30秒

当业务逻辑执行时间超过锁超时时间时,Redisson 会自动续期(每 10 秒续期一次),避免锁提前释放。

1.3 集群模式:解决 Redis 单点故障

java

运行

// Redisson集群配置
Config config = new Config();
config.useClusterServers()
      .addNodeAddress("redis://redis-node1:6379")
      .addNodeAddress("redis://redis-node2:6379")
      .addNodeAddress("redis://redis-node3:6379")
      .setScanInterval(2000); // 集群节点扫描间隔
RedissonClient client = Redisson.create(config);

2. ZooKeeper 分布式锁:高可靠性场景

Curator 是 ZooKeeper 的 Java 客户端,提供分布式锁实现:

java

运行

@Service
public class ZkLockStockService {
    @Autowired
    private CuratorFramework curatorClient;
    @Autowired
    private StockMapper stockMapper;

    // 秒杀下单(ZooKeeper分布式锁)
    public boolean seckill(Long productId, Long userId) {
        // 1. 创建分布式锁(锁路径:/seckill/lock/product_1001)
        InterProcessMutex lock = new InterProcessMutex(
            curatorClient, "/seckill/lock/product_" + productId
        );
        
        try {
            // 2. 尝试获取锁(最多等待3秒)
            boolean locked = lock.acquire(3, TimeUnit.SECONDS);
            if (!locked) {
                return false;
            }
            
            // 3. 业务逻辑(同Redis锁)
            // ... 扣库存、创建订单
            
            return true;
        } catch (Exception e) {
            log.error("ZooKeeper锁异常", e);
            return false;
        } finally {
            // 4. 释放锁
            try {
                if (lock.isAcquiredInThisProcess()) {
                    lock.release();
                }
            } catch (Exception e) {
                log.error("释放ZooKeeper锁失败", e);
            }
        }
    }
}

ZooKeeper 锁的优势:

  • 基于临时节点,客户端宕机后自动释放锁;
  • 支持公平锁,按请求顺序获取锁;
  • 集群部署,可靠性高。

3. etcd 分布式锁:云原生场景首选

etcd 基于 Raft 协议,提供强一致性的分布式锁:

java

运行

@Service
public class EtcdLockStockService {
    @Autowired
    private EtcdClient etcdClient;
    @Autowired
    private StockMapper stockMapper;

    // 秒杀下单(etcd分布式锁)
    public boolean seckill(Long productId, Long userId) {
        // 1. 锁键名
        String lockKey = "/seckill/lock/product_" + productId;
        // 2. 客户端唯一标识(防止误解锁)
        String lockValue = UUID.randomUUID().toString();
        
        try {
            // 3. 尝试获取锁(TTL=10秒)
            boolean locked = etcdClient.tryLock(lockKey, lockValue, 10);
            if (!locked) {
                return false;
            }
            
            // 4. 业务逻辑
            // ... 扣库存、创建订单
            
            return true;
        } finally {
            // 5. 释放锁(通过CAS确保只有持有者释放)
            etcdClient.unlock(lockKey, lockValue);
        }
    }
}

// etcd客户端工具类
@Component
public class EtcdClient {
    private final Client client;

    public EtcdClient(@Value("${etcd.endpoints}") String endpoints) {
        this.client = Client.builder().endpoints(endpoints.split(",")).build();
    }

    // 尝试获取锁
    public boolean tryLock(String key, String value, long ttl) {
        try (LockClient lockClient = client.getLockClient()) {
            Lease lease = client.getLeaseClient().grant(ttl).get();
            LockResponse response = lockClient.lock(key.getBytes(), lease.getId()).get();
            return response.getSucceeded();
        } catch (Exception e) {
            return false;
        }
    }

    // 释放锁
    public void unlock(String key, String value) {
        try (LockClient lockClient = client.getLockClient()) {
            lockClient.unlock(key.getBytes()).get();
        } catch (Exception e) {
            log.error("释放etcd锁失败", e);
        }
    }
}

三、性能优化:秒杀场景的分布式锁调优

1. Redis 锁性能优化

1.1 批量扣减库存

将多次扣减合并为一次操作,减少锁持有时间:

java

运行

// 批量处理秒杀请求
public List<Boolean> batchSeckill(Long productId, List<Long> userIds) {
    RLock lock = redissonClient.getLock("product_stock_" + productId);
    try {
        boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
        if (!locked) {
            return userIds.stream().map(u -> false).collect(Collectors.toList());
        }
        
        // 批量扣库存(一次SQL操作)
        int totalCount = userIds.size();
        int rows = stockMapper.batchDeductStock(productId, totalCount);
        if (rows == 0) {
            return userIds.stream().map(u -> false).collect(Collectors.toList());
        }
        
        // 批量创建订单
        List<Order> orders = userIds.stream().map(userId -> {
            Order order = new Order();
            order.setProductId(productId);
            order.setUserId(userId);
            return order;
        }).collect(Collectors.toList());
        orderMapper.batchInsert(orders);
        
        return userIds.stream().map(u -> true).collect(Collectors.toList());
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return userIds.stream().map(u -> false).collect(Collectors.toList());
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}
1.2 缓存预热 + 限流
  • 缓存预热:秒杀开始前将商品库存加载到 Redis 缓存;
  • 令牌桶限流:控制每秒请求数,避免 Redis 过载:

java

运行

@Autowired
private RateLimiter rateLimiter; // Guava RateLimiter

public boolean seckillWithRateLimit(Long productId, Long userId) {
    // 限流(每秒1000请求)
    if (!rateLimiter.tryAcquire()) {
        return false;
    }
    
    // 执行秒杀逻辑
    return seckill(productId, userId);
}
1.3 主从分离 + 读写分离
  • 主库:处理写操作(扣库存);
  • 从库:处理读操作(查库存);
  • Redis Cluster:分片存储,提高并发能力。

2. 锁粒度优化:从粗粒度到细粒度

2.1 商品维度锁→用户维度锁

java

运行

// 粗粒度锁(商品维度):所有用户竞争同一把锁
RLock coarseLock = redissonClient.getLock("product_stock_" + productId);

// 细粒度锁(用户维度):每个用户竞争自己的锁
RLock fineLock = redissonClient.getLock("user_seckill_" + userId + "_" + productId);
2.2 分段锁:将库存分段,减少锁竞争

java

运行

// 库存分段(如商品ID%10=0~9)
int segment = (int) (productId % 10);
RLock segmentLock = redissonClient.getLock("product_stock_segment_" + segment);

3. 无锁方案:乐观锁 + 消息队列

对于超高并发场景,可完全避免分布式锁:

java

运行

// 乐观锁扣库存(无锁方案)
public boolean seckillWithOptimisticLock(Long productId, Long userId) {
    int retryCount = 3;
    while (retryCount-- > 0) {
        // 1. 查库存(带版本号)
        Stock stock = stockMapper.selectByProductId(productId);
        if (stock == null || stock.getQuantity() <= 0) {
            return false;
        }
        
        // 2. 乐观锁扣减(CAS操作)
        int rows = stockMapper.deductStockWithVersion(
            productId, 1, stock.getVersion()
        );
        if (rows > 0) {
            // 3. 发送订单创建消息到MQ(异步处理)
            mqTemplate.send("seckill_order_topic", new SeckillOrderMsg(productId, userId));
            return true;
        }
        
        // 4. 冲突重试
        LockSupport.parkNanos(100_000); // 100微秒
    }
    return false;
}

四、一致性保障:分布式事务解决方案

1. 分布式事务问题

秒杀场景中,扣库存、创建订单、记录日志需保证原子性,传统分布式锁无法解决跨服务事务问题。

2. 解决方案 1:TCC(Try-Confirm-Cancel)

java

运行

// TCC接口定义
public interface StockTccService {
    // Try:预扣库存
    boolean tryDeductStock(Long productId, Long userId);
    
    // Confirm:确认扣库存
    boolean confirmDeductStock(Long productId, Long userId);
    
    // Cancel:取消扣库存
    boolean cancelDeductStock(Long productId, Long userId);
}

// TCC实现
@Service
public class StockTccServiceImpl implements StockTccService {
    @Autowired
    private StockMapper stockMapper;

    @Override
    @Transactional
    public boolean tryDeductStock(Long productId, Long userId) {
        // 预扣库存(冻结库存)
        return stockMapper.freezeStock(productId, userId) > 0;
    }

    @Override
    @Transactional
    public boolean confirmDeductStock(Long productId, Long userId) {
        // 确认扣库存(扣除冻结库存)
        return stockMapper.confirmFreezeStock(productId, userId) > 0;
    }

    @Override
    @Transactional
    public boolean cancelDeductStock(Long productId, Long userId) {
        // 取消扣库存(解冻库存)
        return stockMapper.unfreezeStock(productId, userId) > 0;
    }
}

3. 解决方案 2:SAGA 模式

基于事件驱动的长事务解决方案:

java

运行

// SAGA状态机配置
@Configuration
public class SeckillSagaConfig {
    @Bean
    public StateMachineFactory<SeckillStates, SeckillEvents> stateMachineFactory() {
        StateMachineBuilder.Builder<SeckillStates, SeckillEvents> builder = StateMachineBuilder.builder();
        
        builder.configureStates()
            .withStates()
            .initial(SeckillStates.INIT)
            .state(SeckillStates.STOCK_DEDUCTED)
            .state(SeckillStates.ORDER_CREATED)
            .end(SeckillStates.SUCCESS)
            .end(SeckillStates.FAIL);
        
        builder.configureTransitions()
            .withExternal()
                .source(SeckillStates.INIT).target(SeckillStates.STOCK_DEDUCTED)
                .event(SeckillEvents.DEDUCT_STOCK)
                .action(deductStockAction(), errorHandler())
            .and()
            .withExternal()
                .source(SeckillStates.STOCK_DEDUCTED).target(SeckillStates.ORDER_CREATED)
                .event(SeckillEvents.CREATE_ORDER)
                .action(createOrderAction(), errorHandler())
            .and()
            .withExternal()
                .source(SeckillStates.ORDER_CREATED).target(SeckillStates.SUCCESS)
                .event(SeckillEvents.SUCCESS)
            .and()
            .withExternal()
                .source(SeckillStates.STOCK_DEDUCTED).target(SeckillStates.FAIL)
                .event(SeckillEvents.CANCEL)
                .action(cancelStockAction());
        
        return builder.build();
    }
}

4. 解决方案 3:最终一致性(本地消息表 + MQ)

java

运行

// 本地消息表记录
@Service
public class SeckillMessageService {
    @Autowired
    private SeckillMessageMapper messageMapper;
    @Autowired
    private MQTemplate mqTemplate;

    // 1. 本地事务:扣库存+记录消息
    @Transactional
    public boolean seckillWithLocalMessage(Long productId, Long userId) {
        // 扣库存
        int rows = stockMapper.deductStock(productId);
        if (rows == 0) {
            return false;
        }
        
        // 记录本地消息
        SeckillMessage message = new SeckillMessage();
        message.setProductId(productId);
        message.setUserId(userId);
        message.setStatus(MessageStatus.PENDING);
        messageMapper.insert(message);
        
        return true;
    }

    // 2. 定时任务:发送消息到MQ
    @Scheduled(fixedRate = 5000)
    public void sendPendingMessages() {
        List<SeckillMessage> messages = messageMapper.selectByStatus(MessageStatus.PENDING);
        for (SeckillMessage message : messages) {
            try {
                mqTemplate.send("seckill_order_topic", message);
                // 更新消息状态为已发送
                messageMapper.updateStatus(message.getId(), MessageStatus.SENT);
            } catch (Exception e) {
                log.error("发送消息失败", e);
            }
        }
    }

    // 3. 消费MQ消息:创建订单
    @RabbitListener(queues = "seckill_order_queue")
    public void handleOrderMessage(SeckillMessage message) {
        // 创建订单
        Order order = new Order();
        order.setProductId(message.getProductId());
        order.setUserId(message.getUserId());
        orderMapper.insert(order);
        
        // 更新消息状态为已完成
        messageMapper.updateStatus(message.getId(), MessageStatus.COMPLETED);
    }
}

五、生产环境避坑指南

1. Redis 锁常见问题

1.1 锁超时导致业务未完成

解决方案:使用 Redisson 的 Watch Dog 自动续期,或预估业务执行时间设置合理超时。

1.2 主从切换导致锁丢失

解决方案:使用 Redis RedLock 算法,同时获取多个 Redis 节点的锁:

java

运行

// RedLock实现
public boolean redLockSeckill(Long productId, Long userId) {
    RLock lock1 = redissonClient1.getLock("product_stock_" + productId);
    RLock lock2 = redissonClient2.getLock("product_stock_" + productId);
    RLock lock3 = redissonClient3.getLock("product_stock_" + productId);
    
    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
    try {
        boolean locked = redLock.tryLock(3, 10, TimeUnit.SECONDS);
        if (!locked) {
            return false;
        }
        
        // 业务逻辑
        // ...
        
        return true;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    } finally {
        redLock.unlock();
    }
}
1.3 误解锁其他客户端的锁

解决方案:锁值设置为客户端唯一标识(如 UUID),释放时校验:

lua

-- Lua脚本:原子性释放锁
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

2. 性能监控与告警

  • Redis 监控:监控SETNX命令执行次数、锁等待时间;
  • 数据库监控:监控库存表的行锁等待次数;
  • 业务监控:监控秒杀成功率、超卖数量;
  • 告警设置:锁等待时间超过 100ms 触发告警。

3. 降级与熔断

秒杀系统需设置降级策略:

  • 库存不足时降级:直接返回 “已售罄”;
  • Redis 故障时降级:使用本地缓存或数据库锁;
  • 并发过高时降级:触发限流,返回 “请稍后重试”。

六、总结:分布式锁选型与实践原则

分布式锁的选型需结合业务场景:

  • 高并发秒杀:首选 Redis 分布式锁(Redisson),结合乐观锁 + MQ 提升性能;
  • 数据一致性要求高:选择 ZooKeeper 或 etcd 分布式锁;
  • 云原生环境:优先使用 etcd(Kubernetes 原生支持);
  • 低并发场景:可使用数据库乐观锁,降低部署复杂度。

实践原则:

  1. 最小锁粒度:尽量缩小锁的范围,避免粗粒度锁;
  2. 最短锁持有时间:业务逻辑尽量精简,减少锁占用时间;
  3. 高可用设计:锁服务集群部署,避免单点故障;
  4. 监控与告警:实时监控锁性能,及时发现问题;
  5. 降级预案:制定锁服务故障时的降级策略。

在电商秒杀等高并发场景中,分布式锁是保障数据一致性的核心技术,但需结合业务特点选择合适的方案,并通过性能优化、监控告警、降级预案等手段,构建高可用、高性能的分布式系统。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐