RedisSon分布式锁 源码解析,在 java 中使用 redis + lua 做秒杀

1. RedisSon 分布式锁

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>
spring:
  profiles:
    active: dev
  redis:
    cluster:
      nodes: 192.168.0.150:6379,192.168.0.151:6379,192.168.0.152:6379,192.168.0.153:6379,192.168.0.154:6379,192.168.0.155:6379
@Autowired
private RedisProperties redisProperties;

@Bean
public Redisson redisson(){
    
    
    Config config = new Config();
    List<String> nodes = redisProperties.getCluster().getNodes();
    List<String> nodeList = new ArrayList<>();
    for (String node : nodes) {
    
    
        nodeList.add("redis://"+node);
    }
    config.useClusterServers().setNodeAddresses(nodeList);
    return (Redisson) Redisson.create(config);
}

1.1 加锁

在这里插入图片描述

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    
    
   //获取线程id
    long threadId = Thread.currentThread().getId();
    //获取锁,有就加锁,有自己的锁就重入,value值+1
    //这个方法在下边文章 1.1.1中会有解析
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // 如果返回null 表示加锁成功,锁续命成功
    if (ttl == null) {
    
    
        return;
    }
	//订阅线程id
    CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
    RedissonLockEntry entry;
    if (interruptibly) {
    
    
        entry = commandExecutor.getInterrupted(future);
    } else {
    
    
        entry = commandExecutor.get(future);
    }

    try {
    
    
       //不断循环来获取锁
        while (true) {
    
    
            ttl = tryAcquire(-1, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
    
    
                break;
            }
            if (ttl >= 0) {
    
    
                try {
    
    
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
    
    
                    if (interruptibly) {
    
    
                        throw e;
                    }
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
    
    
                if (interruptibly) {
    
    
                    entry.getLatch().acquire();
                } else {
    
    
                    entry.getLatch().acquireUninterruptibly();
                }
            }
        }
    } finally {
    
    
        unsubscribe(entry, threadId);
    }
}

1.1.1 尝试获取锁

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    
    
    RFuture<Long> ttlRemainingFuture;
    //锁过期时间,如果是-1 采用看门狗的 30*1000 的时间,在 1.1.1.1 中有截图
    if (leaseTime != -1) {
    
    
        //尝试获取锁方法,在1.1.1.2部分中解析
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
    
    
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
    
    
        // lock acquired
        if (ttlRemaining == null) {
    
    
            if (leaseTime != -1) {
    
    
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
    
    
                //增加调度任务,来给锁续命
                //在1.1.1.3 部分解析
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

1.1.1.1 默认 看门狗时间 30*1000

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

1.1.1.2 尝试获取锁

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    
    
     return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
             //进行判断是否存在这个key,如果不存在返回0
             "if (redis.call('exists', KEYS[1]) == 0) then " +
             //设置类型为hashmap类型,查询的key为第一个key(KEYS[1]),hashmap的key是线程id(ARGV[2]),值为1
             "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
             //设置过期时间
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +
             "return nil; " +
             "end; " +
             //通过key还有线程id查询如果有 返回 1
             "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
             //在原基础上在加1,处理重入锁问题
             "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
             //设置过期时间
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +
             "return nil; " +
             "end; " +
             //返回获取的信息,如果是  -1表示没有设置过期时间,-2表示没有这个key,大于0表示剩余的过期时间(单位毫秒)
             "return redis.call('pttl', KEYS[1]);",
             //第一个key                              //第一个参数                   //第二个参数     
             Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
 }

1.1.1.3 锁续命

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

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    
    
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            //判断这个key 和 线程id存不存在,存在返回 1
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
            //重新设置过期时间
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +
             "return 1; " +
             "end; " +
             "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

1.2 解锁

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

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    
    
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            //判断有没有这个key 和 线程id的hashmap ,如果是0 表示没有
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
            "return nil;" +
            "end; " +
            //hashmap的这个key和线程id的值 减1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            //如果还大于0,说明重入锁,没有完全解锁
            "if (counter > 0) then " +
            //设置过期时间
            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "return 0; " +
            "else " +
            //删除这个key
            "redis.call('del', KEYS[1]); " +
            //发送给其他争抢锁的线程,告诉他们 可以继续争抢锁了
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; " +
            "end; " +
            "return nil;",
            Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

2. java项目中 实现redis+lua

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>

2.1 请注意 在lua中存在两个重要的形参 KEYS 、ARGV

类型 描述
nil 无效值,类似于java的null,在条件表达式中表示false
boolean false或true
number 双精度浮点型
string 字符串,有单引号或双引号括起来
function 函数,类似于linux的函数一样
table 1.数组类型 2.json类型
local name = KEYS[1] -- 第一个key
local value = ARGV[1] -- 第一个参数
if tonumber(redis.call('EXISTS',name)) > 0 then -- 是否存在这个key
else
redis.call('SET',name,value);  -- 不存在 就在创建一个
end;
return redis.call('GET',name);  -- 返回这个key的value

用于java中表示为

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

 @GetMapping("/saveRedis")
 public String saveRedis(){
    
    
     StringBuilder builder = new StringBuilder();
     builder.append(" local name = KEYS[1] ");
     builder.append(" local value = ARGV[1] ");
     builder.append(" if tonumber(redis.call('EXISTS',name)) > 0 then ");
     builder.append(" else   ");
     builder.append(" redis.call('SET',name,value);   ");
     builder.append(" end; ");
     builder.append(" return redis.call('GET',name); ");
     RedisScript<String> script = RedisScript.of(builder.toString(), String.class);
     String string = UUID.randomUUID().toString();
     String result = stringRedisTemplate.execute(script, Arrays.asList("b"), string);
     return result;
 }

执行接口,发现已经在redis生成了一个 key为 aaa的数据
在这里插入图片描述
在这里插入图片描述

2.2 模拟秒杀,扣减redis中的库存

不包含其他业务,仅用作讲述lua脚本如何编写

@GetMapping("/seckill")
public String seckill(String shopName,String userName){
    
    
    StringBuilder builder = new StringBuilder();
    builder.append(" local shopName = KEYS[1] ");//商品名称
    builder.append(" local shopUser = shopName .. 'User' ");//参与秒杀的用户 集合key
    builder.append(" local userName = ARGV[1] ");//参与秒杀的用户
    //判断如果没有商品秒杀的数量,就初始化一个数量为100的,这一步为了省事才这么做,生产环境不要这样写,切记、切记!
    builder.append(" if (not redis.call('GET',shopName)) then  ");
    builder.append("    redis.call('SET',shopName,100);  ");
    builder.append(" end;   ");
    //创建一个函数,用来秒杀商品数量减1,参数秒杀用户集合增加
    builder.append(" local function seckill(shopUser,userName)   ");
    builder.append("    redis.call('SADD',shopUser,userName);   ");
    builder.append("    redis.call('DECRBY',shopName,1);   ");
    builder.append(" end;   ");
    //判断如果 秒杀的用户集合没有就调用函数创建一下
    builder.append(" if tonumber(redis.call('EXISTS',shopUser)) == 0 then   ");
    builder.append("    if tonumber(redis.call('GET',shopName)) > 0 then ");
    builder.append("       seckill(shopUser,userName)   ");
    builder.append("       return 0 ");
    builder.append("    end;   ");
    builder.append(" end;   ");
    //当前用户已秒杀过,则返回1,提示不允许重复秒杀
    builder.append(" if tonumber(redis.call('SISMEMBER',shopUser,userName)) > 0 then ");
    builder.append("    return 1   ");
    builder.append(" end;   ");
    //如果剩余商品数量大于0,就扣减库存,存储秒杀用户
    builder.append(" if tonumber(redis.call('GET',shopName)) > 0 then ");
    builder.append("    seckill(shopUser,userName)   ");
    builder.append("    return 0 ");
    builder.append(" end; ");
    builder.append(" return redis.call('GET',shopName); ");
    RedisScript<Long> script = RedisScript.of(builder.toString(), Long.class);
    String string = UUID.randomUUID().toString();
    Long result = stringRedisTemplate.execute(script, Arrays.asList(shopName), userName);
    if(result == 1){
    
    
        return "请勿重复下单";
    }else {
    
    
        return "秒杀成功";
    }
}

2.3 测试脚本

2.3.1 调用秒杀接口
秒杀成功,商品数量减1,用户秒杀集合增加当前用户

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2.3.2 重复秒杀 直接返回失败
在这里插入图片描述
秒杀失败后,库存不变
在这里插入图片描述
在这里插入图片描述

2.3 集群情况下执行lua脚本

注意点 :
1.上边的脚本在单机情况下是可以正常执行的,如果在集群情况下会产生报错 EvalSha is not supported in cluster environment

2.集群情况下 不同的key 会分配到不同的槽内,会产生报错 No way to dispatch this command to Redis Cluster because keys have different

//整体逻辑是如果hash不存在就加1 并设置key过期时间,如果存在就重新设置过期时间
StringBuilder builder = new StringBuilder();
builder.append(" if tonumber(redis.call('EXISTS',KEYS[1])) < 1 then ");
builder.append("    redis.call('HINCRBY',KEYS[1],KEYS[2],1)  ");
builder.append("    redis.call('EXPIRE',KEYS[1],ARGV[1])  ");
builder.append("    return 0  ");
builder.append(" else  ");
builder.append("    redis.call('EXPIRE',KEYS[1],ARGV[1])  ");
builder.append("    return 1  ");
builder.append(" end  ");
//脚本的 key 使用  {前缀标识} 用来存放在一个槽里边
List<String> keys = Arrays.asList("{pre}iphone","{pre}"+Thread.currentThread().getName());
//参数信息
List<String> argv = Arrays.asList("300");
//在集群情况下 使用这种方式 执行脚本
redisTemplate.execute(new RedisCallback<Long>() {
    
    
	@Override
	public Long doInRedis(RedisConnection connection) throws DataAccessException {
    
    
		JedisCluster jedisCluster = (JedisCluster) connection.getNativeConnection();
		return (Long)jedisCluster.eval(text,keys,argv);
	}
});

这里的hash槽统一前缀使用了 aaa ,执行以上脚本成功
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_47752736/article/details/128374633