1

redis分布式锁实现,使用redis实现分布式的优点:

  1. redis基于内存操作,加锁、加锁非常快
  2. redis中间件非常常用,基于redis实现分布式锁不需要再引入其他中间件
  3. 实现很简单,redis本身就是单线程,不存在数据同步问题

最简单的方式,使用setnx命令实现,缺点是不支持可重入。

使用lua脚本实现分布式锁,lua脚本执行是原子性的,使用hash数据结构实现,不同的线程生成不同的hash表的key,实现锁机制。

lua脚本

加锁的lua代码:

local key=KEYS[1]
local lockKey=ARGV[1]
local expireSecond=ARGV[2]
local exists=redis.call('exists',key)
if exists==0 then
    redis.call('hset',key,lockKey,1)
    redis.call('expire',key,expireSecond)
    return 1
end
local value=redis.call('hget',key,lockKey)
if value then
    redis.call('hincrby',key,lockKey,1)
    redis.call('expire',key,expireSecond)
    return 1
end
return 0

解锁lua脚本:

local key=KEYS[1]
local lockKey=ARGV[1]
local value=redis.call('hget',key,lockKey)
if value then
    if tonumber(value)>1 then
        redis.call('hincrby',key,lockKey,-1)
    else
        redis.call('del',key)
    end
end
return 1

spring中实现

定义锁接口

package top.mybiao.redis.core;

import java.util.concurrent.TimeUnit;

public interface DistributeLock {
    boolean tryLock();

    boolean tryLock( long time, TimeUnit timeUnit);

    void lock();

    void unlock();
}

实现接口:

package top.mybiao.redis.core;

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

import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

public class RedisLockImpl implements DistributeLock{

    private final StringRedisTemplate redisTemplate;

    private final String key;

    private final String lockKey;

    //过期时间30秒
    private static final int TIMEOUT = 30;

    public RedisLockImpl(StringRedisTemplate redisTemplate, String key,String lockKey){
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.lockKey = lockKey;
        System.out.println("初始化lockKey:"+lockKey);
    }
    private static final String lockScript = "" +
            "local key=KEYS[1]\n" +
            "local lockKey=ARGV[1]\n" +
            "local expireSecond=ARGV[2]\n" +
            "local exists=redis.call('exists',key)\n" +
            "if exists==0 then\n" +
            "   redis.call('hset',key,lockKey,1)\n" +
            "   redis.call('expire',key,expireSecond)\n" +
            "   return 1\n" +
            "end\n" +
            "local value=redis.call('hget',key,lockKey)\n" +
            "if value then \n" +
            "   redis.call('hincrby',key,lockKey,1)\n" +
            "   redis.call('expire',key,expireSecond)\n" +
            "   return 1\n" +
            "end\n" +
            "return 0\n";
    private static final String unlockScript = "" +
            "local key=KEYS[1]\n" +
            "local lockKey=ARGV[1]\n"  +
            "local value=redis.call('hget',key,lockKey)\n" +
            "if value then\n" +
            "   if tonumber(value)>1 then\n" +
            "       redis.call('hincrby',key,lockKey,-1)\n" +
            "   else\n" +
            "       redis.call('del',key)\n" +
            "   end\n" +
            "end\n" +
            "return 1";

    private final RedisScript<Long> lockRedisScript = new DefaultRedisScript<>(lockScript,Long.class);
    private final RedisScript<Long> unlockRedisScript = new DefaultRedisScript<>(unlockScript,Long.class);

    @Override
    public boolean tryLock() {
        Long code = redisTemplate.execute(lockRedisScript, Collections.singletonList(key),lockKey,String.valueOf(TIMEOUT));
        Objects.requireNonNull(code);
        return code==1;
    }

    @Override
    public boolean tryLock(long time, TimeUnit timeUnit) {
        if (time<=0) throw new IllegalArgumentException("time must greater than zero");
        long start = System.currentTimeMillis();
        long end = start+timeUnit.toMillis(time);
        try {
            while (start < end) {
                if(tryLock()) return true;
                Thread.sleep(10);
                start = System.currentTimeMillis();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
        return false;
    }

    @Override
    public void lock() {
        try {
            while (!tryLock()) {
                Thread.sleep(30);
            }
            System.out.println("获取到锁");
        }catch (InterruptedException e){
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public void unlock() {
        System.out.println("释放锁");
        redisTemplate.execute(unlockRedisScript,Collections.singletonList(key),lockKey);
    }
}

获取锁对象接口:

package top.mybiao.redis.core;

public interface RedisLockFactory {

    DistributeLock getLock(String key);
}

接口实现:

package top.mybiao.redis.core;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class RedisLockFactoryImpl implements RedisLockFactory{

    private final StringRedisTemplate redisTemplate;

    public RedisLockFactoryImpl(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public DistributeLock getLock(String key) {
        return new RedisLockImpl(redisTemplate,key, UUID.randomUUID().toString());
    }
}

每个锁都要有不同的值,对应redis的key,每个线程对用不同的lock key,表示不同的线程。

使用方法

package top.mybiao.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import top.mybiao.redis.core.DistributeLock;
import top.mybiao.redis.core.RedisLockFactory;

import javax.annotation.PostConstruct;
import java.util.Objects;

@SpringBootApplication
@RestController
public class RedisApplication {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedisLockFactory redisLockFactory;

    private static final Logger log = LoggerFactory.getLogger(RedisApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class, args);
    }

    @PostMapping("/seal")
    public String seal(String name){
        DistributeLock lock = redisLockFactory.getLock("seal");
        try{
            lock.lock();
            log.info("name={},want seal 1 iphone",name);
            String num = redisTemplate.opsForValue().get("iphone");
            Thread.sleep(1000);
            if (Objects.nonNull(num) && Integer.parseInt(num)>0) {
                redisTemplate.opsForValue().increment("iphone", -1);
                log.info("buy one iphone success");
                return "success";
            }
            log.info("库存不足");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return "系统繁忙";
    }


    @PostConstruct
    public void init(){
        redisTemplate.opsForValue().set("iphone","100");
    }

}

image-20210916144557975

总结

之前写的redis分布式锁有明显bug,就是每个线程的lock key相同,同时多个线程获取到锁,达不到锁的要求。本文中这样改进的实现解决了这个bug,并且接口更加易于使用,每次加锁解锁不需要传额外的参数。

这样实现基本满足需求,不会发生死锁,解锁其他线程的锁的情况,但没有实现锁续期,当锁超时时,会有多个线程同时获得锁,进一步可以通过redis键空间通知实现。锁是非公平锁,会发生线程饥饿情况,长时间获取不到锁。


一直,,,
4 声望0 粉丝