找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1464

积分

0

好友

216

主题
发表于 5 小时前 | 查看: 1| 回复: 0

在任何一个系统上都有一些共享资源需要互斥访问,云上系统更是如此。比如库存、订票、余额支付等等,都需要保证数据一致性、避免重复处理、防止资源冲突。在传统的单体应用中,进程间的互斥访问可以通过操作系统提供的信号量、互斥锁或共享内存等机制实现。这些机制的核心是找到一个全局可见的锚定物,如内存地址或文件描述符,作为协调的基点。而云原生架构下,微服务实例分散在不同设备、甚至不同集群,早已脱离单机边界,此时需要找到云系统内全局统一的锚定物,才能协调多个实例的访问顺序,避免资源冲突。

下面将从「多实例需互斥访问场景 -->多实例互斥访问的核心需求,如互斥性、防死锁等 --> 主流实现方案和范例」来解析多实例微服务如何实现共享资源的互斥访问。

图片

一、多实例需互斥访问的典型场景

云原生微服务中,以下场景必须通过跨实例互斥保证业务正确性,也是分布式锁的核心应用场景:

  1. 库存与订单类:秒杀活动中多个实例同时扣减同一商品库存,需避免超卖;订单创建时防止重复下单(如用户快速点击提交按钮触发多请求)。
  2. 支付与账务类:用户支付时,多个实例同时发起余额扣减,需保证金额一致性;退款流程中避免重复退款。
  3. 资源抢占类:分布式任务调度(如定时对账、数据同步),多个实例竞争同一任务,需确保仅一个实例执行;云资源分配(如虚拟机创建、端口占用)避免资源冲突。
  4. 数据更新类:多实例同时修改同一用户的配置信息、状态数据(如会员等级变更),需保证更新操作的原子性,避免数据覆盖。

二、多实例互斥访问的核心需求

由于云原生微服务架构中,各个微服务的部署运行跟传统的单体应用有了根本性变化:

  • 无共享架构:服务实例通常部署在不同的主机、容器甚至不同的可用区
  • 动态弹性:实例数量会随负载自动伸缩,实例标识动态变化
  • 网络分区:网络延迟、丢包等成为常态而非异常
  • 故障常态:实例可能随时故障重启

所以对于设计分布式锁,也有其明确的核心诉求:

  • 互斥性:同一时刻仅一个实例持有锁;
  • 防死锁:异常场景(服务宕机、网络中断)下锁能自动释放;
  • 高可用:锁服务(中间件)需集群部署,避免单点故障;
  • 原子性:锁的 “获取” 和 “释放” 操作需原子执行,无中间状态;
  • 重入性(可选):同一线程可多次获取同一把锁(避免自死锁);
  • 公平性(可选):按请求顺序分配锁(平衡性能与公平性)。

三、主流实现方案(基于云原生全局锚定物)

如引言所述,多实例互斥的核心是 “选对全局锚定物”。以下方案按 “云原生场景适配性 + 易用性” 排序,每个方案均明确其 “全局锚定物”、原理、落地范例及适用场景:

方案 1:Redis 分布式锁(推荐首选)

核心锚定物:Redis 集群的键值对(通过原子命令和过期机制实现锁逻辑)

Redis 是云原生环境中最常用的 “全局锚定物”—— 部署简单、高性能(单机万级 QPS)、支持集群扩缩容,完美适配高并发场景,且通过 Redisson 等组件封装了成熟的锁逻辑,无需手动处理复杂细节。

图片

实现原理
  • 锁获取:用 SET resource_key lock_value NX EX expire_time 原子命令(NX = 键不存在时才设置,EX = 指定过期时间),lock_value 需唯一(如 UUID + 实例 ID + 线程 ID),确保仅一个实例能成功设置键(即获取锁)。
  • 锁释放:用 Lua 脚本原子执行 “验证锁持有者 + 删除键”,避免单独执行 GET 和 DEL 导致的误释放(如 A 实例获取锁后超时未释放,B 实例获取锁,A 实例后续误删 B 的锁)。
  • 云原生优化:支持 Redis 集群 / 主从 + 哨兵部署(高可用),Redisson 的 “看门狗(WatchDog)” 机制可自动续期锁超时时间(适配云环境业务执行时长不确定的场景)。
落地范例(Java + Redisson,生产级)

Redisson 已封装可重入锁、公平锁、超时续期等特性,无需手动编写 Lua 脚本,适配 K8s 容器化部署:

<!-- 依赖引入(支持Spring Boot、原生Java) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class StockService {
    // 共享资源标识:商品ID(锁粒度最小化,避免全局锁)
    private static final String LOCK_KEY_PREFIX = "lock:stock:";
    @Resource
    private RedissonClient redissonClient;
    // 扣减库存(核心业务,需互斥访问)
    public boolean deductStock(Long productId, Integer quantity) {
        String lockKey = LOCK_KEY_PREFIX + productId;
        // 获取可重入锁(全局锚定物:Redis中的lock:stock:xxx键)
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁:最多等待10秒(避免长期阻塞),持有30秒(默认看门狗自动续期)
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            // 互斥执行的业务逻辑:查询库存→验证→扣减
            int currentStock = queryStockFromDB(productId); // 从数据库查询当前库存
            if (currentStock < quantity) {
                throw new RuntimeException("库存不足");
            }
            updateStockToDB(productId, currentStock - quantity); // 扣减库存
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("扣减库存失败");
        } finally {
            // 仅当前实例持有锁时才释放(避免误释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    // 模拟数据库查询库存
    private int queryStockFromDB(Long productId) {
        // 实际场景中从MySQL/PostgreSQL等数据库查询
        return 100;
    }
    // 模拟数据库更新库存
    private void updateStockToDB(Long productId, int newStock) {
        // 实际场景中执行UPDATE语句更新库存
        System.out.println("商品" + productId + "库存更新为:" + newStock);
    }
}
优缺点
  • 优点:高性能、云原生适配性强(支持容器化、集群扩缩容)、生态完善、易用性高;
  • 缺点:弱一致性(Redis 主从切换时可能丢失锁,需 Redlock 算法补偿)、依赖 Redis 集群可用性;
  • 适用场景:云原生高并发场景(秒杀、库存扣减、订单创建)、对一致性要求中等的业务。

方案 2:ZooKeeper 分布式锁

核心锚定物:ZooKeeper 集群的临时有序节点(通过节点层级和 Watcher 机制实现锁逻辑)

ZooKeeper 是强一致性的分布式协调服务,其 “临时有序节点” 天然适合作为云原生环境的 “全局锚定物”—— 节点创建 / 删除具有原子性,且临时节点会随客户端会话断开自动删除(防死锁),适合对一致性要求极高的场景。

图片

实现原理
  • 锁初始化:创建持久节点 /locks(全局锁根目录);
  • 锁获取:客户端在 /locks 下创建临时有序子节点(如 /locks/stock_1001-xxx-000000001),节点名含有序序号;
  • 锁竞争:客户端获取 /locks 下所有子节点,若自己是序号最小的节点,则获取锁成功;否则监听前一个节点的删除事件(Watcher 机制);
  • 锁释放:正常业务执行完后删除自身节点,或客户端宕机导致会话超时(临时节点自动删除),触发下一个节点的 Watcher 通知,实现锁传递。

图片

落地范例(Java + Curator,生产级)

Curator 是 ZooKeeper 的 Java 客户端,封装了分布式锁逻辑,避免手动处理节点和 Watcher:

<!-- 依赖引入 -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.5.0</version>
</dependency>
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;
@Service
public class PaymentService {
    // 全局锚定物路径:ZooKeeper的/locks/payment节点下的临时有序子节点
    private static final String LOCK_PATH = "/locks/payment";
    private CuratorFramework curatorClient;
    private InterProcessMutex distributedLock;
    // 初始化ZooKeeper客户端(集群地址适配云环境部署)
    @PostConstruct
    public void initCuratorClient() {
        curatorClient = CuratorFrameworkFactory.builder()
                .connectString("zk-node1:2181,zk-node2:2181,zk-node3:2181") // 云环境ZooKeeper集群地址
                .retryPolicy(new ExponentialBackoffRetry(1000, 3)) // 重试策略(适配云网络抖动)
                .sessionTimeoutMs(6000) // 会话超时(临时节点自动删除的触发条件)
                .connectionTimeoutMs(3000)
                .build();
        curatorClient.start();
        // 创建可重入分布式锁
        distributedLock = new InterProcessMutex(curatorClient, LOCK_PATH);
    }
    // 余额支付(需互斥访问,避免双重扣款)
    public boolean pay(Long userId, BigDecimal amount) {
        try {
            // 尝试获取锁:最多等待15秒,超时失败
            boolean isLocked = distributedLock.acquire(15, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("支付繁忙,请稍后重试");
            }
            // 互斥业务逻辑:查询余额→验证→扣减
            BigDecimal currentBalance = queryBalanceFromDB(userId);
            if (currentBalance.compareTo(amount) < 0) {
                throw new RuntimeException("余额不足");
            }
            updateBalanceToDB(userId, currentBalance.subtract(amount));
            return true;
        } catch (Exception e) {
            throw new RuntimeException("支付失败:" + e.getMessage());
        } finally {
            // 释放锁(仅当前实例持有锁时执行)
            if (distributedLock.isAcquiredInThisProcess()) {
                try {
                    distributedLock.release();
                } catch (Exception e) {
                    throw new RuntimeException("释放锁失败");
                }
            }
        }
    }
    // 模拟查询用户余额
    private BigDecimal queryBalanceFromDB(Long userId) {
        return new BigDecimal("1000");
    }
    // 模拟更新用户余额
    private void updateBalanceToDB(Long userId, BigDecimal newBalance) {
        System.out.println("用户" + userId + "余额更新为:" + newBalance);
    }
    // 销毁客户端
    @PreDestroy
    public void closeCuratorClient() {
        if (curatorClient != null) {
            curatorClient.close();
        }
    }
}
优缺点
  • 优点:强一致性(ZooKeeper 集群基于 Paxos 协议)、自动释放锁(临时节点)、天然支持重入、无锁超时风险;
  • 缺点:性能中等(QPS 千级)、云环境部署维护复杂(需 3 + 节点集群)、Watcher 通知可能有延迟;
  • 适用场景:云原生高一致性场景(分布式事务、主从切换选举、支付对账)、中低并发业务。

方案 3:数据库分布式锁

核心锚定物:数据库的表行 / 版本号(通过事务和锁机制实现互斥)

无需额外部署中间件,直接利用现有数据库/中间件作为 “全局锚定物”,适合小型云原生系统或已有数据库集群的场景,实现成本最低。

图片

两种实现方式
方式 A:悲观锁(SELECT FOR UPDATE)
  • 锚定物:数据库表中的特定行(如分布式锁表的 resource 字段);
  • 原理:通过SELECT ... FOR UPDATE语句锁定目标行,事务提交 / 回滚后释放锁,利用数据库行级锁实现互斥;
  • 前提:数据库隔离级别需为 REPEATABLE READ 及以上(云数据库默认满足)。
方式 B:乐观锁(版本号 / 时间戳)
  • 锚定物:数据行的版本号字段(如 stock 表的 version 字段);
  • 原理:无锁机制,更新时通过版本号匹配判断是否有并发冲突(UPDATE ... WHERE version = ?),匹配则更新成功(获取锁),否则重试。
落地范例(乐观锁,MySQL + MyBatis)

适合低并发库存更新场景,无阻塞、适配云数据库(如阿里云 RDS、AWS RDS):

-- 库存表结构(含版本号字段,作为全局锚定物)
CREATE TABLE `stock` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `count` int NOT NULL COMMENT '库存数量',
  `version` int NOT NULL DEFAULT '0' COMMENT '版本号(乐观锁锚定物)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface StockMapper {
    // 查询商品库存和版本号
    StockDO selectStockByProductId(@Param("productId") Long productId);
    // 乐观锁更新库存:仅版本号匹配时更新(原子操作)
    int updateStockWithVersion(@Param("productId") Long productId,
                               @Param("quantity") Integer quantity,
                               @Param("oldVersion") Integer oldVersion);
}
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
public class StockOptimisticLockService {
    @Resource
    private StockMapper stockMapper;
    // 扣减库存(乐观锁实现)
    public boolean deductStock(Long productId, Integer quantity) {
        int retryCount = 3; // 重试次数(避免高并发下频繁失败)
        while (retryCount > 0) {
            // 1. 查询当前库存和版本号(获取锚定物状态)
            StockDO stock = stockMapper.selectStockByProductId(productId);
            if (stock == null || stock.getCount() < quantity) {
                throw new RuntimeException("库存不足");
            }
            // 2. 乐观锁更新(原子操作,验证锚定物版本号)
            int affectedRows = stockMapper.updateStockWithVersion(
                    productId, quantity, stock.getVersion()
            );
            // 3. 判断是否更新成功(无并发冲突则成功)
            if (affectedRows > 0) {
                System.out.println("商品" + productId + "库存扣减成功,剩余:" + (stock.getCount() - quantity));
                return true;
            }
            // 4. 并发冲突,重试(间隔100ms避免数据库压力)
            retryCount--;
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        throw new RuntimeException("系统繁忙,请稍后重试");
    }
}
优缺点
  • 优点:无需额外部署中间件、实现简单、适配云数据库(RDS)、成本低;
  • 缺点:性能有限(悲观锁阻塞,乐观锁高并发下重试频繁)、数据库压力大(需集群部署避免单点);
  • 适用场景:小型云原生系统、低并发场景(后台管理系统、低频数据更新)、对性能要求不高的业务。

方案 4:etcd 分布式锁

核心锚定物:etcd 集群的租约临时键(基于 Raft 协议,适配 K8s 云原生环境)

etcd 是 Kubernetes 生态的核心组件,天然适配云原生部署,强一致性(Raft 协议)、高性能(万级 QPS),其 “租约临时键” 作为全局锚定物,既保证自动释放锁,又支持动态扩缩容。

图片

实现原理
  • 锁获取:在 etcd 的 /locks/resource_key 目录下创建租约临时键(键名含随机数保证唯一性),通过原子 Compare 操作判断是否为第一个创建的键;
  • 锁竞争:若不是第一个键,监听前一个键的删除事件;
  • 锁释放:租约过期(实例宕机)或主动删除键,触发下一个键的监听事件,实现锁传递。
  • Revision:etcd内部维护了一个全局的Revision值,并会随着事务的递增而递增。可以用Revision值的大小来决定获取锁的先后顺序,在上锁的时候已经决定了获取锁先后顺序,后续有客户端释放锁也不会产生惊群效应。
  • watch:watch机制可以用于监听锁的删除事件,不必使用忙轮询的方式查看是否释放了锁,更加高效。同时,在watch时候可以通过Revision来进行监听,只需要监听距离自己最近而且比自己小的一个Revision就可以做到锁的实时获取。
落地范例(Go + etcd,K8s 环境)

适合 K8s 部署的云原生微服务,与 K8s 生态无缝集成:

package main

import (
        "context"
        "fmt"
        "time"
        clientv3 "go.etcd.io/etcd/client/v3"
)
func main() {
        // 1. 连接K8s环境的etcd集群(云原生部署,通过Service名称访问)
        cli, err := clientv3.New(clientv3.Config{
                Endpoints:   []string{"etcd-client.kube-system.svc:2379"}, // K8s中etcd服务地址
                DialTimeout: 5 * time.Second,
        })
        if err != nil {
                panic(fmt.Sprintf("连接etcd失败:%v", err))
        }
        defer cli.Close()
        // 2. 定义锁参数(全局锚定物:/locks/task调度键)
        lockKey := "/locks/task:data-sync"
        leaseTTL := 5 // 租约超时5秒(自动释放锁)
        // 3. 创建租约
        leaseResp, err := cli.Grant(context.Background(), leaseTTL)
        if err != nil {
                panic(fmt.Sprintf("创建租约失败:%v", err))
        }
        leaseID := leaseResp.ID
        // 4. 原子获取锁:Compare判断锁是否存在,不存在则创建
        txn := cli.Txn(context.Background())
        txn.If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)).
                Then(clientv3.OpPut(lockKey, "instance-1", clientv3.WithLease(leaseID))).
                Else(clientv3.OpGet(lockKey))
        txnResp, err := txn.Commit()
        if err != nil {
                panic(fmt.Sprintf("获取锁失败:%v", err))
        }
        // 5. 处理锁结果
        if txnResp.Succeeded {
                fmt.Println("获取锁成功,执行分布式任务(数据同步)...")
                // 租约续期(避免任务执行中锁过期)
                keepAliveCh, err := cli.KeepAlive(context.Background(), leaseID)
                if err != nil {
                        panic(fmt.Sprintf("租约续期失败:%v", err))
                }
                defer func() {
                        // 释放锁:撤销租约(临时键自动删除)
                        _, err = cli.Revoke(context.Background(), leaseID)
                        if err != nil {
                                fmt.Printf("释放锁失败:%v", err)
                        }
                }()
                // 模拟任务执行(5秒)
                time.Sleep(5 * time.Second)
                fmt.Println("任务执行完成,释放锁")
                <-keepAliveCh // 阻塞接收续期响应
        } else {
                fmt.Println("获取锁失败,任务已被其他实例执行")
        }
}
优缺点
  • 优点:强一致性、云原生友好(K8s 集成)、高性能、自动续期、部署维护简单(K8s 自带 etcd 集群);
  • 缺点:生态较 Redis/ZooKeeper 不完善、非 K8s 环境部署成本高;
  • 适用场景:K8s 部署的云原生微服务、分布式任务调度、对一致性和性能均有要求的场景。

四、方案对比与云原生选型建议

方案 全局锚定物 并发性能 一致性 云原生适配性 适用场景
Redis Redis 键值对 高(万级) 强(容器 / 集群) 秒杀、库存扣减、高并发业务
ZooKeeper 临时有序节点 中(千级) 中(集群部署) 分布式事务、支付对账、高一致性业务
数据库(乐观锁) 数据行版本号 中(千级) 强(云 RDS) 小型系统、低并发数据更新
数据库(悲观锁) 数据行锁 低(百级) 强(云 RDS) 后台管理系统、低频互斥操作
etcd 租约临时键 高(万级) 极强(K8s) K8s 环境、分布式任务调度

选型优先级(云原生场景):

  1. 优先选 Redis(Redisson):覆盖 80% 以上云原生场景,平衡性能、易用性和部署成本,支持高并发;
  2. K8s 环境选 etcd:无需额外部署中间件,与 K8s 生态无缝集成,强一致性 + 高性能;
  3. 高一致性需求选 ZooKeeper:如分布式事务、支付对账等核心业务,牺牲部分性能换数据可靠性;
  4. 小型系统 / 快速落地选数据库锁:利用现有云数据库/中间件,无需额外组件,降低部署维护成本。



上一篇:SRC漏洞挖掘实战技巧:403绕过与二维码跳转漏洞案例分析
下一篇:Maven多模块项目JDK版本管理:使用Toolchains实现JDK8与JDK21混合编译
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 19:00 , Processed in 0.242674 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表