redis和数据库实现分布式锁
目录
- 一、为什么需要分布式锁
- 1. 什么是锁
- 2. 单机锁为什么失效
- 二、分布式锁是什么
- 三、分布式锁需要满足什么条件
- 1. 互斥性
- 2. 可重入
- 3. 防死锁
- 4. 高可用
- 四、Redis实现分布式锁
- 1. 原理
- 2. Spring Boot实现
- 加锁
- 释放锁
- 3. Redisson实现
- 五、数据库唯一索引实现分布式锁
- 1. 建表
- 2. 获取锁
- 3. 原理
- 4. 缺点
- 六、数据库乐观锁(版本号)
- 1. 表结构
- 2. 查询
- 3. 更新
- 4. 原理
- 5. MyBatis Plus实现
- 七、数据库悲观锁
- 1. 获取锁
- 2. 原理
- 3. 缺点
- 八、ZooKeeper实现分布式锁
- 九、各种方案对比
- 十、单机环境如何实现锁
- 1. synchronized
- 2. ReentrantLock
- 3. ReadWriteLock
- 总结
一、为什么需要分布式锁
1. 什么是锁
锁(Lock)的本质是:
保证同一时刻只有一个线程(或一个进程)能够执行某段代码。
例如:
publicvoiddeductStock(){stock--;}假设库存为 1:
- 用户A购买
- 用户B购买
两个线程同时执行:
stock--;结果可能变成:
库存:-1这就是典型的并发问题。
因此需要锁来保证:
synchronized(lock){stock--;}同一时刻只能有一个线程进入。
2. 单机锁为什么失效
在单体应用中:
JVM ┌─────────────────┐ │ Thread-1 │ │ Thread-2 │ │ synchronized │ └─────────────────┘synchronized、ReentrantLock都能正常工作。
但是微服务部署后:
Nginx / \ Service-A Service-B JVM1 JVM2用户请求可能进入:
请求1 -> JVM1 请求2 -> JVM2此时:
synchronized(lock)只锁住当前 JVM。
因为:
JVM1 不知道 JVM2 的锁状态 JVM2 不知道 JVM1 的锁状态于是两个服务同时执行。
这就是:
本地锁失效问题
因此需要:
所有节点共享同一把锁
这就是分布式锁。
二、分布式锁是什么
定义:
分布式系统中多个节点共同竞争同一个共享资源时,用来保证同一时刻只有一个节点执行操作的机制。
例如:
秒杀活动 定时任务 订单创建 库存扣减 退款操作都可能需要分布式锁。
三、分布式锁需要满足什么条件
一个合格的分布式锁通常需要满足:
1. 互斥性
同一时间只能一个客户端获得锁2. 可重入
例如:
methodA()->methodB()methodA已经获得锁。
methodB再次加锁:
lock.lock();不能死锁。
3. 防死锁
持有锁的服务挂掉:
JVM Crash锁必须自动释放。
通常依赖:
TTL过期时间4. 高可用
Redis挂了:
锁全部失效需要主从、哨兵或集群保障。
四、Redis实现分布式锁
这是目前最常见的方案。
1. 原理
Redis提供:
SET key value NX EX 30含义:
NX:不存在才创建 EX:30秒过期执行成功:
OK说明获得锁。
执行失败:
null说明锁已被占用。
2. Spring Boot实现
加锁
@AutowiredprivateStringRedisTemplateredisTemplate;publicbooleantryLock(Stringkey){returnBoolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key,UUID.randomUUID().toString(),30,TimeUnit.SECONDS));}底层执行:
SET lock:order uuid NX EX 30释放锁
错误写法:
redisTemplate.delete(key);问题:
线程A锁超时 线程B获得锁 线程A执行delete 把线程B的锁删了正确做法:
Stringlua="if redis.call('get',KEYS[1])==ARGV[1] then "+" return redis.call('del',KEYS[1]) "+"else return 0 end";redisTemplate.execute(newDefaultRedisScript<>(lua,Long.class),Collections.singletonList(key),uuid);保证:
谁加锁 谁解锁3. Redisson实现
实际项目更推荐。
引入:
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId></dependency>获取锁:
RLocklock=redissonClient.getLock("orderLock");try{lock.lock();createOrder();}finally{lock.unlock();}优点:
自动续期 可重入 看门狗机制 高可靠面试中一般回答:
生产环境推荐 Redisson。
五、数据库唯一索引实现分布式锁
这是最简单的方案之一。
1. 建表
CREATETABLEdistributed_lock(lock_nameVARCHAR(100)PRIMARYKEY,create_timeDATETIME);2. 获取锁
try{lockMapper.insert(lockName);returntrue;}catch(DuplicateKeyExceptione){returnfalse;}对应SQL:
INSERTINTOdistributed_lockVALUES('ORDER_LOCK',NOW());3. 原理
假设:
JVM1 插入成功 JVM2 插入同样主键数据库保证:
主键唯一第二次插入失败。
因此:
只有一个服务获得锁4. 缺点
数据库压力大:
频繁insert 频繁delete性能远低于Redis。
六、数据库乐观锁(版本号)
严格来说:
它不是传统意义上的分布式锁
但经常用于解决并发问题。
1. 表结构
CREATETABLEstock(idBIGINT,stockINT,versionINT);数据:
stock = 100 version = 12. 查询
SELECTstock,versionFROMstockWHEREid=1;得到:
stock=100 version=13. 更新
UPDATEstockSETstock=stock-1,version=version+1WHEREid=1ANDversion=1;4. 原理
线程A:
version=1线程B:
version=1线程A先更新:
version=2线程B再更新:
WHEREversion=1匹配不到。
更新失败:
update count = 0然后重试。
5. MyBatis Plus实现
实体:
@VersionprivateIntegerversion;配置:
@BeanpublicMybatisPlusInterceptorinterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}更新时自动带:
version=oldVersion条件。
七、数据库悲观锁
利用数据库行锁。
1. 获取锁
SELECT*FROMstockWHEREid=1FORUPDATE;2. 原理
事务提交前:
其他事务无法修改这条记录例如:
@TransactionalpublicvoiddeductStock(){Stockstock=stockMapper.selectForUpdate(1L);stock.setCount(stock.getCount()-1);stockMapper.update(stock);}3. 缺点
性能较差:
锁粒度大 阻塞严重 数据库压力高八、ZooKeeper实现分布式锁
原理:
临时顺序节点例如:
/lock/order0001 /lock/order0002 /lock/order0003最小节点获得锁:
order0001删除后:
order0002获得锁。
常见框架:
Apache Curator优点:
一致性强 可靠缺点:
实现复杂 性能低于Redis九、各种方案对比
| 方案 | 性能 | 实现难度 | 推荐程度 |
|---|---|---|---|
| synchronized | ★★★★★ | 简单 | 单机 |
| ReentrantLock | ★★★★★ | 简单 | 单机 |
| 数据库唯一索引 | ★★ | 简单 | 小项目 |
| 数据库悲观锁 | ★★ | 简单 | 一般 |
| 数据库乐观锁 | ★★★★ | 简单 | 库存扣减 |
| ZooKeeper | ★★★ | 复杂 | 大型系统 |
| Redis+Lua | ★★★★★ | 中等 | 推荐 |
| Redisson | ★★★★★ | 简单 | 生产推荐 |
十、单机环境如何实现锁
如果系统没有部署多个实例:
只有一个JVM那么根本不需要分布式锁。
1. synchronized
publicsynchronizedvoidcreateOrder(){}或者:
privatefinalObjectlock=newObject();synchronized(lock){}特点:
JDK内置 自动释放 简单2. ReentrantLock
privatefinalReentrantLocklock=newReentrantLock();publicvoidcreateOrder(){lock.lock();try{//业务逻辑}finally{lock.unlock();}}优点:
可重入 可中断 支持公平锁 支持超时3. ReadWriteLock
读多写少场景:
privateReadWriteLocklock=newReentrantReadWriteLock();读锁:
lock.readLock().lock();写锁:
lock.writeLock().lock();特点:
多个读线程同时执行 写线程独占总结
分布式锁的核心目标只有一句话:
在多个 JVM、多个服务实例同时访问同一资源时,保证同一时刻只有一个节点执行关键业务逻辑。
实际生产中:
- 单机应用:
synchronized、ReentrantLock - 库存扣减:乐观锁(Version)
- 中小项目:Redis + Lua
- 企业级项目:Redisson
- 强一致性场景:ZooKeeper
学习路径,掌握:
synchronized → ReentrantLock → Redis SET NX EX → Lua释放锁 → Redisson看门狗 → 乐观锁Version → ZooKeeper临时顺序节点
