1. 微服务架构下的并发挑战
在分布式系统架构中,微服务已经成为现代应用开发的主流模式。与传统的单体架构不同,微服务将应用拆分为多个独立部署的服务单元,每个服务运行在自己的进程中,通过轻量级通信机制进行交互。这种架构带来了部署灵活性和可扩展性的提升,但也引入了新的并发控制难题。
单体应用中常用的synchronized关键字,在单JVM环境下确实能有效解决多线程并发问题。它通过获取对象监视器锁(monitor)来实现同步,确保同一时刻只有一个线程可以执行临界区代码。但在微服务集群环境下,多个服务实例可能分布在不同的物理节点上,每个节点都有自己的JVM实例和内存空间。此时synchronized只能保证单个JVM内的线程安全,无法跨JVM实现全局互斥。
2. synchronized的局限性分析
2.1 JVM级别的锁机制
synchronized的实现依赖于JVM内部的对象头Mark Word和监视器锁机制。当线程进入synchronized代码块时,会尝试获取对象的监视器锁。如果获取成功,线程可以执行临界区代码;如果锁已被其他线程持有,当前线程会被阻塞并放入该对象的等待队列中。
这种机制在单JVM环境下工作良好,因为所有线程共享同一个内存空间,可以实时感知锁状态的变化。但在分布式环境中,不同JVM之间的内存空间是隔离的,一个JVM中的锁状态变化无法自动同步到其他JVM。
2.2 典型分布式并发场景
假设我们有一个库存扣减服务部署在三个节点上,使用synchronized实现扣减逻辑:
public synchronized void deductStock(String productId, int quantity) { // 查询库存 int stock = stockDao.getStock(productId); if (stock >= quantity) { stockDao.updateStock(productId, stock - quantity); } else { throw new RuntimeException("库存不足"); } }在并发请求下,即使每个节点内部的synchronized能保证线程安全,三个节点之间仍然可能出现超卖问题。这是因为三个JVM各自维护独立的锁状态,无法形成全局互斥。
3. 分布式锁解决方案
3.1 基于Redis的分布式锁
Redis因其高性能和原子操作特性,成为实现分布式锁的常用选择。核心是利用SETNX(SET if Not eXists)命令实现互斥:
public boolean tryLock(String lockKey, String requestId, int expireTime) { return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS); } public boolean unlock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; return redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(lockKey), requestId) == 1; }关键点:
- 必须设置过期时间,避免死锁
- 解锁时要验证请求ID,防止误删其他线程的锁
- 建议使用Redisson等成熟框架,处理锁续期等问题
3.2 基于ZooKeeper的分布式锁
ZooKeeper通过临时顺序节点实现分布式锁:
public void lock() throws Exception { String path = zkClient.createEphemeralSequential(LOCK_PATH + "/lock-", null); List<String> children = zkClient.getChildren(LOCK_PATH); Collections.sort(children); if (path.endsWith(children.get(0))) { return; // 获取锁成功 } else { String watchNode = LOCK_PATH + "/" + children.get(0); CountDownLatch latch = new CountDownLatch(1); zkClient.subscribeDataChanges(watchNode, new IZkDataListener() { @Override public void handleDataChange(String dataPath, Object data) {} @Override public void handleDataDeleted(String dataPath) { latch.countDown(); } }); latch.await(); } }ZooKeeper锁的优势在于通过Watcher机制能实时感知锁释放,避免轮询;缺点是性能不如Redis,且需要维护ZooKeeper集群。
4. 分布式事务方案
对于需要强一致性的场景,单纯的分布式锁可能不够,需要考虑分布式事务:
4.1 TCC模式
Try-Confirm-Cancel模式将事务分为两个阶段:
- Try:预留资源
- Confirm/Cancel:确认或取消操作
// 库存服务 public boolean tryDeduct(String productId, int quantity) { // 冻结库存 return stockDao.freezeStock(productId, quantity) > 0; } public boolean confirmDeduct(String productId, int quantity) { // 扣减冻结的库存 return stockDao.reduceFreezeStock(productId, quantity) > 0; } public boolean cancelDeduct(String productId, int quantity) { // 释放冻结的库存 return stockDao.unfreezeStock(productId, quantity) > 0; }4.2 Saga模式
将长事务拆分为多个本地事务,每个事务有对应的补偿操作:
// 订单服务 @SagaStart public void createOrder(Order order) { // 1. 创建订单 orderDao.create(order); // 2. 扣减库存 inventoryService.deduct(order.getProductId(), order.getQuantity()); // 3. 扣减余额 accountService.debit(order.getUserId(), order.getAmount()); } // 补偿操作 public void cancelOrder(Order order) { // 1. 恢复库存 inventoryService.restore(order.getProductId(), order.getQuantity()); // 2. 恢复余额 accountService.credit(order.getUserId(), order.getAmount()); // 3. 取消订单 orderDao.cancel(order.getId()); }5. 性能优化实践
5.1 锁粒度控制
避免大范围的锁竞争,尽量减小锁的粒度:
// 不推荐 - 锁整个方法 public synchronized void updateUser(User user) { // 更新用户所有信息 } // 推荐 - 按用户ID加锁 private final ConcurrentHashMap<Long, Object> userLocks = new ConcurrentHashMap<>(); public void updateUser(User user) { Object lock = userLocks.computeIfAbsent(user.getId(), k -> new Object()); synchronized (lock) { // 更新用户信息 } }5.2 读写锁分离
对于读多写少的场景,使用ReadWriteLock提高并发性:
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); public Data getData(String key) { rwLock.readLock().lock(); try { return cache.get(key); } finally { rwLock.readLock().unlock(); } } public void updateData(String key, Data data) { rwLock.writeLock().lock(); try { cache.put(key, data); db.update(data); } finally { rwLock.writeLock().unlock(); } }6. 常见问题排查
6.1 死锁问题
分布式环境下死锁更难排查,常见原因包括:
- 锁未设置超时时间
- 业务处理时间超过锁过期时间
- 网络分区导致锁无法释放
解决方案:
- 为所有锁设置合理的超时时间
- 实现锁续期机制(如Redisson的Watch Dog)
- 添加锁持有者标识,便于排查
6.2 脑裂问题
当集群出现网络分区时,可能导致多个客户端同时持有锁。应对策略:
- 使用RedLock等算法,要求多数节点确认
- 设置fencing token(防护令牌)机制
- 监控网络健康状况,及时报警
7. 监控与治理
完善的监控体系对分布式系统至关重要:
7.1 关键指标监控
- 锁等待时间
- 锁持有时间
- 锁获取失败率
- 死锁发生次数
7.2 治理策略
- 熔断机制:当锁失败率超过阈值时熔断
- 降级方案:如本地缓存替代分布式锁
- 自动扩容:根据锁竞争情况动态调整资源
在实际项目中,我曾遇到一个典型的超卖案例:促销活动期间,虽然使用了Redis分布式锁,但由于未设置合理的锁超时时间,导致部分请求长时间等待,最终引发系统雪崩。后来我们通过以下措施解决了问题:
- 根据业务特点调整锁超时时间(从默认30秒改为3秒)
- 实现阶梯式回退策略,避免集中重试
- 引入本地缓存减少对共享资源的竞争