u=1724509320,3686655656&fm=253&fmt=auto&app=138&f=JPEG.webp
本文已参加「新人创作礼」活动,一起开启创作之路。

前言

       咱们知道,在Java单进程中,多线程的环境下,假如咱们要操作一个同享变量,需求运用synchronized或者是JUC同步工具类才干确保线程安全。那么,多进程环境下,咱们要怎样确保线程安全?

为什么需求分布式锁?

       咱们知道,synchronized或者是JUC同步工具类只能在同一进程中确保线程安全,他们的影响范围没办法超出本Java进程。可是跟着分布式成为主流,多进程同享数据的状况越来越常见。

image.png
       如上图,两个进程一起对存储在MySQL、Redis或者是zookeeper中的同享数据进行读写,即有或许呈现线程安全问题,这种状况下,咱们就需求一个能够在分布式环境下也能够运用的锁,来确保线程安全,这便是分布式锁。

分布式锁能够运用什么组件完成?

       因为要在分布式环境下生效,因而完成分布式锁运用的组件也必须是每个进程都能够连接到的,现在比较常见的是运用Redis和Zookeeper来完成。

Redis分布式锁完成逻辑

       Redis中,咱们运用String数据结构来完成分布式锁。

加锁

       Redis中,set的语法如下:
       SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :设置键的过期时刻为 second 秒。 SET key value EX second 作用等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时刻为 millisecond 毫秒。 SET key value PX millisecond 作用等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 作用等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。

       上述参数中,咱们能够将key作为锁标识,然后设置NX参数。假如key写入成功,表明当前Redis中不存在这个key,能够加锁;假如key写入失利,表明当前Redis中已存在这个key,已经有其它线程获取到锁了。指令如下:

SET lockKey requestId NX
lockKey为锁标识,最好带上运用同享变量唯一标识,能够运用订单编号、用户编号等。
requestId为本次锁恳求编号,开释锁时运用。

       上述指令中,能够完成加锁,可是假如在加锁后,运用挂了,或者呈现了其它问题,导致没有及时解锁,就有或许呈现死锁,因而,需求再给加上一个过期时刻,让锁能够自动消失。指令如下:

SET lockKey requestId EX seconds NX

开释锁

       开释锁的时候,咱们不能直接运用del指令去删去Redis键值,不然会呈现A获取的锁,B也能够开释的状况。因而,咱们在开释锁的时候,需求判断当前锁的恳求ID是否是加锁时是否共同,假如共同,才干开释锁。
       由于没有现成的指令能够完成上述的开释锁的逻辑,所以咱们需求运用Redis的script来完成,script脚本如下:

if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end
KEYS[1]为锁标识,即加锁时的lockKey
ARGV[1]为锁恳求编号,即加锁时的requestId

Redis分布式锁代码

public class RedisDistributedLock implements Lock {
    // 锁键值
    private String              lockKey;
    // 锁恳求编号
    private String              requestId;
    // redis集群客户端
    private JedisCluster        jedisCluster;
    public RedisDistributedLock(String lockKey, JedisCluster jedisCluster){
        this.lockKey = lockKey;
        // 运用UUID作为锁唯一标识
        requestId = UUID.randomUUID().toString();
        this.jedisCluster = jedisCluster;
    }
    /**
     * 测验获取锁
     */
    @Override
    public boolean tryLock() {
        // 获取锁
        String result = jedisCluster.set(lockKey, requestId, "NX", "EX", 2);
        return StringUtils.isNotBlank(result) && LOCK_SUCCESS.equals(result);
    }
    /**
     * 开释锁
     */
    @Override
    public void unlock() {
        // 若redis中存在lockKey,则删去lockKey,不然回来0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedisCluster.eval(script, Collections.singletonList(lockKey),
                                          Collections.singletonList(requestId));
        // 成功
        if (result != null && "OK".equals(result.toString())) {
            logger.debug("开释锁成功,lockKey:{}, requestId:{}", lockKey, requestId);
        } else {// 失利
            logger.debug("开释锁失利,lockKey:{}, requestId:{}", lockKey, requestId);
        }
    }
}

后言

       已然看到这里了,感觉有所收成的朋友,无妨来个大大的点赞吧~~~