【Redis】实现分布式锁初级版本

这节我们先来实现初级版本,也就是说在后续我们会去升级和扩展分布式锁的实现方案,让它变的更加的完善。

一、业务分析

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。

ILock其实就是一个锁接口。

tryLock:尝试获取锁。因为我们采用的是非阻塞,我去获取锁我只试一次,如果成功了,那我就返回true,代表成功;那如果失败了返回false,代表失败。我不会不断重试,也不会阻塞等待。

另外再获取锁的时候还需要指定一个超时时间 timeoutSec,这个就等于 setex

image-20240528102906803

接下来就是根据流程图来实现方法

image-20240528104557196

二、代码实现

首先找到我们要实现的接口,这个已经提前写好了

image-20240528104118777

接下来在utils中定义一个 SimpleRedisLock类,让它去实现ILock。

SimpleRedisLock

首先需要拿到StringRedisTemplate,因为只有拿到了它,我们词啊可以执行这些reids操作。

public class SimpleRedisLock implements ILock{
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public boolean tryLock(long timeoutSec) {
    }

    public void unlock() {
    }
}

另外我们在获取锁和将来释放锁的时候,其实这里都需要指定锁的key。之前我们分析过,锁的名称是不能再这个类中写死的,写死就意味着不管任何一个业务,它来获取锁的时候都是同一把锁,这肯定是不对的。我们将来希望的是不同的业务有不同的锁,因此这个锁的名称应该跟业务有关,因此这个锁的名称我们不应该写死,而是由使用的人传递给我们。

下面代码中 name 就是业务的名称,事实上也是将来我们锁的名称。当然你可以给锁加上一个统一前缀,让它看起来更专业一些。

但是这里的value就比较特殊了,因为这个value值需要加上线程的标识,例如之前我们做测试的时候使用的是 thread1(thread + id)

image-20240528100418138
package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{
    private StringRedisTemplate stringRedisTemplate;
    private String name;

    private static final String KEY_PREFIX = "lock:";

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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        long threadId = Thread.currentThread().getId();
        // 获取锁
        // 可以发现它执行的是Boolean,但是在我们业务逻辑中,我们执行setnx后,返回值是ok和nil,但是我们这里要求的是Boolean。事实上spring在给我们封装函数的时候,它帮我们对结果做了判断,直接返回布尔值。
        Boolean success = stringRedisTemplate.opsForValue()
            // 不过由于获取到的线程id是long,这里直接""拼一下即可
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // 因此我们这里直接返回success即可,但是返回值是boolean类型,因此返回的时候会有一个自动拆箱的过程,有自动拆箱就有可能会有空指针安全风险
        // Boolean.TRUE:是个常量,它肯定不是null,调用它的equals方法,如果你是true,返回true;如果你是false,返回false;如果是你null,它返回的也是false,这样就可以避免空指针风险了
        return Boolean.TRUE.equals(success);
    }

    // 释放锁
    public void unlock() {
        //通过del删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

三、修改业务代码

之前我们是采用synchronized做锁

image-20240528161825746

现在我们不这么做了,我们需要自己来创建锁对象,自己加锁了。

  @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();
        //创建锁对象(新增代码)
        // 这里需要注意锁的范围,如果你这么写,那就表示:凡是来下单的业务都会被锁定。但事实上我们锁定的范围应该是用户,同一个用户我们才要加限制,不同用户无所谓
        //  new SimpleRedisLock("order"
        // 因此这里锁的范围应该是用户
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁对象,这里传入的锁超时时间跟业务执行的时间有关,例如我们这个下单业务耗时大概是500ms,那么这个地方的超时时间就可以设置为5s,如果你执行超时,再长也不可能超过5s,但是由于这里我们代码要做测试,断点什么的都耗费时间比较长,因此这里给个长一点的,例如1200秒
        boolean isLock = lock.tryLock(1200); // 由于这里查到不一定成功,因此需要判断
		//加锁失败
        if (!isLock) {
            // 获取锁失败,解决办法一般是返回错误信息或重试。但这里我们是为了一个用户避免重复下单,因此直接返回错误信息即可
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 由于上面代码不管会不会产生异常,都需要释放锁,因此放到finally中做
            lock.unlock();
        }
    }

四、测试

在方法中打一个断点,然后重启代码

image-20240528163527433

同样我们会启动两个tomcat,模拟两台服务器。在之前的测试中因为这两个是独立的进程,因此每个进程内都会有自己的一个锁对象,这样就导致当我们并发的去访问时,两个进程就会有两把锁,以至于会出现并发安全问题。

现在我们来测试一下我们一旦使用了这种自定义的redis锁,它还会不会有这种并发的安全漏洞。

首先将数据库库存恢复为100,然后删掉所有订单

接下来依旧用Postman来发请求,发送两个同样的请求。

image-20240528164118375

回到IDEA,可以发现两个服务都进断点了,但是你会发现 8082 得到的 isLocktrue

image-20240528164437719

8081isLockfalse

image-20240528164456357

那就证明现在其实只有一个人获取锁成功了,这是因为我们虽然有两个进程,但是它们都是去同一台redis机器上获取锁,而且获取锁的名字是一样的。

此时我们到redis中刷新一下,可以看见它获取锁的时候会记录用户id1010,表示是1010这个用户来的,然后value记录的是当前线程。

因此由于是同一个用户来争抢,所以只能有一个线程拿到锁,另外一个线程是拿不到的。

image-20240528164641494

这就是redis分布式锁实现的一个思路,只要锁只有一把,就不会出现同时执行的情况。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值