分布式锁的前提介绍
因为分布式系统之间是不同进程的,单机版的锁无法满足要求。所以我们可以借助中间件Redis的setnx()命令实现分布式锁。setnx()命令只会对不存在的key设值,返回1代表获取锁成功。
分布式锁的基础要点
分布式锁的特性是排他、避免死锁、高可用。
分布式锁的实现原理
分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是序列中最小的节点,如果不是则监听比当前节点还要小的节点。如果是,获取锁成功。当被监听的节点释放了锁(也就是被删除),会通知当前节点。然后当前节点再尝试获取锁,如此反复)。
Zookeeper的分布式锁原理
- Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是序列中最小的节点,如果不是则监听比当前节点还要小的节点。如果是,获取锁成功。
- 当被监听的节点释放了锁(也就是被删除),会通知当前节点。然后当前节点再尝试获取锁,如此反复)
数据库的分布式锁原理
如果获取锁的逻辑只有这三行代码的话,会造成死循环,明显不符合分布式锁的特性。
我们知道分布式锁的特性是排他、避免死锁、高可用。分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)。
Redis的分布式锁原理
- Redis对存在的key设值,会返回0代表获取锁失败。这里的value是System.currentTimeMillis() (获取锁的时间)+锁持有的时间。
- 这里设置锁持有的时间是200ms,实际业务执行的时间远比这200ms要多的多,持有锁的客户端应该检查锁是否过期,保证锁在释放之前不会过期。因为客户端故障的情况可能是很复杂的。
分布式案例分析
- 比如现在有A,B俩个客户端。A客户端获取了锁,执行业务中做了骚操作导致阻塞了很久,时间应该远远超过200ms,当A客户端从阻塞状态下恢复继续执行业务代码时,A客户端持有的锁由于过期已经被其他客户端占有。这时候A客户端执行释放锁的操作,那么有可能释放掉其他客户端的锁。
- 这里设置的客户端等待锁的时间是200ms。这里通过轮询的方式去让客户端获取锁。如果客户端在200ms之内没有锁的话,直接返回false。实际场景要设置合适的客户端等待锁的时间,避免消耗CPU资源。
接下来我们就要用redis去开发一个我们自己的一个常用的分布式锁的组件。
总体设计结构图
引用Maven配置
首先我们先进行配置相关的maven的依赖,这些依赖呢大家选择性进行使用即可。
xml
复制代码
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.13.3</version> </dependency> <dependency> <groupId>com.fengwenyi</groupId> <artifactId>JavaLib</artifactId> <version>2.1.6</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <!--joda--> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.0-jre</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.8</version> </dependency> </dependencies>
建立分布式锁的参数模型
构建分布式锁的参数模型类:DistributeLockParam。
java
复制代码
@Data public class DistributeLockParam { private String lockUUid; private String lockNamePrefix; private Long expireTime; private Long waitTime; private TimeUnit timeUnit; private String delimiter; private DistributeLockType lockType; }
参数的一个大概的一个分析介绍:
- lockUUid:分布式锁的唯一ID主键标识,作为主键操作。
- lockNamePrefix:锁名称的前缀,用于作为查询锁状态的标准,
- expireTime:为了防止死锁,我们需要加入一个参数作为过期时间,防止系统宕机后,或者长时间占用进行资源不释放的问题。
- waitTime:与过期时间不同,等待时间作为锁需要占用或者其他线程会等待获取锁的时间。
- timeUnit:等待时间和过期时间的时间单位
- delimiter:锁标识key的分隔符,redis而言一般采用”:“的方式进行控制。
- lockType:锁的类型。
所以还需要定义分布式锁类型:
java
复制代码
public enum DistributeLockType { /** * 重入锁 */ REENTRANT_LOCK, /** * 非公平锁 */ FAIR_LOCK, /** * 联和锁 */ MULTI_LOCK, /** * 红锁 */ RED_LOCK, /** * 读写锁 */ READ_WRITE_LOCK, ; }
定义分布式锁的核心接口
接下来我们要定义一下分布式锁的核心逻辑接口DistributeLockSupport。
java
复制代码
public interface DistributeLockSupport<T> { /** * 默认的分隔符 */ String DEFAULT_DELIMTER = ":"; String DEFAULT_KEY_PREFIX = "LOCK"; Long DEFAULT_EXPIRE_TIME = 10L; Long DEFAULT_WAIT_TIME = 10L; Joiner DEFAULT_JOINER = Joiner.on(DistributeLockSupport.DEFAULT_DELIMTER). skipNulls(); /** * 加锁 * @param distributeLockParam * @return */ T lock(DistributeLockParam distributeLockParam); /** * 解锁 * @param distributeLockParam */ void unlock(T param, DistributeLockParam distributeLockParam); }
其中前四个属性静态常量值主要作用是给我们的分布式所提供默认值。
java
复制代码
/** * 默认的分隔符 */ String DEFAULT_DELIMTER = ":"; String DEFAULT_KEY_PREFIX = "LOCK"; Long DEFAULT_EXPIRE_TIME = 10L; Long DEFAULT_WAIT_TIME = 10L;
分别代表
- 分布是所的键值的分割符。
- 默认的key的前缀。
- 还有就是锁的过期时间和等待时间。
这里我们采用了Guava的连接器,进行我们的特殊风格符的连接。
java
复制代码
Joiner DEFAULT_JOINER = Joiner.on(DistributeLockSupport.DEFAULT_DELIMTER). skipNulls();
业务加锁和解锁方法
主要用于枷锁和解锁我们的分布式锁。
java
复制代码
/** * 加锁 * @param distributeLockParam * @return */ T lock(DistributeLockParam distributeLockParam); /** * 解锁 * @param distributeLockParam */ void unlock(T param, DistributeLockParam distributeLockParam);
定义分布式锁的键Key生成接口
接下来主要去定一个接口,专门为我们生成不同样式,不同格式的键值,进行一个扩展的一个接口(LockKeyGenerator)。
java
复制代码
public interface LockKeyGenerator { String getLockKey(ProceedingJoinPoint pjp); }
可以看到啊对应的参数是AOP的一个代理参数:ProceedingJoinPoint, 这也被我们后面进行批处理奠定一定的基础。
定义分布式锁的异常类
主要用于定义分布式锁的异常输出类:RedisDistributedLockException。
java
复制代码
public class RedisDistributedLockException extends RuntimeException { private String key; public RedisDistributedLockException (String key) { super("key [" + key + "] tryLock fail"); this.key = key; } public RedisDistributedLockException (String key, String errorMessage) { super("key [" + key + "] tryLock fail error message :" + errorMessage); this.key = key; } }
可以看到我们的该类是实现了RuntimeException的运行时异常类。
【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的分布式锁的功能组件(二)https://developer.aliyun.com/article/1471008