Redis实战 黑马点评(优惠卷秒杀)

1 全局唯一id

每个店铺都可以发布优惠券:

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显

  • 受单表数据量的限制

场景分析:如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。

场景分析二:随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

这几个特性我们可以使用redis中的string类型来实现 redis独立于数据库之外安全性可以,redis客户端可以满足高性能的需求strin类型的方法可以实现递增性,将id保存到redis中可以保证唯一性。

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

ID的组成部分:符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

各种实现:

redis实现:

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

知识小贴士:关于countdownlatch

countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题

我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch

CountDownLatch 中有两个最重要的方法

1、countDown

2、await

await 方法 是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

测试:

利用CountDownLatch 生成300个线程每个线程执行100次获取id的方法 并且打印id并且计算时间

package com.hmdp;

import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.RedsiData;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import javax.annotation.Resource;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SpringBootTest
class HmDianPingApplicationTests {
    @Autowired
    private RedisIdWorker redisIdWorker;
    private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
    void test() throws Exception {
    CountDownLatch latch = new CountDownLatch(300);

    Runnable task = () -> {
        for (int i = 0; i < 100; i++) {
            long id = redisIdWorker.nextId("order");
            System.out.println("id = " + id);
        }
        latch.countDown();
    };
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 300; i++) {
        es.submit(task);
    }
    latch.await();
    long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin));
}
}

  2 添加优惠卷

   每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:

tb_voucher:优惠券的基本信息,优惠金额、使用规则等 tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

平价卷由于优惠力度并不是很大,所以是可以任意领取

而代金券由于优惠力度大,所以像第二种卷,就得限制数量,从表结构上也能看出,特价卷除了具有优惠卷的基本信息以外,还具有库存,抢购时间,结束时间等等字段

新增普通卷代码: VoucherController

t添加完成把数据返会给前端,方便前端做出展示,支付等动作

@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {
    voucherService.save(voucher);
    return Result.ok(voucher.getId());
}

新增秒杀卷代码:VoucherController

@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {
    voucherService.addSeckillVoucher(voucher);
    return Result.ok(voucher.getId());
}

VoucherServiceImpl

@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    // 保存秒杀库存到Redis中
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

3 实现秒杀优惠券下单的功能

下单核心思路:当我们点击抢购时,会触发右侧的请求,我们只需要编写对应的controller即可

秒杀下单应该思考的内容:

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单

  • 库存是否充足,不足则无法下单

逻辑分析:

优惠券下单核心逻辑分析:

当用户开始进行下单(带着之前添加完成的优惠券返回的id来进行购买),我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件

比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

service层的代码:

 @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    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("库存不足!");
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
            //扣减库存
            return Result.fail("库存不足!");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        //订单写进数据库
        save(voucherOrder);

        return Result.ok(orderId);
    }

4 超卖问题

 多线程并发的去访问购买秒杀优惠券的接口,很容易发生库存出现负数的现象。也就是超卖的问题

原因:

        在多线程并发的访问接口时当线程一在执行判断库存的问题通过还没来的及时进行库存的扣减时线程二也通过了库存的判断在平时可能每什么问题但是当这个库存的数值为1 时问题就出现了

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:而对于加锁,我们通常有两种解决方案:见下图:

乐观锁的核心:

        在每次修改之前都判断一下数据是否被修改过

    版本号法:

        给每一次操作都加上一个版本号,在我们每一次操作之前都要查询出版本号和库存,然后判断库存是否大于0,如果大于0就根据我们查询得到的版本号和库存去在数据库里面查询能得到结果才进行库存减一和版本号加一的操作

        cas法:

                在版本号法的基础上做了一个简化,直接比较库存是否发生了变化

代码:

修改代码方案一、

VoucherOrderServiceImpl 在扣减库存时,改为:

boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1") //set stock = stock -1
            .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ?

以上逻辑的核心含义是:只要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败  

5 实现一人一单的功能:

初级逻辑:(单体使用)

在扣除库存前加一步操作: 判断用户是否已经购买过(根据用户id查询用户是否存在订单)

// 5.一人一单逻辑
    // 5.1.用户id
    Long userId = UserHolder.getUser().getId();
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    // 5.2.判断是否存在
    if (count > 0) {
        // 用户已经购买过了
        return Result.fail("用户已经购买过一次!");
    }

问题来了在测试中同时使用同一个tooken并发的法请求还是可以获取多个订单

原因:

        多线程并发的执行多个线程穿插执行,当你访问此时判断可以购买还没有在数据库里更改,这时当另一个线程来访问来是可以购买 (判断直到同步到数据库这个过程多线程并发执行出现问题)

解决:       

        更新数据用乐观锁,这个是插入数据加一个悲观锁更好用

判断完成直到同步到数据库这个过程加悲观锁(如果只是给判断的这个过程加锁,当你同步到数据库之前多个线程并发的访问还是会出现这个问题)

 加锁每一个用户用一个锁:

synchronized (voucherId.toString().intern())

        但是以上代码还是存在问题,问题的原因在于当前方法被spring的事务控制,如果你在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放也会导致问题,所以我们选择将当前方法整体包裹起来(将整个逻辑封装成一个方法在方法的调用出加锁),确保事务不会出现问题:如下:

在seckillVoucher 方法中,添加以下逻辑,这样就能保证事务的特性,同时也控制了锁的粒度

 synchronized (voucherId.toString().intern()) {
            return createVoucherOrder(voucherId, voucher);
        }
 @Transactional
    public Result createVoucherOrder(Long voucherId, SeckillVoucher voucher) {


            // 5.一人一单逻辑
            // 5.1.用户id
            Long userId = UserHolder.getUser().getId();
            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("stock", voucher.getStock())
                    .eq("voucher_id", voucherId).update();
            if (!success) {
                //扣减库存
                return Result.fail("库存不足!");
            }
            //6.创建订单
            VoucherOrder voucherOrder = new VoucherOrder();
            // 6.1.订单id
            long orderId = redisIdWorker.nextId("order");
            voucherOrder.setId(orderId);
            // 6.2.用户id
            // Long userId = UserHolder.getUser().getId();
            voucherOrder.setUserId(userId);
            // 6.3.代金券id
            voucherOrder.setVoucherId(voucherId);
            //订单写进数据库
            save(voucherOrder);

            return Result.ok(orderId);
        }

         但是以上做法依然有问题,因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效(使用事务注解利用的是动态代理拿到当前类的代理对象),所以这个地方,我们需要获得原始的事务对象, 来操作事务

 synchronized (voucherId.toString().intern()) {
            //拿到当前的代理对象
            IVoucherOrderService o = (IVoucherOrderService) AopContext.currentProxy();
            return o.createVoucherOrder(voucherId, voucher);
        }

 添加依赖:

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>

在启动类添加配置:

@EnableAspectJAutoProxy(exposeProxy = true)

集群下的多线程的并发安全问题

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

1、我们将服务启动两份,端口分别为8081和8082:

2、然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:

有关锁失效原因分析(锁的原理是在jvm里维护了一个锁的监视器储存到jvm的常量池中)

        由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

 分布式锁解决

        基本原理和实现方式对比

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

        分布式锁的核心思想就是让大家都使用同一把锁(集群中所有的线程共用),只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

那么分布式锁他应该满足一些什么样的条件呢?

可见性:多个线程都能看到相同的结果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个进程之间都能感知到变化的意思

互斥:互斥是分布式锁的最基本的条件,使得程序串行执行

高可用:程序不易崩溃,时时刻刻都保证较高的可用性

高性能:由于加锁本身就让性能降低,所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能

安全性:安全也是程序中必不可少的一环

常见的分布式锁有三种

Mysql:mysql本身就带有锁机制,但是由于mysql性能本身一般,所以采用分布式锁的情况下,其实使用mysql作为分布式锁比较少见

Redis:redis作为分布式锁是非常常见的一种使用方式,现在企业级开发中基本都使用redis或者zookeeper作为分布式锁,利用setnx这个方法,如果插入key成功,则表示获得到了锁,如果有人插入成功,其他人插入失败则表示无法获得到锁,利用这套逻辑来实现分布式锁

Zookeeper:zookeeper也是企业级开发中较好的一个实现分布式锁的方案,由于本套视频并不讲解zookeeper的原理和分布式锁的实现,所以不过多阐述

Redis分布式锁的实现核心思路

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

  • 获取锁:(jdk提供)

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

    • 非阻塞:尝试一次,成功返回true,失败返回false(推荐使用)

  • 释放锁:

    • 手动释放

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

逻辑思路:

代码实现:

锁的接口:

        

public interface ILock {
    //获取锁
    boolean tryLock(long timeSec);

    //释放锁
    void unlock();
}

锁的实现类:

public class RedisLock implements ILock{
    //这两个由new一个类来调用方法来传递过来
    //不同的业务不可能公用一个锁 所以锁的名字由调用者传入
    private static final String KEY_PREFIX="lock:";
    private StringRedisTemplate stringRedisTemplate;
    private String name;

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

    @Override
    public boolean tryLock(long timeSec) {
        // 获取线程标示
        Long threadId = Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

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

代码更改:

   RedisLock lock =new RedisLock(stringRedisTemplate, "order"+voucherId);
        boolean lock1 = lock.tryLock(2000);
        if (!lock1){
            return Result.fail("不允许重复下单");
        }
        //拿到当前的代理对象(事务)
        try {
            IVoucherOrderService o = (IVoucherOrderService) AopContext.currentProxy();
            return  o.createVoucherOrder(voucherId, voucher);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            //手动释放这里try catch一下 避免方法内部出现异常无法释放
            lock.unlock();
            //释放成功 其实finally 最后都会执行但是cry catch finally 需要return 一下 这个是没必要的
            return Result.ok();
        }

Redis分布式锁误删情况说明

逻辑说明: 

持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除这是再来个线程3来判断获取锁就可以获取成功这就导致了多线程并发执行的情况。失去了锁的作用

解决Redis分布式锁误删问题

解决方案:

        解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果属于自己,则不进行锁的删除,假设还是上边的情况,线程1卡顿,锁自动释放,线程2进入到锁的内部执行逻辑,此时线程1反应过来,然后删除锁,但是线程1,一看当前这把锁不是属于自己,于是不进行删除锁逻辑,当线程2走到删除锁逻辑时,如果没有卡过自动释放锁的时间点,则判断当前这把锁是属于自己的,于是删除这把锁。

逻辑思路: 

集群环境下存在多台服务器存在的多个JVM可能村存在线程id重复的场景这是采用线程的id作为判断的条件(之前使用线程id只是作为锁在redis中存储的value没有影响)这里使用uuid和线程的id拼接起来作为判断的条件

改进后代码:

        


//这中类类不用交给spring管理 安全
public class RedisLock implements ILock{

    private static final String KEY_PREFIX="lock:";
    private StringRedisTemplate stringRedisTemplate;
    private String name;
    private  static final String ID_PREFIX= UUID.randomUUID(true)+"-";

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

    @Override
    public boolean tryLock(long timeSec) {

        // 获取线程标示  拼接 作为value存入
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId , timeSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 获取线程标示  拼接
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        //获取redis中锁的value
        String suo = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //通过del删除锁
        if (suo.equals(threadId)){
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

 在我们修改完此处代码后,我们重启工程,然后启动两个线程,第一个线程持有锁后,手动释放锁,第二个线程 此时进入到锁内部,再放行第一个线程,此时第一个线程由于锁的value值并非是自己,所以不能释放锁,也就无法删除别人的锁,此时第二个线程能够正确释放锁,通过这个案例初步说明我们解决了锁误删的问题。

分布式锁的原子性问题

更为极端的误删逻辑说明:

        线程1现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生,

Lua脚本解决多条命令原子性问题

 Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程,这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,又能保证他的原子性,这样就可以实现拿锁比锁删锁是一个原子性动作了。

    Redis提供的调用函数 :

redis.call('命令名称', 'key', '其它参数', ...)

执行set name jack

# 执行 set name jack
redis.call('set', 'name', 'jack')

先执行set name Rose,再执行get name

# 先执行 set name jack
redis.call('set', 'name', 'Rose')
# 再执行 get name
local name = redis.call('get', 'name')
# 返回
return name

用Redis命令来调用脚本

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

 

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

使用Lua脚本操作锁的逻辑逻辑:

释放锁的业务流程是这样的

1、获取锁中的线程标示

2、判断是否与指定的标示(当前线程标示)一致

3、如果一致则释放锁(删除)

4、如果不一致则什么都不做

如果用Lua脚本来表示则是这样的:

最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

利用Java代码调用Lua脚本改造分布式锁(解决了超时释放的误删问题)

        可以利用RedisTemplate 中的execute方法去执行lua脚本 来执行Lua脚本来操作redis锁的比较和删除实现了原子性

 代码实现:

ulock.lua:(放在资源目录下)

-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

加载脚本: 

 //提前加载.lua中的脚本防止调用一次加载一次导致io的浪费
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("ulock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

 调用脚本来比较和删除锁:

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

小总结:

基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示

  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

    • 特性:

      • 利用set nx满足互斥性

      • 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性

      • 利用Redis集群保证高可用和高并发特性

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值