Redis 实现分布式锁

为什么要用分布式锁

假设你现在正在浏览你们学校的二手物品交易网站,你看到了大四学长正在拍卖的一本全新的《Java 编程思想》,你心动了,下单,支付,库存减 1,等待收货。但是,你有没有考虑这样一个问题,你隔壁寝室的哥们也看中了这本《Java 编程思想》,他和你执行着一样的流程,你俩同时完成支付,假如你们这个网站的开发人员没有做并发处理,让你们两个都买到了这本书,但总共只有一本,那这本书到底该归谁。当然,这个假设发生的概率极低。如果要彻底避免这样的情况,开发人员只要在减少库存对应的代码块上加一个 synchronized 关键字,加一个锁,这块代码同时只有一个线程可以访问,那便可解决这个问题。

对于学校内部,这种流量小,并发量低的单体应用,这样操作无疑可以解决问题。但是,对于像淘宝,京东这类大型的电商网站,它们采用分布式的系统架构,尤其在双 11,618 这种购物狂欢节,如此高并发的情况下,单单对代码加一个同步锁是绝对不能解决问题的,它们要保证整个分布式系统的数据一致性,我们的分布式锁应运而生。

什么是分布式锁

分布式锁也是一把锁,用来锁住共享资源的,比如库存这一资源,大型电商网站,单单处理库存这一个服务就会部署到成千上万台机器上,库存就属于共享资源,我们用分布式锁来保证共享资源的一致性。简单地说,就是一个线程开始执行处理库存的代码后,他就会持有一把锁,关起门来,自己偷偷处理,处理完后,开锁,走人,也就是释放锁,释放了的锁可以被其他线程持有,这把锁就是分布式锁,这样就保证了在同一时间,只有一个线程可以操作库存,保证了分布式系统的数据一致性。

怎样实现分布式锁

实现分布式锁的方式有很多,MySQL、ZooKeeper、Redis 等都能实现分布式锁,本文来探讨一下如何用 Redis 实现分布式锁。

分布式锁实现的思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试。

在 Redis 中,占位一般使用 setnx 指令,先进来的线程先占位,线程的操作执行完成后,再调用 del 指令释放位置。同时为了防止死锁,我们一般还要给锁加一个过期时间,到期了自动释放。

下面看第一种实现方式

@GetMapping("/deduct_Stock")
public String deductStock() {
    // 给每一把锁分配不同的值
    String clientId = UUID.randomUUID().toString();
    try {
        // 获得锁
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", clientId, 10, TimeUnit.SECONDS);
        // 业务代码
        if (!result) {
            return "error";
        }
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + stringRedisTemplate.opsForValue().get("stock"));
        } else {
            System.out.println("扣减失败,库存不足");
        }
    } finally {
        // 释放锁
        if (clientId.equals(stringRedisTemplate.opsForValue().get("lockKey"))) {
            stringRedisTemplate.delete("lockKey");
        }
    }
    return "success";
}

Spring Boot 为我们提供了 StringRedisTemplate 来操作 Redis,StringRedisTemplate 为我们提供的opsForValue().setIfAbsent() 方法就相当于 Redis 的 setnx 命令,表示当 key 不存在的时候进行 set。

当线程执行到 deductStock() 方法的时候,首先会获得一把锁,业务执行完毕之后要释放锁,因为当业务执行过程中可能会抛出异常,所以将删除锁的操作放在 finally 中。

因为我们设置锁的时间是 10 s,如果业务执行之间大于 10 s,锁就会失效,第二个线程会获得锁,这是为了防止第一个线程执行结束的时候误删第二个线程的锁,就需要在删除的时候进行判断,我们给每一把锁设置一个 UUID 的值,这样可以做到在删除的时候判断是否是自己的锁。

上面的代码写着还是蛮长的,那么有没有简单一点的办法呢?当然是有的!那就是 Redisson。Redisson 对 Redis 请求做了较多的封装,对于锁,也提供了对应的方法可以直接使用。

第二种实现方式

@GetMapping("/deduct_Stock")
public String deductStock() {
    Config config = new Config();
    // 配置 Redis 基本连接信息
    config.useSingleServer().setAddress("redis://47.104.218.84:6379");
    // 获取一个 RedissonClient 对象
    RedissonClient redisson = Redisson.create(config);
    // 获取一个锁对象实例
    RLock redissonLock = redisson.getLock("lockKey");
    try {
        // 设置超时时间
        redissonLock.lock(30, TimeUnit.SECONDS);
        // 业务代码
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + stringRedisTemplate.opsForValue().get("stock"));
        } else {
            System.out.println("扣减失败,库存不足");
        }
    } finally {
        // 释放锁
        redissonLock.unlock();
    }
    return "success";
}

Redisson 分布式锁实现原理

在这里插入图片描述

从图中我们可以看到,一个线程获得锁之后,会开启一个后台线程,每 10 s 检查是否还持有锁,如果持有则延长锁的时间。当线程从 Master 获得锁之后,Master 需要将锁的信息同步到 Slave 上,如果在同步的过程中 Master down 掉,Slave 重新选举新的 Master,新选举的 Master 中没有锁的信息,所以用 Redis 做分布式锁天生性能是不高的。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值