redis和数据库实现分布式锁

目录

一、为什么需要分布式锁

1. 什么是锁

锁(Lock)的本质是:

保证同一时刻只有一个线程(或一个进程)能够执行某段代码。

例如:

java 复制代码
public void deductStock() {
    stock--;
}

假设库存为 1:

  • 用户A购买
  • 用户B购买

两个线程同时执行:

java 复制代码
stock--;

结果可能变成:

text 复制代码
库存:-1

这就是典型的并发问题。

因此需要锁来保证:

java 复制代码
synchronized(lock){
    stock--;
}

同一时刻只能有一个线程进入。


2. 单机锁为什么失效

在单体应用中:

text 复制代码
          JVM
 ┌─────────────────┐
 │ Thread-1        │
 │ Thread-2        │
 │ synchronized    │
 └─────────────────┘

synchronizedReentrantLock 都能正常工作。

但是微服务部署后:

text 复制代码
        Nginx

     /         \
 Service-A   Service-B

     JVM1       JVM2

用户请求可能进入:

text 复制代码
请求1 -> JVM1
请求2 -> JVM2

此时:

java 复制代码
synchronized(lock)

只锁住当前 JVM。

因为:

text 复制代码
JVM1 不知道 JVM2 的锁状态
JVM2 不知道 JVM1 的锁状态

于是两个服务同时执行。

这就是:

本地锁失效问题

因此需要:

所有节点共享同一把锁

这就是分布式锁。


二、分布式锁是什么

定义:

分布式系统中多个节点共同竞争同一个共享资源时,用来保证同一时刻只有一个节点执行操作的机制。

例如:

text 复制代码
秒杀活动
定时任务
订单创建
库存扣减
退款操作

都可能需要分布式锁。


三、分布式锁需要满足什么条件

一个合格的分布式锁通常需要满足:

1. 互斥性

text 复制代码
同一时间只能一个客户端获得锁

2. 可重入

例如:

java 复制代码
methodA()
    -> methodB()

methodA已经获得锁。

methodB再次加锁:

java 复制代码
lock.lock();

不能死锁。


3. 防死锁

持有锁的服务挂掉:

text 复制代码
JVM Crash

锁必须自动释放。

通常依赖:

text 复制代码
TTL过期时间

4. 高可用

Redis挂了:

text 复制代码
锁全部失效

需要主从、哨兵或集群保障。


四、Redis实现分布式锁

这是目前最常见的方案。


1. 原理

Redis提供:

text 复制代码
SET key value NX EX 30

含义:

text 复制代码
NX:不存在才创建
EX:30秒过期

执行成功:

text 复制代码
OK

说明获得锁。

执行失败:

text 复制代码
null

说明锁已被占用。


2. Spring Boot实现

加锁

java 复制代码
@Autowired
private StringRedisTemplate redisTemplate;

public boolean tryLock(String key) {

    return Boolean.TRUE.equals(
            redisTemplate.opsForValue().setIfAbsent(
                    key,
                    UUID.randomUUID().toString(),
                    30,
                    TimeUnit.SECONDS
            )
    );
}

底层执行:

redis 复制代码
SET lock:order uuid NX EX 30

释放锁

错误写法:

java 复制代码
redisTemplate.delete(key);

问题:

text 复制代码
线程A锁超时

线程B获得锁

线程A执行delete

把线程B的锁删了

正确做法:

java 复制代码
String lua =
        "if redis.call('get',KEYS[1])==ARGV[1] then " +
        " return redis.call('del',KEYS[1]) " +
        "else return 0 end";

redisTemplate.execute(
        new DefaultRedisScript<>(lua, Long.class),
        Collections.singletonList(key),
        uuid
);

保证:

text 复制代码
谁加锁
谁解锁

3. Redisson实现

实际项目更推荐。

引入:

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>

获取锁:

java 复制代码
RLock lock = redissonClient.getLock("orderLock");

try {
    lock.lock();

    createOrder();

} finally {
    lock.unlock();
}

优点:

text 复制代码
自动续期
可重入
看门狗机制
高可靠

面试中一般回答:

生产环境推荐 Redisson。


五、数据库唯一索引实现分布式锁

这是最简单的方案之一。


1. 建表

sql 复制代码
CREATE TABLE distributed_lock(
    lock_name VARCHAR(100) PRIMARY KEY,
    create_time DATETIME
);

2. 获取锁

java 复制代码
try {

    lockMapper.insert(lockName);

    return true;

} catch (DuplicateKeyException e) {

    return false;
}

对应SQL:

sql 复制代码
INSERT INTO distributed_lock
VALUES('ORDER_LOCK',NOW());

3. 原理

假设:

text 复制代码
JVM1 插入成功
JVM2 插入同样主键

数据库保证:

text 复制代码
主键唯一

第二次插入失败。

因此:

text 复制代码
只有一个服务获得锁

4. 缺点

数据库压力大:

text 复制代码
频繁insert
频繁delete

性能远低于Redis。


六、数据库乐观锁(版本号)

严格来说:

它不是传统意义上的分布式锁

但经常用于解决并发问题。


1. 表结构

sql 复制代码
CREATE TABLE stock(
    id BIGINT,
    stock INT,
    version INT
);

数据:

text 复制代码
stock = 100
version = 1

2. 查询

sql 复制代码
SELECT stock,version
FROM stock
WHERE id = 1;

得到:

text 复制代码
stock=100
version=1

3. 更新

sql 复制代码
UPDATE stock
SET stock = stock - 1,
    version = version + 1
WHERE id = 1
AND version = 1;

4. 原理

线程A:

text 复制代码
version=1

线程B:

text 复制代码
version=1

线程A先更新:

text 复制代码
version=2

线程B再更新:

sql 复制代码
WHERE version=1

匹配不到。

更新失败:

text 复制代码
update count = 0

然后重试。


5. MyBatis Plus实现

实体:

java 复制代码
@Version
private Integer version;

配置:

java 复制代码
@Bean
public MybatisPlusInterceptor interceptor() {

    MybatisPlusInterceptor interceptor =
            new MybatisPlusInterceptor();

    interceptor.addInnerInterceptor(
            new OptimisticLockerInnerInterceptor());

    return interceptor;
}

更新时自动带:

sql 复制代码
version = oldVersion

条件。


七、数据库悲观锁

利用数据库行锁。


1. 获取锁

sql 复制代码
SELECT *
FROM stock
WHERE id = 1
FOR UPDATE;

2. 原理

事务提交前:

text 复制代码
其他事务无法修改这条记录

例如:

java 复制代码
@Transactional
public void deductStock(){

    Stock stock =
            stockMapper.selectForUpdate(1L);

    stock.setCount(
            stock.getCount()-1
    );

    stockMapper.update(stock);
}

3. 缺点

性能较差:

text 复制代码
锁粒度大
阻塞严重
数据库压力高

八、ZooKeeper实现分布式锁

原理:

text 复制代码
临时顺序节点

例如:

text 复制代码
/lock/order0001
/lock/order0002
/lock/order0003

最小节点获得锁:

text 复制代码
order0001

删除后:

text 复制代码
order0002

获得锁。

常见框架:

text 复制代码
Apache Curator

优点:

text 复制代码
一致性强
可靠

缺点:

text 复制代码
实现复杂
性能低于Redis

九、各种方案对比

方案 性能 实现难度 推荐程度
synchronized ★★★★★ 简单 单机
ReentrantLock ★★★★★ 简单 单机
数据库唯一索引 ★★ 简单 小项目
数据库悲观锁 ★★ 简单 一般
数据库乐观锁 ★★★★ 简单 库存扣减
ZooKeeper ★★★ 复杂 大型系统
Redis+Lua ★★★★★ 中等 推荐
Redisson ★★★★★ 简单 生产推荐

十、单机环境如何实现锁

如果系统没有部署多个实例:

text 复制代码
只有一个JVM

那么根本不需要分布式锁。


1. synchronized

java 复制代码
public synchronized void createOrder() {

}

或者:

java 复制代码
private final Object lock = new Object();

synchronized(lock){

}

特点:

text 复制代码
JDK内置
自动释放
简单

2. ReentrantLock

java 复制代码
private final ReentrantLock lock =
        new ReentrantLock();

public void createOrder(){

    lock.lock();

    try{

        //业务逻辑

    }finally{

        lock.unlock();
    }
}

优点:

text 复制代码
可重入
可中断
支持公平锁
支持超时

3. ReadWriteLock

读多写少场景:

java 复制代码
private ReadWriteLock lock =
        new ReentrantReadWriteLock();

读锁:

java 复制代码
lock.readLock().lock();

写锁:

java 复制代码
lock.writeLock().lock();

特点:

text 复制代码
多个读线程同时执行
写线程独占

总结

分布式锁的核心目标只有一句话:

在多个 JVM、多个服务实例同时访问同一资源时,保证同一时刻只有一个节点执行关键业务逻辑。

实际生产中:

  • 单机应用:synchronizedReentrantLock
  • 库存扣减:乐观锁(Version)
  • 中小项目:Redis + Lua
  • 企业级项目:Redisson
  • 强一致性场景:ZooKeeper

学习路径,掌握:

synchronized → ReentrantLock → Redis SET NX EX → Lua释放锁 → Redisson看门狗 → 乐观锁Version → ZooKeeper临时顺序节点

相关推荐
爱吃牛肉的大老虎1 小时前
Kafka集群之抛弃 Zookeeper
分布式·zookeeper·kafka
zhougl9961 小时前
Database(数据库)和 Schema(模式)
数据库·oracle
weixin_523185321 小时前
Java内存模型详解:栈、堆、方法区、本地方法栈与程序计数器
java·开发语言
ywl4708120871 小时前
泛型extends和super的区别
java
专注API从业者1 小时前
告别手动翻页!基于淘宝商品接口 + Open Claw 实现自动化选品与实时监控(附完整 Python 代码)
大数据·运维·数据库·自动化
曹牧1 小时前
Oracle:xml转义
xml·数据库·oracle
湖南天硕国产SSD1 小时前
工业存储可靠性进阶:天硕工业固态硬盘动态温控与寿命优化技术实践
网络·数据库·算法·工业存储·天硕存储·工业固态硬盘
我星期八休息1 小时前
Linux系统编程— Mmap实现⽂件LRU缓存
linux·运维·服务器·数据库·mysql·缓存