基于Redis的分布式锁

引入

在jvm内部只有一个锁监视器,所以只有一个线程可以获取锁,可以实现线程间的互斥

但是,当有多个jvm的时候,就会有多个锁监视器,就会有多个线程获取到锁,这样就没有办法实现多jvm进程之间的互斥了

要解决这个问题,就不能再用jvm的锁监视器了,而是在jvm外有一个共同使用的锁监视器

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

基于redis的分布式锁实现思路

实现分布式锁需要实现的两个基本方法:

1.获取锁

  • 互斥:确保只能有一个线程获取锁

  • 非阻塞:尝试一次,成功返回true,失败返回false

    添加锁,NX是互斥,EX是设置超时时间

    SET lock thread1 NX EX 10

2.释放锁

  • 手动释放

  • 超时释放:获取锁时添加一个超时时间

    释放锁,删除即可

    DEL key

流程图:

代码实现1.0

接口:

java 复制代码
public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁的过期时间,单位秒
     * @return true代表获取锁成功,false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unLock();

}

工具类:

java 复制代码
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = Thread.currentThread().getId() + "";
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

实现类:

java 复制代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始");
        }
        // 3.判断秒杀是否结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 已经结束
            return Result.fail("秒杀已经结束");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足");
        }
        Long userId= UserHolder.getUser().getId();
        // 创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁
        boolean isLock = lock.tryLock(1200L);
        // 判断是否获取锁成功
        if (!isLock) {
            // 获取锁失败,返回错误信息
            return Result.fail("不允许重复下单");
        }
        try {
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        }finally {
            // 释放锁
            lock.unLock();
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        // 5.一人一单
        Long userId = UserHolder.getUser().getId();
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 存在,不能重复下单
            return Result.fail("不能重复下单");
        }
        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock=stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足");
        }
        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2 用户id
        voucherOrder.setUserId(userId);
        // 7.3 代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 8.返回订单id
        return Result.ok(orderId);
    }
}

存在问题:

假设线程1获取到锁了,但是业务阻塞了,于是锁超时释放了,这时线程2获取到了锁,正在执行自己业务的过程中,线程1的业务完成了,这个时候线程1就会把线程2的锁给释放掉,这时线程3就能获取到锁,导致线程2和线程3同时获取到了锁,出现了安全问题

解决方法:

释放锁时需要获取锁标识并判断是否一致,一致才能释放锁

流程图:

代码实现2.0

工具类

java 复制代码
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    // 使用hutool包的UUID来唯一标识jvm进程
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标识是否一致
        if (threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

解决了上述问题,但是仍然存在问题:

线程1在获取锁标识并判断是一致的时候,刚要释放锁,突然阻塞了,导致锁超时释放,这时线程2获取到了锁,开始执行自己的业务。。。(接下来与上一个问题一样)

解决思路:把判断和释放做成一个原子性的动作。

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

Lua

Lua是一种编程语言,它的基本语法可以参考网站:Lua 教程 | 菜鸟教程

redis提供的调用函数

语法:

java 复制代码
# 执行redis命令
redis.call('命令名称', 'key', '其他参数', ...)

例如,我们要执行set name jack,则脚本是这样:

java 复制代码
# 执行set name jack
redis.call('set', 'name', 'jack')

例如,我们要先执行set name jack,再执行get name,则脚本如下:

java 复制代码
# 先执行set name jack
redis.call('set', 'name', 'jack')
# 在执行get name
local name = redis.call('get', 'name')
# 返回
return name

调用脚本

写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:

java 复制代码
EVAL script numkeys key [key ...] arg [arg ...]

例如,我们要执行redis.call('set','name',jack')这个脚本,语法如下:

java 复制代码
# 调用脚本
EVAL "return redis.call('set', 'name', 'jack')" 0

双引号内为脚本内容,后面的数字为脚本需要的key类型的参数个数

如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:

java 复制代码
# 调用脚本
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name Rose

name对应KEYS[1],Rose对应ARGV[1]

在Lua中,第一个元素的下标为1

代码实现3.0

释放锁的Lua脚本

Lua 复制代码
-- 获取锁中的线程标识
local id = redis.call("GET", KEYS[1])
-- 比较线程标识与锁中的标识是否一致
if (id == ARGV[1]) then
    -- 一致则删除锁,释放锁
    return redis.call("DEL", KEYS[1])
else
    -- 不一致则直接返回0,表示未释放锁
    return 0
end

首先安装Lua的插件

然后在resources下新建一个Lua脚本写入这段代码

工具类

java 复制代码
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";
    // 声明静态常量:脚本对象是类级别的,全局唯一且不可修改
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    // 静态代码块:类加载时初始化这个常量
    static {
        // 第一步:创建 Redis Lua 脚本对象实例(只 new 一次)
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 第二步:指定 Lua 脚本文件的路径(加载脚本内容,只加载一次)
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        // 第三步:指定脚本执行后的返回值类型(只配置一次)
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        // 防止拆箱出现空指针的情况(Boolean->boolean),所以用下面这个表达式,而不是直接返回success
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

}

这样就解决了上述问题,但是还可以优化

Redisson框架

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

官网地址: Redisson | Valkey & Redis Java client. Ultimate Real-Time Data Platform

GitHub地址: redisson/redisson: Redisson - Valkey & Redis Java client. Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..

Redisson入门

1.引入依赖

XML 复制代码
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.13.6</version>
</dependency>

2.配置Redisson客户端

java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.100.128:6379").setPassowrd("1234") ;
        // 创建客户端
        return Redisson.create(config);
    }
}

3.使用Redisson的分布式锁

java 复制代码
@Resource
private Redissonclient redissonclient;

@Test
void testRedisson() throws InterruptedException {
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonclient.getLock("anyLock");
    // 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SEcoNDS);
    // 判断释放获取成功
    if(isLock){
        try {
            System.out.println("执行业务");
        }finally {
            // 释放锁
            Lock.unlock();
        }
    }
}

Redisson功能

解决了不可重入、不可重试、超时释放、主从一致性的问题

  • 不可重入:同一个线程无法多次获取同一把锁
  • 不可重试 :获取锁只尝试一次就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现
相关推荐
LDG_AGI1 小时前
【推荐系统】深度学习训练框架(二十一):DistributedCheckPoint(DCP) — PyTorch分布式模型存储与加载
pytorch·分布式·深度学习
LDG_AGI1 小时前
【推荐系统】深度学习训练框架(二十三):TorchRec端到端超大规模模型分布式训练+推理实战
人工智能·分布式·深度学习·机器学习·数据挖掘·推荐算法
清晓粼溪2 小时前
SpringCloud-05-Micrometer Tracing+ZipKin分布式链路追踪
分布式·spring·spring cloud
独自破碎E2 小时前
聊聊RabbitMQ
分布式·rabbitmq
小股虫2 小时前
缓存攻防战:在增长中台设计一套高效且安全的缓存体系
java·分布式·安全·缓存·微服务·架构
2503_946971862 小时前
【FullStack/ZeroDay】2026年度全栈魔法架构与分布式恶意节点清除基准索引 (Benchmark Index)
分布式·网络安全·架构·系统架构·区块链·数据集·全栈开发
回家路上绕了弯2 小时前
Resilience4j全面指南:轻量级熔断限流框架的实战与落地
分布式·后端
LDG_AGI3 小时前
【推荐系统】深度学习训练框架(二十二):PyTorch2.5 + TorchRec1.0超大规模模型分布式推理实战
人工智能·分布式·深度学习
2503_946971863 小时前
【SystemDesign/HA】2025年度高可用分布式仿真节点与预测模型容灾演练配置 (Disaster Recovery Config)
大数据·分布式·算法·系统架构·数据集
linux修理工3 小时前
kafka topic consumer
分布式·kafka·linq