Redis分布式锁的个人实现(二)


前言

当我们有了前一篇的经验以后继续说说我们写锁的时候应该注意的细节
1独占排他锁
2防止死锁
3防止误删
4原子性
5可重入
6自动续期
当然我们还得了解一下redis的数据类型Hset


提示:以下是本篇文章正文内容,下面案例可供参考

一、Hset?

1redis的一种数据结构其结构为
在这里插入图片描述
在这里插入图片描述
可以看到他的数据结构类似于Map<String,Map<String,String>>这种,为什么我们选择这种呢,分布式锁的应用即为高并发,分布式的情况下进行使用,比如我们同一时刻只有被抢到锁的处理并减库存,我们给他加一个key为lock,value为uid的锁,这时我们知道了这个唯一商品现在是被谁秒杀到了进行程序的处理,当然现实我们不可能这么简单要考虑缓存,并发数量,消峰,等等用到消息列队,这我们只做个示例,好了现在知道比如这个叫lock锁的被这个uid抢到了,在除了抢物品的同时我们还要做减库存操作,这是加入调用其他方法我们还要能够重入锁保证串行执行那么我们的hset就能够使用了 lock->uid,1,初始重入次数为1 直到扣减为0

二、加锁流程

//是否存在锁,不存在直接获取锁(抢到锁),如果存在看看是不是当前线程uid的锁,是的话value+1重入,否则获取锁失败,流程很简单这边我的uid用uuid实现大家也可以雪花算法或者其他方式实现,只要保证分布式下的uid唯一就行,我们建立一个类似工厂结构的类需要什么锁就获取什么所

@Component
public class LockClient {

    @Autowired
    private StringRedisTemplate redisTemplate;
    public RedisRLock getRedisLock(String lockName,String uuid){
        return new RedisRLock(lockName,uuid,redisTemplate);
    }

    public RedisRLock getZkLock(String lockName,String uuid){
        return new getZkLock(lockName);
    }
}


import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class RedisRLock implements Lock {
    private String lockName;

    private StringRedisTemplate redisTemplate;
    private String uuid;
    private long expire = 30L;
    RedisRLock(){

    }
    RedisRLock(String lockName,String uuid, StringRedisTemplate redisTemplate){
        this.lockName =lockName;
        this.uuid = uuid;
        this.redisTemplate = redisTemplate;
    }


    @Override
    public void lock() {

        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(expire, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {

        String script = "if redis.call('exists',KEYS[1])== 0 or redis.call('hexists',KEYS[1],ARGV[1]) ==1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end ";
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(time))){
            Thread.sleep(50);
        }
        expireLock();
        return true;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('hexists',KEYS[1],ARGV[1])==0 then return nil elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then return redis.call('del',KEYS[1]) else return 0 end";
        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(lockName),uuid);
        if (flag == null){
            throw new IllegalMonitorStateException();
        }
    }

    //续期每隔一段时间访问一下 进行续期
    private void expireLock(){
        String script="if redis.call('hexists',KEYS[1],ARGV[1])==1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if(redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),Arrays.asList(lockName),uuid,String.valueOf(expire))){
                    expireLock();
                }
            }
        },expire*1000/3);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

RedisRLock implements Lock 我们实现锁的加锁和解锁方法

        String script = "if redis.call('exists',KEYS[1])== 0 or redis.call('hexists',KEYS[1],ARGV[1]) ==1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end ";
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(time))){
            Thread.sleep(50);
        }
        expireLock();
        return true;

我们先读一下这lua脚本判断如果锁不存在我们可以加锁或者呢这个锁存在了我们要进行重入所以这里就用了或成立我们就调用
redis.call(‘hincrby’,KEYS[1],ARGV[1],1)
这个的意思就是自增如果没锁就是 lock uuid 1如果再次重入比如我第二次同一个线程获取锁那么就会自增1变成 lock uuid 2
这里的keys[1] Arrays.asList(lockName) 锁的名称
ARGV我传了2个 一个是uuid 标记某个线程 另一个是 超时时间 现在我们的加锁满足了1独占排他锁,可重入,防止没有过期时间引发的死锁和加锁并设置过期时间的原子性
1加锁成功走业务流程和expireLock();这里的expireLock();我们下面再讲
2如果失败我们sleep让出过一会在继续尝试

三、解锁流程

   @Override
    public void unlock() {
        String script = "if redis.call('hexists',KEYS[1],ARGV[1])==0 then return nil elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)==0 then return redis.call('del',KEYS[1]) else return 0 end";
        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(lockName),uuid);
        if (flag == null){
            throw new IllegalMonitorStateException();
        }
    }

这个也很好理解
1看看这个要解锁的线程锁还在不 如果不在的话我们和ReentrantLock一样抛出异常
2如果在的话还是自己线程的锁我们重入次数减一后判断是否为0 如果为0我们就删除锁 说明重入锁全部解完
3否则我们return 0 不报错表示已经解了一次该做啥做啥
这里我们同样做到防误删,原子性等,当然在集群当中我们主io还没有写入从redis就挂了这种问题我会单独对redis的红锁以及后面的redission提出一篇文章

四、自动续期

当然使我们老生常谈的看门狗机制了,就是找一个线程我们专门去处理过期时间,建议线程池,有些
业务因为种种原因我们的过期时间都结束了,代码块还没有执行完,会导致,其他线程来立刻获取到锁,虽然我们解决了误删的问题但是这种锁自动过期的问题也难以忍受

    private void expireLock(){
        String script="if redis.call('hexists',KEYS[1],ARGV[1])==1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if(redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class),Arrays.asList(lockName),uuid,String.valueOf(expire))){
                    expireLock();
                }
            }
        },expire*1000/3);
    }

1每隔锁的2/3过期时间看看锁是不是存在,存在说明这个还在使用但是业务没有处理完,起来继续重置过期时间,然后再把这个方法递归调用一下,直到下次调用时,发现锁不存在了 那好吧说明锁都没了我们就不递归调用了等待线程cg

我们举个栗子测试一下 设置一个key为stock的10000件商品
在这里插入图片描述
在这里插入图片描述

java开了2个服务通过负载均衡打过来做压力测试
 @SneakyThrows
 @Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
    @GetMapping("/lock")
    private void lockTest(){
        String uuid = UUID.randomUUID().toString();
        RedisRLock redisRLock = lockClient.getRedisLock("lock",uuid);
        redisRLock.lock();
        try {
            String stock = stringRedisTemplate.opsForValue().get("stock");
            if (!StringUtils.isEmpty(stock)&&Integer.valueOf(stock)>0){
                Integer st =new Integer(stock);
                stringRedisTemplate.opsForValue().set("stock", String.valueOf(--st));
            }
            test(redisRLock);
        }

        finally {
            redisRLock.unlock();
        }

    }

    @SneakyThrows
    public void test(RedisRLock redisRLock){
        redisRLock.lock();
        System.out.println(Thread.currentThread().getId());
        redisRLock.unlock();
    }

在这里插入图片描述
在这里插入图片描述

总结

本文仅仅简单介绍了redis基于lua的使用,我们实现了分布式锁并且在下不同的服务情况下依旧能实现能实现商品的不超卖

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
网络上有非常多关于分布式锁具体实现的文章和案例,其中基于 Redis 和 Python 的也不少;综合多方意见 我认为 Redis 官方提出的 Redlock 是一种很有意义的分布式锁实现方式,接着参考了 SPSCommerce 团队的实现 编写了你现在看到的这段代码; 在 Redlock 的 Python 范例的基础上,我做了一些小的改动,让它更贴近实际场景; ### 稳定性 RedLock 在理论上有 Redis Redlock 的支撑,同时参考了 SPSCommerce 的具体实现,我认为它是稳定的,完全可以用在生产环境当中; ### 使用方法 近段时间工作忙,还没来得及将它制作成为一个库,大家可以直接把代码复制到项目中使用,文件中有具体的调用方法; 假设你在本机启动了 3 个 Redis 服务,它们分别是 ` localhost 6379丨localhost 6389丨localhost 6399`,那么运行文件时你会在终端看到如下提示: -------- 该资源内项目源码是个人的毕设,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! <项目介绍> 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 --------

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值