Featured image of post 使用 Redis 实现分布式锁

使用 Redis 实现分布式锁

Redis NX 实现

利用 NX 的原子性,多个线程并发时,只有一个线程可以设置成功,设置成功即获取了锁。

如果没有获取锁,不会阻塞当前方法,直接跳过任务。

获取锁

1
2
# set key unique_value NX PX 30000
set product:stock:clothes UUID NX PX 30000
  • key
    • 根据不同的业务,区分不同的锁
  • unique_value
    • 保证每个线程的随机值都不同,用于释放锁时的校验
  • NX
    • key 不存在时设置成功,key 存在则不成功
  • PX
    • 自动失效时间。若出现异常,没有主动释放锁,可以保证超时后,锁可以过期失效(毫秒)

释放锁

释放锁将该 key 删除,在释放锁之前需要校验设置的随机数,相同才表示是该线程加的锁,能释放。

需要采用 LUA 脚本,del 命令没有提供校验值的功能。

redis 执行命令是按照一条指令完成之后,再执行下一条,用 lua 脚本,能保证 redis 执行完这个脚本才执行下一条,所以能保证判断 和 删除是原子性的

1
2
3
4
5
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@RestController
public class RedisLockController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/redisLock")
    public String redisLock() {

        // 获取分布式锁
        Boolean lock = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Expiration expiration = Expiration.seconds(30);
            // NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            // 需要使用 redisTemplate 中的序列化器
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);

            return redisConnection.set(redisKey, redisValue, expiration);
        });

        if (lock) {  // 获取到了锁
            log.info("获取到了锁");
            try {
                TimeUnit.SECONDS.sleep(15);  // 模拟业务处理
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
                        "\treturn redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "\treturn 0\n" +
                        "end";
                RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
                boolean result = redisTemplate.execute(redisScript, Arrays.asList(key), value);
                log.info("释放锁 {}", result);
            }
        }

        log.info("业务完成");
        return "success";
    }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Slf4j
@RestController
public class RedisLock {
    private RedisTemplate redisTemplate;
    // 锁名称,不同业务可能锁不同
    private String key;
    private String value;
    // 锁过期时间,单位秒
    private int expireTime;

    public RedisLock(RedisTemplate redisTemplate, String key, int expireTime) {
        this.redisTemplate = redisTemplate;
        this.key = key;
        this.expireTime = expireTime;
        // value 可以不暴露出去,每个线程都是不一样的
        this.value = UUID.randomUUID().toString();
    }

    /**
     * 获取锁
     */
    public boolean getLock() {
        // 获取分布式锁
        Boolean lock = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Expiration expiration = Expiration.seconds(expireTime);
            // NX
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            // 由于这里需要接受 byte, 不能暴力的使用 string.getBytes()
            // 要使用模板里面的 key\value 序列化器来实现
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
            Boolean result = redisConnection.set(redisKey, redisValue, expiration, setOption);
            return result;
        });
        return lock;
    }

    /**
     * 释放锁
     */
    public boolean unLock() {
        // lua 脚本
        String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
                "\treturn redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "\treturn 0\n" +
                "end";
        RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
        Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value);
        return result;
    }
}

Redisson

依赖配置

1
2
3
4
5
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.16.7</version>
</dependency>

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Slf4j
@RestController
public class RedissonLockController {
    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/redissonLock")
    public String redissonLock() {
        log.info("执行方法");
        final String key = "redisson";

        RLock lock = redissonClient.getLock(key);
        // 锁超时时间,如果未获得锁,会阻塞等待获取到锁(-1 表示没有超时时间)
        lock.lock(30, TimeUnit.SECONDS);
        log.info("获取锁");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.info("释放锁");
            lock.unlock();
        }

        log.info("完成业务");
        return "success";
    }
}