search.png
关于我
menu.png
Redis 分布式锁的两种写法你会吗

收录于墨的2020~2021开发经验总结

1、概述

在并发编程中,锁是用来保证数据同步的重要举措,Java 自带了多种锁的实现,如synchronized、ReentrantLock、ReentrantReadWriteLock等。但这些锁只在该进程内有效。一但跨进程就会不起作用。然而在当下的技术趋势下,微服务,单服务多实例,多实例间负载均衡已经成为常见的架构基础。在这样的多进程情况下,要保证同步,那么分布式锁就尤为重要。

这篇文章,描述了两种Redis分布式锁的实现方式。希望对大家有用。

2、原理

锁的原理来自于其“唯一持有性”,因此只要能保证这个唯一持有性,那么很多东西都能作为锁。例如数据库、文件、Redis等。

其原理大概如下:

A 尝试建立资源
在A之前没有这个资源
A建立资源成功,A获得了锁

B尝试建立资源
在B之前A已经建立了资源
B建立资源失败,B未获得锁

A过了一段时间删除了该资源,释放了锁


B尝试建立资源
在B之前没有这个资源
B建立资源成功,B获得了锁

将这个资源套入到Mysql的某一行、某一个文件、Redis的某一个Key都可以成立。

但这个成立,需要一个条件,那就是这个建立锁的过程必须是原子性的。

也就是说

A 尝试建立资源
在A之前没有这个资源
A建立资源成功,A获得了锁

这个过程必须完全执行,不能中断。

Redis是单线程的。因此只要保证一条命令执行完这个建立锁的过程,就可以保证这个锁是有效的。

我们刚好有两种法子。

1、set k v nx px ms 原子操作 版 set 命令(nx 表示仅当这个key不存在时才执行set,px 设置过期时间,单位是毫秒)
2、通过lua脚本来执行这个建立锁的过程(redis 执行lua脚本也是原子性的)

分别对应着下文的两种锁的实现。

3、set k v nx px ms


import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 用redis 实现的分布式锁
 *
 * set k v nx px ms 原子操作 版
 */
@Slf4j
public class RedisLock {

    private final static String LOCK_KEY_PRE = "lock_";

    // 此处需要设置可见性,以保证同步
    private volatile boolean locked = false;

    private final RedisTemplate<String, Object> redisTemplate;

    private final String lockMaster;

    private final String lockKey;

    private String lockValue;

    private final long expireMillis;

    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockMaster,
                     String lockKey, long expireMillis) {
        Assert.isTrue(StringUtils.hasLength(lockMaster), "lockMaster不能为空");
        Assert.isTrue(StringUtils.hasLength(lockKey), "lockKey不能为空");
        Assert.isTrue(expireMillis > 0, "expireMillis 需要大于 0");
        this.redisTemplate = redisTemplate;
        this.lockMaster = lockMaster;
        this.lockKey = LOCK_KEY_PRE + lockKey;
        this.expireMillis = expireMillis;
    }

    /**
     * 尝试获取锁,获取不到立即返回
     *
     * @return 是否获得锁
     */
    public boolean getLock() {
        // 通过UUID生成一个随机的字符串,作为锁的值,解锁时除了需要对比这个值是否一致,类似于一把钥匙
        lockValue = UUID.randomUUID().toString();
        locked = this.setLock(lockKey, lockValue, expireMillis);
        log.info("{} 尝试获得锁{},结果{}", lockMaster, lockKey, locked);
        return locked;
    }

    /**
     * 尝试获取锁,等待一段时间
     *
     * @param timeout 毫秒
     * @return 是否获得锁
     */
    public boolean tryGetLock(long timeout) {
        // 生成随机value
        lockValue = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();
        while ((System.currentTimeMillis() - startTime) <= timeout) {
            if (this.setLock(lockKey, lockValue, expireMillis)) {
                locked = true;
                log.info("{} 获得到锁{},预计阻塞时间{},实际阻塞时间:{}ms",
                        lockMaster, lockKey, timeout, System.currentTimeMillis() - startTime);
                return true;
            }
            sleep();
        }
        log.info("{} 获得锁{}失败!预计阻塞时间{},实际阻塞时间:{}ms",
                lockMaster, lockKey, timeout, System.currentTimeMillis() - startTime);
        locked = false;
        return false;
    }

    /**
     * 阻塞获取锁,一直阻塞直到获取锁
     *
     * @return true
     */
    public boolean tryGetLockBlock() {
        // 生成随机value
        lockValue = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();
        while (true) {
            if (this.setLock(lockKey, lockValue, expireMillis)) {
                locked = true;
                log.info("{} 获得到锁{},阻塞时间:{}ms",
                        lockMaster, lockKey, System.currentTimeMillis() - startTime);
                return true;
            }
            sleep();
        }
    }

    /**
     * 解锁
     *
     * @return 解锁是否成功
     */
    public boolean unlock() {
        Boolean unlockOk = false;
        // 持有锁才能释放锁
        if (locked) {
            if (checkLock(lockKey, lockValue)) {
                unlockOk = redisTemplate.execute((RedisConnection conn) -> {
                    Long count = conn.keyCommands().del(lockKey.getBytes());
                    return count != null && count > 0;
                });
                unlockOk = unlockOk != null && unlockOk;
                locked = !unlockOk;
                log.info("{} 释放锁{}结果:{}", lockMaster, lockKey, unlockOk);
            }
        }
        return unlockOk;
    }

    private void sleep() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            log.warn("RedisLock " + lockKey + " 被外部中断");
        }
    }

    private Boolean checkLock(final String key, final String value) {
        return value.equals(redisTemplate.execute((RedisConnection conn) -> {
            byte[] bytes = conn.stringCommands().get(key.getBytes());
            if (bytes != null) {
                return new String(bytes);
            }
            return null;
        }));
    }

    Boolean setLock(String key, String value, long milliSeconds) {
        // set k v nx px ms 原子操作
        return redisTemplate.execute((RedisConnection conn) -> {
            Boolean result = conn.stringCommands().set(
                    key.getBytes(),
                    value.getBytes(),
                    Expiration.from(milliSeconds, TimeUnit.MILLISECONDS),
                    RedisStringCommands.SetOption.SET_IF_ABSENT);
            log.info("{} 尝试获取锁{}:{} ", lockMaster, lockKey, result);
            return result != null && result;
        });
    }

    public boolean isLocked() {
        return locked;
    }

    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

    public String getLockMaster() {
        return lockMaster;
    }
    
    public String getLockKey() {
        return lockKey;
    }

    public String getLockValue() {
        return lockValue;
    }

    public long getExpireMillis() {
        return expireMillis;
    }
}

代码并不复杂,提供的获取锁的方法有三个:
1、boolean getLock() 尝试获取锁,获取不到立即返回
2、boolean tryGetLock(long timeout) 尝试获取锁,等待一段时间
3、boolean tryGetLockBlock() 阻塞获取锁,一直阻塞直到获取锁

释放锁的方法:
boolean unlock()

判断是否持有锁的方法:
boolean isLocked()

4、使用 lua 脚本实现的原子操作版

为了保持代码简洁,此处直接继承了RedisLock 实现代码复用。通过覆盖了两个方法,来设置不同的处理过程。

对于Redis使用lua脚本,可以看我的这一篇文章,里边讲的很详细:Redis 使用lua脚本


import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Arrays;

/**
 * 用redis 实现的分布式锁
 *
 * 使用 lua 脚本实现的原子操作版
 * 继承了RedisLock从而复用了一些方法
 */
@Slf4j
public class RedisLockUseLua extends RedisLock {

    // 加锁脚本,成功返回1,失败返回0
    private final static String LOCK_LUA_SCRIPT =
            "if redis.call('get',KEYS[1]) then " +
                "return 0;" +
            "else " +
                "redis.call('set',KEYS[1],ARGV[1]);" +
                "redis.call('pexpire',KEYS[1],ARGV[2]);" +
                "return  1;" +
            "end;";

    // 解锁脚本,成功返回1,失败返回0
    private final static String UNLOCK_LUA_SCRIPT =
            "local v = redis.call('get',KEYS[1]);" +
                "if v then " +
                    "if v~=ARGV[1] then " +
                        "return 0;" +
                    "end;" +
                "redis.call('del',KEYS[1]);" +
            "end;" +
            "return 1;";

    public RedisLockUseLua(RedisTemplate<String, Object> redisTemplate, String lockMaster, String lockKey, long expireMillis) {
        super(redisTemplate, lockMaster, lockKey, expireMillis);
    }

    @Override
    public boolean unlock() {
        boolean isOk = false;
        // 持有锁才能释放锁
        if (isLocked()) {
            isOk = evalLuaScriptReturnBool(UNLOCK_LUA_SCRIPT,
                    new String[]{ getLockKey() }, getLockValue());
            log.info("{} 释放锁{}结果:{}", getLockMaster(), getLockKey(), isOk);
        }
        return isOk;
    }

    @Override
    Boolean setLock(String key, String value, long milliSeconds) {
        return evalLuaScriptReturnBool(LOCK_LUA_SCRIPT,
                new String[]{ key },
                value, String.valueOf(milliSeconds));
    }

    private Boolean evalLuaScriptReturnBool(String luaScript, String[] keys, String... argv) {
        return getRedisTemplate().execute(RedisScript.of(luaScript, Boolean.class),
                new StringRedisSerializer(),
                new GenericToStringSerializer<>(Boolean.class),
                Arrays.asList(keys),
                (Object[]) argv);
    }
}

5、测试和验证

通过Junit 写了几个测试用例,验证了一下。同时也大概演示了如何使用:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DawnRedisLockTest {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    private void sleep(long milli) {
        try {
            Thread.sleep(milli);
        } catch (InterruptedException ignore) {
        }
    }

    @Test
    public void testRedisLock1() throws InterruptedException {
        testLock1(RedisLock::new);
    }

    @Test
    public void testRedisLockUseLua1() throws InterruptedException {
        testLock1(RedisLockUseLua::new);
    }

    @Test
    public void testRedisLock2() throws InterruptedException {
        testLock2(RedisLock::new);
    }

    @Test
    public void testRedisLockUseLua2() throws InterruptedException {
        testLock2(RedisLockUseLua::new);
    }

    @FunctionalInterface
    interface CreateRedisLock {
        RedisLock create(RedisTemplate<String, Object> redisTemplate, String lockMaster, String lockKey, long expireMillis);
    }

    // 模拟高并发下,多线程竞争瞬间是否只有一个线程可以获得到锁,测试50次,每次产生40条线程竞争锁
    public void testLock1(CreateRedisLock createRedisLock) throws InterruptedException {
        final int testTimes = 50;
        for (int i = 0; i < testTimes; i++) {
            final AtomicInteger atomicInteger = new AtomicInteger(1);
            final AtomicInteger lockNum = new AtomicInteger(0);
            Supplier<Runnable> supplier = () -> () -> {
                int j = atomicInteger.getAndIncrement();
                RedisLock redisLock = createRedisLock.create(redisTemplate, "runnable" + j, "TestLock", 100);
                System.out.printf("runnable%d 执行\n", j);
                if (redisLock.getLock()) {
                    System.out.printf("runnable%d 获得锁\n", j);
                    lockNum.incrementAndGet();
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 释放锁
                        redisLock.unlock();
                    }
                }
            };
            final int threadNum = 40;
            ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
            for (int j = 0; j < threadNum; j++) {
                executorService.submit(supplier.get());
            }
            executorService.shutdown();
            //noinspection ResultOfMethodCallIgnored
            executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
            Assert.assertEquals(1, lockNum.get());
        }
    }

    // 模拟锁的获得过程来校验其是否是按照预期的
    public void testLock2(CreateRedisLock createRedisLock) throws InterruptedException {
        String result = "runnable1 获得锁runnable2 未获得锁runnable3 获得锁runnable4 获得锁";
        StringBuilder sb = new StringBuilder();
        Runnable runnable1 = () -> {
            RedisLock redisLock = createRedisLock.create(redisTemplate, "runnable1", "TestLock", 100);
            System.out.println("runnable1 执行");
            if (redisLock.getLock()) {
                System.out.println("runnable1 获得锁");
                sb.append("runnable1 获得锁");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    redisLock.unlock();
                }
            }
        };
        Runnable runnable2 = () -> {
            RedisLock redisLock = new RedisLock(redisTemplate, "runnable2", "TestLock", 100);
            sleep(30);
            System.out.println("runnable2 执行");
            if (redisLock.getLock()) {
                System.out.println("runnable2 获得锁");
                sb.append("runnable2 获得锁");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    redisLock.unlock();
                }
            } else {
                System.out.println("runnable2 未获得锁");
                sb.append("runnable2 未获得锁");
            }
        };
        Runnable runnable3 = () -> {
            RedisLock redisLock = new RedisLock(redisTemplate, "runnable3", "TestLock", 100);
            sleep(60);
            System.out.println("runnable3 执行");
            if (redisLock.tryGetLock(101)) {
                System.out.println("runnable3 获得锁");
                sb.append("runnable3 获得锁");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    redisLock.unlock();
                }
            }
        };
        Runnable runnable4 = () -> {
            RedisLock redisLock = new RedisLock(redisTemplate, "runnable4", "TestLock", 100);
            sleep(150);
            System.out.println("runnable4 执行");
            if (redisLock.tryGetLockBlock()) {
                sb.append("runnable4 获得锁");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    redisLock.unlock();
                }
            }
        };
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        executorService.submit(runnable1);
        executorService.submit(runnable2);
        executorService.submit(runnable3);
        executorService.submit(runnable4);
        executorService.shutdown();
        //noinspection ResultOfMethodCallIgnored
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        Assert.assertEquals(result, sb.toString());
    }
}

使用这个锁时要保证在异常情况下锁能得到释放,因此要使用try catch finally的格式,在finally里释放锁

像这样:

 if (redisLock.tryGetLockBlock()) {
     try {
          // 实际业务代码
         Thread.sleep(200);
     } catch (InterruptedException e) {
         e.printStackTrace();
     } finally {
         // 释放锁
         redisLock.unlock();
     }
 }

测试的结果:

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

测试结果的分析:

两种Redis分布式锁的实现都能保证在同一刻只有一个线程持有锁。

使用set k v nx px ms 的实现和lua的脚本的实现从耗时来说基本一致,lua脚本的会稍快一丢丢。

6、多进程下模拟锁竞争

多进程下锁竞争的模拟是通过定时任务实现的,对于同一个实例,分别启动了五个实例,五个实例的端口不一,但是定时任务的时间一致。

@Component
public class RedisLockScheduled {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Scheduled(cron = "0 * * * * ?")
    public void getRedisLock() {
        RedisLock redisLock = new RedisLock(redisTemplate,
                "RedisLockScheduled", "RedisLock", 2333);
        if (redisLock.getLock()) {
            System.out.println("\n\n\n\n获得锁!!!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                redisLock.unlock();
            }
        }
    }


    @Scheduled(cron = "0 * * * * ?")
    public void getRedisLockUseLua() {
        RedisLock redisLock = new RedisLockUseLua(redisTemplate,
                "RedisLockScheduled", "RedisLockUseLua", 2333);
        if (redisLock.getLock()) {
            System.out.println("\n\n\n\n获得锁!!!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                redisLock.unlock();
            }
        }
    }
}

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

以下是五个进程的日志:
2021-06-16 00:00:00.001 进程1获得了锁lock_RedisLockUseLua
2021-06-16 00:00:00.024 进程4获得了锁lock_RedisLock
2021-06-16 00:00:02.002 进程4释放锁lock_RedisLock
2021-06-16 00:00:02.025 进程4释放锁lock_RedisLockUseLua
2021-06-16 00:00:02.026 进程1获得了锁lock_RedisLock

1:

111111111111111111111111111111111
2021-06-16 00:00:00.001  INFO 41580 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果true




获得锁RedisLockUseLua!!!
2021-06-16 00:00:02.002  INFO 41580 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 释放锁lock_RedisLockUseLua结果:true
111111111111111111111111111111111
2021-06-16 00:00:02.026  INFO 41580 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获取锁lock_RedisLock:true 
2021-06-16 00:00:02.026  INFO 41580 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLock,结果true




获得锁RedisLock!!!
2021-06-16 00:00:04.030  INFO 41580 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 释放锁lock_RedisLock结果:true

2:

111111111111111111111111111111111
2021-06-16 00:00:00.014  INFO 43260 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false




失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.027  INFO 43260 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获取锁lock_RedisLock:false 
2021-06-16 00:00:00.027  INFO 43260 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLock,结果false




失败:未获得锁RedisLock!!!

3:

111111111111111111111111111111111
2021-06-16 00:00:00.001  INFO 38504 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false




失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.027  INFO 38504 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获取锁lock_RedisLock:false 
2021-06-16 00:00:00.027  INFO 38504 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLock,结果false




失败:未获得锁RedisLock!!!

4:

111111111111111111111111111111111
2021-06-16 00:00:00.001  INFO 41200 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false




失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.024  INFO 41200 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获取锁lock_RedisLock:true 
2021-06-16 00:00:00.024  INFO 41200 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLock,结果true




获得锁RedisLock!!!
2021-06-16 00:00:02.025  INFO 41200 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 释放锁lock_RedisLock结果:true

5:

111111111111111111111111111111111
2021-06-16 00:00:00.001  INFO 14768 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLockUseLua,结果false




失败:未获得锁RedisLockUseLua!!!
111111111111111111111111111111111
2021-06-16 00:00:00.027  INFO 14768 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获取锁lock_RedisLock:false 
2021-06-16 00:00:00.027  INFO 14768 --- [   scheduling-1] cn.hengyumo.dawn.utils.RedisLock         : RedisLockScheduled 尝试获得锁lock_RedisLock,结果false




失败:未获得锁RedisLock!!!

END

Tell me why are we, so blind to see
That the one's we hurt, are you and me

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2021年06月16日 00:15:58

评论区#

还没有评论哦,期待您的评论!

关闭特效