尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

分布式锁 — 概念、原理与实践

分布式锁 — 概念、原理与实践 分布式锁 — 概念、原理与实践一、为什么需要分布式锁1.1 单机锁的局限单机应用1个JVM进程 线程A ─┐ 线程B ─┼→ synchronized / ReentrantLock → 资源 线程C ─┘ ✅ 有效所有线程在同一个JVM内共享同一把锁 分布式应用多个JVM进程/多台服务器 服务器1JVM-1线程A → synchronized → 本地锁1 服务器2JVM-2线程B → synchronized → 本地锁2 服务器3JVM-3线程C → synchronized → 本地锁3 ❌ 无效三台服务器各自加的是各自的本地锁互相不可见 → 三个线程可以同时访问共享资源数据库同一行1.2 类比理解本地锁 家门锁只能锁住自己家门邻居有自己的锁 分布式锁 小区门禁所有住户共享同一个门禁系统一次只能一个人通过1.3 核心问题多个服务实例同时操作同一条数据时如何保证同一时刻只有一个实例能执行关键操作注博客https://blog.csdn.net/badao_liumang_qizhi二、核心概念2.1 什么是分布式锁分布式锁是一种跨进程/跨服务器的互斥机制通过一个所有实例都能访问的共享存储Redis/ZooKeeper/数据库来协调并发访问。2.2 关键术语概念说明类比锁标记当前资源正在被使用公共厕所的使用中牌子获取锁加锁尝试标记资源被自己占用进门后翻转牌子为使用中释放锁解锁标记资源不再被自己占用出门后翻转牌子为空闲锁持有者标识记录谁持有锁requestId牌子上写了3号隔间-张三锁过期时间TTL锁自动释放的时间超过30分钟自动解锁防止人晕在里面自旋等待获取锁失败后反复重试门口排队等待超时放弃等待超过一定时间后放弃等了10分钟还没轮到走人2.3 分布式锁必须满足的条件条件说明互斥性同一时刻只有一个客户端能持有锁防死锁持有者崩溃后锁能自动释放TTL 过期防误释放只有锁的持有者才能释放锁A 加的锁 B 不能释放高可用锁服务本身不能是单点故障三、与本地锁的对比3.1 Java 本地锁JVM 内有效锁类型特点适用范围synchronizedJVM 内置自动释放同一 JVM 内的线程ReentrantLock可重入、可中断、可超时同一 JVM 内的线程ReadWriteLock读写分离读不互斥同一 JVM 内的线程3.2 分布式锁跨 JVM 有效实现方式存储介质特点Redis内存性能最高但 Redis 宕机有风险ZooKeeper磁盘内存强一致性但性能略低数据库磁盘最简单但性能最低Etcd磁盘内存强一致性云原生场景四、底层原理4.1 基于 Redis 的分布式锁加锁原理客户端A → RedisSET lock_key requestId_A NX PX 30000 NX Only set if Not eXistskey不存在时才设置成功 PX 30000 30秒后自动过期 如果返回 OK → 加锁成功 如果返回 nil → 加锁失败锁被别人持有为什么用 NX时刻T1客户端A执行 SET lock NX → 成功key不存在 时刻T2客户端B执行 SET lock NX → 失败key已存在 → 只有A获得锁B被拒绝为什么需要 PX过期时间客户端A加锁成功后崩溃了没来得及释放锁 没有过期时间 → 锁永远不释放 → 死锁其他人永远无法获取 有过期时间 → 30秒后Redis自动删除key → 其他人可以获取锁为什么需要 requestId场景A加的锁B来释放 时刻T1A 加锁成功lock requestId_A 时刻T2A 处理业务超时锁过期自动释放 时刻T3B 加锁成功lock requestId_B 时刻T4A 处理完毕执行释放锁操作 如果不校验 requestId A 直接 DEL lock → 把B的锁删了B以为自己还持有锁实际已经没了 如果校验 requestId A 释放时检查 lock 的值是否是 requestId_A → 不是 → 不释放 → B 的锁不受影响加锁 Lua 脚本原子操作-- KEYS[1] 锁的key-- KEYS[2] requestId的key-- ARGV[1] requestId值-- ARGV[2] 过期时间毫秒if(redis.call(exists,KEYS[1])0)thenredis.call(hset,KEYS[1],KEYS[2],ARGV[1])redis.call(pexpire,KEYS[1],ARGV[2])return1-- 加锁成功elsereturn0-- 锁已存在加锁失败end为什么用 Lua 脚本existshsetpexpire三个命令需要原子执行如果分开执行两条命令之间可能被其他客户端插入操作Redis 保证 Lua 脚本原子性执行解锁 Lua 脚本原子操作-- KEYS[1] 锁的key-- KEYS[2] requestId的key-- ARGV[1] requestId值ifredis.call(hget,KEYS[1],KEYS[2])ARGV[1]thenredis.call(del,KEYS[1])return1-- 解锁成功elsereturn0-- 不是自己的锁拒绝释放end4.2 基于数据库的分布式锁原理利用数据库的唯一约束或排他锁实现互斥。方式1唯一索引INSERT 方式-- 建表CREATETABLEdistributed_lock(idBIGINTAUTO_INCREMENTPRIMARYKEY,lock_keyVARCHAR(100)NOTNULLUNIQUE,-- 唯一约束request_idVARCHAR(64)NOTNULL,expire_timeDATETIMENOTNULL,create_timeDATETIMENOTNULL);-- 加锁INSERT唯一约束保证互斥INSERTINTOdistributed_lock(lock_key,request_id,expire_time,create_time)VALUES(order_process_123,uuid-xxx,NOW()INTERVAL30SECOND,NOW());-- 成功 → 获得锁-- 失败Duplicate entry → 锁被占用-- 解锁DELETE校验 request_idDELETEFROMdistributed_lockWHERElock_keyorder_process_123ANDrequest_iduuid-xxx;方式2SELECT FOR UPDATE悲观锁-- 加锁BEGIN;SELECT*FROMdistributed_lockWHERElock_keyorder_process_123FORUPDATE;-- 其他事务对同一行的 SELECT FOR UPDATE 会阻塞等待-- 执行业务逻辑...-- 解锁COMMIT;-- 事务提交后锁自动释放4.3 基于 ZooKeeper 的分布式锁原理利用 ZooKeeper 的临时有序节点实现。/locks/order_process_123/ ├── node_0000000001 (客户端A创建) ← 序号最小获得锁 ├── node_0000000002 (客户端B创建) ← 监听前一个节点 └── node_0000000003 (客户端C创建) ← 监听前一个节点 客户端A处理完毕 → 删除 node_0000000001 → 触发 node_0000000002 的 Watch 通知 → 客户端B检查自己是否最小 → 是 → 获得锁优点临时节点在客户端断连时自动删除防死锁Watch 机制避免轮询。五、JPA/数据库中的锁实现5.1 乐观锁Optimistic Locking思想假设冲突很少发生不加锁提交时检查是否被修改过。EntityTable(namestock)publicclassStock{IdprivateIntegerid;privateIntegeritemSkuId;privateIntegerqty;Version// JPA 乐观锁注解privateIntegerversion;}执行流程线程ASELECT * FROM stock WHERE id1 → version5, qty100 线程BSELECT * FROM stock WHERE id1 → version5, qty100 线程AUPDATE stock SET qty90, version6 WHERE id1 AND version5 → 成功影响1行 线程BUPDATE stock SET qty80, version6 WHERE id1 AND version5 → 失败影响0行version已变为6 → JPA 抛出 OptimisticLockException → 线程B可以重试适用场景并发冲突概率低读多写少。5.2 悲观锁Pessimistic Locking思想假设冲突频繁操作前先加锁。// JPA 悲观锁查询RepositorypublicinterfaceStockRepositoryextendsJpaRepositoryStock,Integer{Lock(LockModeType.PESSIMISTIC_WRITE)Query(SELECT s FROM Stock s WHERE s.itemSkuId :itemSkuId)StockfindByItemSkuIdForUpdate(Param(itemSkuId)IntegeritemSkuId);}执行的 SQLSELECT*FROMstockWHEREitem_sku_id?FORUPDATE;-- 其他事务对同一行的写操作会阻塞直到当前事务提交适用场景并发冲突概率高写多读少。5.3 乐观锁 vs 悲观锁 vs 分布式锁维度乐观锁悲观锁分布式锁加锁时机更新时检查查询时加锁业务操作前加锁锁的范围数据库行级数据库行级跨服务/跨资源冲突处理抛异常/重试阻塞等待阻塞等待/超时放弃性能高无锁中行锁等待取决于实现Redis最快死锁风险无有多表交叉锁有靠TTL解决适用场景低冲突高冲突单表跨服务/跨资源互斥典型用法VersionFOR UPDATERedis SETNX六、各技术框架中的分布式锁实现6.1 Redisson最流行的 Redis 分布式锁框架// 依赖// dependency// groupIdorg.redisson/groupId// artifactIdredisson-spring-boot-starter/artifactId// version3.27.0/version// /dependencyServicepublicclassOrderService{ResourceprivateRedissonClientredissonClient;publicvoidprocessOrder(StringorderId){RLocklockredissonClient.getLock(order_lock_orderId);try{// 等待10秒锁自动释放时间30秒booleanacquiredlock.tryLock(10,30,TimeUnit.SECONDS);if(acquired){doProcess(orderId);}else{thrownewRuntimeException(获取锁超时);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}}Redisson 的高级特性看门狗机制Watchdog自动续期防止业务未完成锁就过期可重入锁同一线程可以重复获取同一把锁红锁RedLock多 Redis 节点加锁防单点故障6.2 Spring Integration数据库锁// 依赖// dependency// groupIdorg.springframework.integration/groupId// artifactIdspring-integration-jdbc/artifactId// /dependencyConfigurationpublicclassLockConfig{BeanpublicDefaultLockRepositorylockRepository(DataSourcedataSource){DefaultLockRepositoryrepositorynewDefaultLockRepository(dataSource);repository.setPrefix(APP_LOCK_);repository.setTimeToLive(30000);// 30秒过期returnrepository;}BeanpublicJdbcLockRegistrylockRegistry(LockRepositorylockRepository){returnnewJdbcLockRegistry(lockRepository);}}ServicepublicclassOrderService{ResourceprivateLockRegistrylockRegistry;publicvoidprocessOrder(StringorderId){LocklocklockRegistry.obtain(order_orderId);try{if(lock.tryLock(10,TimeUnit.SECONDS)){try{doProcess(orderId);}finally{lock.unlock();}}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}6.3 CuratorZooKeeper 锁// 依赖// dependency// groupIdorg.apache.curator/groupId// artifactIdcurator-recipes/artifactId// version5.5.0/version// /dependencyServicepublicclassOrderService{ResourceprivateCuratorFrameworkcuratorClient;publicvoidprocessOrder(StringorderId){InterProcessMutexlocknewInterProcessMutex(curatorClient,/locks/order_orderId);try{if(lock.acquire(10,TimeUnit.SECONDS)){try{doProcess(orderId);}finally{lock.release();}}}catch(Exceptione){thrownewRuntimeException(获取锁失败,e);}}}七、通用示例代码7.1 基于 Redis 的分布式锁/** * 分布式锁实现基于Redis Lua脚本. * * 设计要点 * 1. NX 保证互斥 * 2. PX 防死锁自动过期 * 3. requestId 防误释放 * 4. Lua 脚本保证原子性 * 5. 自旋等待 超时退出 */publicclassRedisDistributedLockimplementsAutoCloseable{privatestaticfinalLoggerlogLoggerFactory.getLogger(RedisDistributedLock.class);privatefinalStringRedisTemplateredisTemplate;privatefinalStringlockKey;privatefinalStringrequestId;privatefinallongleaseTimeMillis;privatevolatilebooleanlockedfalse;privatestaticfinalStringLOCK_PREFIXdistributed_lock:;// 加锁Lua脚本privatestaticfinalStringLOCK_SCRIPTif redis.call(setnx, KEYS[1], ARGV[1]) 1 then redis.call(pexpire, KEYS[1], ARGV[2]); return 1; else return 0; end;// 解锁Lua脚本privatestaticfinalStringUNLOCK_SCRIPTif redis.call(get, KEYS[1]) ARGV[1] then redis.call(del, KEYS[1]); return 1; else return 0; end;publicRedisDistributedLock(StringRedisTemplateredisTemplate,StringbusinessKey,longleaseTimeMillis){this.redisTemplateredisTemplate;this.lockKeyLOCK_PREFIXbusinessKey;this.requestIdUUID.randomUUID().toString().replace(-,);this.leaseTimeMillisleaseTimeMillis;}/** * 尝试获取锁支持超时等待. * * param waitTimeMillis 最大等待时间毫秒0表示不等待立即返回 * return true-获取成功false-超时失败 */publicbooleantryLock(longwaitTimeMillis){longdeadlineSystem.currentTimeMillis()waitTimeMillis;// 第一次尝试if(doLock()){this.lockedtrue;log.debug(获取锁成功: key{}, requestId{},lockKey,requestId);returntrue;}// 自旋等待while(System.currentTimeMillis()deadline){try{Thread.sleep(100);// 每100ms重试一次}catch(InterruptedExceptione){Thread.currentThread().interrupt();returnfalse;}if(doLock()){this.lockedtrue;log.debug(获取锁成功(重试): key{}, requestId{},lockKey,requestId);returntrue;}}log.warn(获取锁超时: key{},lockKey);returnfalse;}/** * 释放锁. */publicbooleanunlock(){if(!locked){returntrue;}DefaultRedisScriptLongscriptnewDefaultRedisScript(UNLOCK_SCRIPT,Long.class);LongresultredisTemplate.execute(script,Collections.singletonList(lockKey),requestId);booleansuccessresult!nullresult1;if(success){this.lockedfalse;log.debug(释放锁成功: key{}, requestId{},lockKey,requestId);}else{log.warn(释放锁失败(非持有者): key{}, requestId{},lockKey,requestId);}returnsuccess;}Overridepublicvoidclose(){unlock();}privatebooleandoLock(){DefaultRedisScriptLongscriptnewDefaultRedisScript(LOCK_SCRIPT,Long.class);LongresultredisTemplate.execute(script,Collections.singletonList(lockKey),requestId,String.valueOf(leaseTimeMillis));returnresult!nullresult1;}}7.2 锁工厂/** * 分布式锁工厂. * 注入后直接使用无需关心底层 Redis 操作. */ComponentpublicclassDistributedLockFactory{ResourceprivateStringRedisTemplatestringRedisTemplate;/** * 获取分布式锁实例. * * param businessKey 业务锁标识如 order_123 * param leaseTime 锁过期时间 * param unit 时间单位 */publicRedisDistributedLockgetLock(StringbusinessKey,longleaseTime,TimeUnitunit){returnnewRedisDistributedLock(stringRedisTemplate,businessKey,unit.toMillis(leaseTime));}/** * 获取分布式锁默认过期10分钟. */publicRedisDistributedLockgetLock(StringbusinessKey){returngetLock(businessKey,10,TimeUnit.MINUTES);}}7.3 使用示例ServicepublicclassStockDeductService{ResourceprivateDistributedLockFactorylockFactory;ResourceprivateStockRepositorystockRepository;/** * 扣减库存分布式锁保证并发安全. */publicvoiddeductStock(IntegeritemId,Integerqty){StringlockKeystock_deduct_itemId;// try-with-resources 自动释放锁try(RedisDistributedLocklocklockFactory.getLock(lockKey,30,TimeUnit.SECONDS)){// 尝试获取锁最多等待5秒if(!lock.tryLock(5000)){thrownewRuntimeException(系统繁忙请稍后重试);}// 获取锁成功安全执行业务StockstockstockRepository.findByItemId(itemId);if(stock.getQty()qty){thrownewRuntimeException(库存不足);}stock.setQty(stock.getQty()-qty);stockRepository.save(stock);}// 离开 try 块自动调用 close() → unlock()}}八、常见问题与解决方案问题原因解决方案死锁持有者崩溃未释放TTL 过期自动释放误释放A 释放了 B 的锁requestId 校验锁过期但业务未完成业务执行时间超过 TTLWatchdog 自动续期RedissonRedis 主从切换丢锁主节点宕机从节点升主但未同步锁RedLock多节点加锁锁饥饿某些线程一直获取不到锁公平锁按申请顺序排队重入问题同一线程再次获取同一把锁可重入锁计数器线程ID九、关键设计总结设计要点Redis 实现数据库实现ZooKeeper 实现互斥性SETNXUNIQUE KEY / FOR UPDATE临时有序节点防死锁PEXPIRE TTL定时清理过期记录临时节点自动删除防误释放Lua 校验 requestIdWHERE request_id ?只能删除自己创建的节点原子性Lua 脚本数据库事务ZK 原子性保证等待通知轮询sleep retry无需轮询Watch 机制事件驱动性能⭐⭐⭐⭐⭐内存操作⭐⭐磁盘IO⭐⭐⭐网络磁盘可靠性⭐⭐⭐主从可能丢锁⭐⭐⭐⭐事务保证⭐⭐⭐⭐⭐强一致性
返回列表