使用Redis实现分布式锁

1. 实现一个基础的分布式锁

1.1 定义Lock接口

public interface Lock {
    boolean tryLock(long timeout);
    boolean unLock();

1.2 实现接口

public class LockImpl implements Lock{
    private static final String defaultNamePre = "sanjin:lock:";
    private String lockName;
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 因为LockImpl不是bean不会注入redistemplate, 需要手动传
    public LockImpl(String name, StringRedisTemplate stringRedisTemplate) {
        this.lockName = name;
        this.redisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeout) {
        String key = defaultNamePre + lockName;
        Boolean b = redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, TimeUnit.SECONDS);
        return b;
    }

    @Override
    public boolean unLock() {
        String key = defaultNamePre + lockName;
        Boolean b = redisTemplate.delete(key);
        return b;
    }
}

1.3 测试

@SpringBootTest
public class TestA {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void test1() {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        // 每次测试都先把锁删一下
        stringRedisTemplate.delete("sanjin");

        // 线程A
        new Thread(() -> {
            LockImpl lock = new LockImpl("sanjin", stringRedisTemplate);
            boolean b = lock.tryLock(120);
            while (!b) {
                System.out.println("A线程再次尝试获取锁");
                b = lock.tryLock(120);
                try {Thread.sleep(1000);} catch (Exception e) {System.out.println(e.getMessage());}
            }
            System.out.println("A线程获取到锁");
            System.out.println("A线程开始执行任务...");
            // 模拟执行任务
            try {Thread.sleep(3000);} catch (Exception e) {System.out.println(e.getMessage());}
            System.out.println("A线程任务执行完毕");
            boolean b1 = lock.unLock();
            if (b1) System.out.println("A线程成功释放锁");
            countDownLatch.countDown();
        }, "threadA").start();

        try {Thread.sleep(1000);} catch (Exception e) {System.out.println(e.getMessage());}

        // 线程B
        new Thread(() -> {
            LockImpl lock = new LockImpl("sanjin", stringRedisTemplate);
            boolean b = lock.tryLock(120);
            while (!b) {
                System.out.println("B线程再次尝试获取锁");
                b = lock.tryLock(120);
                try {Thread.sleep(1000);} catch (Exception e) {System.out.println(e.getMessage());}
            }
            System.out.println("B线程获取到锁");
            System.out.println("B线程开始执行任务...");
            // 模拟执行任务
            try {Thread.sleep(3000);} catch (Exception e) {System.out.println(e.getMessage());}
            System.out.println("B线程任务执行完毕");
            boolean b1 = lock.unLock();
            if (b1) System.out.println("B线程成功释放锁");
            countDownLatch.countDown();
        }, "threadB").start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}
-------------------------------------结果----------------------------------------------------
A线程获取到锁
A线程开始执行任务...
B线程再次尝试获取锁
B线程再次尝试获取锁
B线程再次尝试获取锁
A线程任务执行完毕
A线程成功释放锁
B线程再次尝试获取锁
B线程获取到锁
B线程开始执行任务...
B线程任务执行完毕
B线程成功释放锁

 

2. 问题1:误删其他线程的锁

问题:如果线程A的锁超时释放了,但是A还没有删除锁,B获取到了同一个name的锁,A在删除的时候会删除掉B的锁

解决:添加线程标识,每次删锁前判断一下是不是自己的线程所获取到的锁

@Override
public boolean tryLock(long timeout) {
    String key = defaultNamePre + lockName;
    String value = String.valueOf(Thread.currentThread().getId());
    // 这个加锁和设置过期是原子性的
    Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
    return b != null ? b : false;
}
@Override
public boolean unLock() {
    String key = defaultNamePre + lockName;
    String value = redisTemplate.opsForValue().get(key);
    String trueValue = String.valueOf(Thread.currentThread().getId());
    Boolean b = null;
    if (trueValue.equals(value)) {
        b = redisTemplate.delete(key);
    }
    return b != null ? b : false;
}

 

3. 问题2:删锁不是原子性的

解决方案:使用lua脚本比锁、删锁

JDK17版本

public class LockImpl implements Lock{
    private static final String defaultNamePre = "sanjin:lock:";
    private String lockName;
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 因为LockImpl不是bean不会注入redistemplate, 需要手动传
    public LockImpl(String name, StringRedisTemplate stringRedisTemplate) {
        this.lockName = name;
        this.redisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeout) {
        String key = defaultNamePre + lockName;
        String value = String.valueOf(Thread.currentThread().getId());
        Boolean res = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
        return res != null && res;
    }

    @Override
    public boolean unLock() {
        String key = defaultNamePre + lockName;
        String trueValue = String.valueOf(Thread.currentThread().getId());
        // 文本块要求jdk大于15
        String luaScript = """
                if redis.call('get', KEYS[1]) == ARGV[1] then
                    return redis.call('del', KEYS[1])
                else
                    return 0;
                end
                """;
        // List.of() 要求大于jdk9, RedisScript.of也是new了一个DefaultRedisScript
        Long res = redisTemplate.execute(RedisScript.of(luaScript, Long.class), List.of(key), trueValue);
        return res != null && res > 0;
    }
}

JDK8版本

public class LockImpl implements Lock{
    private static final String defaultNamePre = "sanjin:lock:";
    private String lockName;

    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 因为LockImpl不是bean不会注入redistemplate, 需要手动传
    public LockImpl(String name, StringRedisTemplate stringRedisTemplate) {
        this.lockName = name;
        this.redisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeout) {
        String key = defaultNamePre + lockName;
        String value = String.valueOf(Thread.currentThread().getId());
        Boolean res = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
        return res != null && res;
    }

    @Override
    public boolean unLock() {
        String key = defaultNamePre + lockName;
        String trueValue = String.valueOf(Thread.currentThread().getId());
        Long res = redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(key), trueValue);
        return res != null && res > 0;
    }
}

lua脚本的位置

测试结果

A线程获取到锁
A线程开始执行任务...
B线程再次尝试获取锁
B线程再次尝试获取锁
A线程任务执行完毕
B线程再次尝试获取锁
A线程成功释放锁
B线程再次尝试获取锁
B线程获取到锁
B线程开始执行任务...
B线程任务执行完毕
B线程成功释放锁

 

4. 其他问题

问题

  • 过期时间是固定的,线程如果在过期时间内未完成任务,锁会被自动释放
  • 如果 Redis 节点宕机,加锁和解锁功能将失效
  • 不支持公平锁,线程之间的锁竞争是随机的
  • 不支持可重入锁,如果同一线程再次尝试获取锁会失败,可能引发死锁
  • 一旦需求复杂化,需要投入更多开发时间完善功能
  • ....

解决:利用Redisson,跳转链接

5. 实现可重入分布式锁

public class DistributedLock {
    private static final String DEFAULT_PRE = "Jesus:";
    private String name;
    private StringRedisTemplate template;
    private int timeout;

    public DistributedLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = DEFAULT_PRE + name;
        this.template = stringRedisTemplate;
    }

    public boolean tryLock(int timeOut) {
        this.timeout = timeOut;
        Long threadId = Thread.currentThread().getId();
        String script = """
                    local key = KEYS[1]
                    local file1 = 'filed1'
                    local value1 = ARGV[1]
                    local file2 = 'count'
                    local value2 = 1
                    local timeout = ARGV[2]

                    -- 1.锁不存在
                    if redis.call('exists', key) == 0 then
                        redis.call('hset', key, file1, value1, file2, value2)
                        redis.call('expire', key, timeout)
                        return nil
                    end

                    -- 2.锁存在并且是自己的锁
                    if redis.call('hget', key, file1) == value1 then
                        value2 = tonumber(redis.call('hget', key, file2)) + 1
                        redis.call('hset', key, file2, value2);
                        redis.call('expire', key, timeout)
                        return nil
                    end   
                    return 0
                """;

        Object execute = template.execute(RedisScript.of(script, Long.class),
                List.of(name),
                String.valueOf(threadId), String.valueOf(timeOut));
        if (execute == null) {
            return true;
        }
        return false;
    }

    public boolean unLock() {
        Long threadId = Thread.currentThread().getId();
        String script = """
                    local key = KEYS[1]
                    local file1 = 'filed1'
                    local value1 = ARGV[1]
                    local file2 = 'count'
                    local value2 = 1
                    local timeout = ARGV[2]

                    -- 1.锁不存在
                    if redis.call('exists', key) == 0 then
                        return nil
                    end

                    -- 2.锁存在并且是自己的锁
                    if redis.call('hget', key, file1) == value1 then
                        value2 = tonumber(redis.call('hget', key, file2)) - 1
                        if value2 > 0 then
                            redis.call('hset', key, file2, value2);
                            redis.call('expire', key, timeout)
                            return nil
                        end
                        redis.call('del', key)
                        return nil
                    end
                """;

        Object execute = template.execute(RedisScript.of(script, Long.class),
                List.of(name),
                String.valueOf(threadId), String.valueOf(timeout));
        if (execute == null) {
            return true;
        }
        return false;
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇