一、引言
不知道你是否有过这样的经历:在引入其他的包和库后,通常什么都不用加,直接启动Spring Boot程序,就能够使用该包或库提供的功能,或者也仅仅需要添加一个 @Enablexxx的注解,在配置文件中提供参数就能够使用相应的功能。我想可能和多人都被注解“迷惑”了,然后就去搜寻和注解相关的知识:AOP、AspectJ、切面等等。实际上,这些操作仍然需要我们通过程序去主动的寻找类、方法等,获取上面的注解,然后进行逻辑处理。实际上,我们真正寻找的并不是【AOP】,而是【自动化】或者叫【模块化】,指的就是 SpringBoot 启动后就能自动扫描类完成配置,在使用时SpringBoot能够识别出注解并作出拦截,这个自动化的过程,才是真正的追求。本文以注解驱动实现Redis分布式锁的方式,来介绍SpringBoot模块化开发,并给出最终实践。
二、准备
技术选型
由Redis实现分布式锁的方案特别多,往下说由通过操作Redis本身来直接实现分布式锁的,往上说由通过对Redis包装的各种框架来实现分布式锁的,如Jedis,Lettuce,Redisson等框架。本文选择对Redisson框架进行包装,从而实现分布式锁。Redisson是一款十分优秀的框架,它封装Redis操作的大部分细节,并采用Java原生API实现了分布式锁,异步调用等功能。本文的分布式锁方案只是用了Redisson分布式锁的API,感兴趣的朋友可以考虑用Redisson取代RedisTemplate来操作Redis
期望方案
1. 配置文件驱动
本文主要设计一款注解驱动的Redis分布式锁方案,通过配置文件就可以轻松配置多个数据源和模式。配置文件参考如下:
lock:
redisson:
list:
# 单实例模式
- schema: redis-single-1
settings:
threads: 16
nettyThreads: 32
# codec:
transportMode: "NIO"
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
# password: null
subscriptionsPerConnection: 5
clientName: null
address: redis://127.0.0.1:6379
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 24
connectionPoolSize: 64
database: 0
dnsMonitoringInterval: 5000
# 哨兵模式
- schema: redis-sentinel-1
settings:
threads: 16
nettyThreads: 32
# codec:
transportMode: "NIO"
sentinelServersConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
# password: null
subscriptionsPerConnection: 5
clientName: null
# loadbalancer:
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 24
connectionPoolSize: 64
database: 0
failedSlaveReconnectionInterval: 3000
failedSlaveCheckInterval: 60000
slaveConnectionMinimumIdleSize: 24
slaveConnectionPoolSize: 64
readMode: "SLAVE"
sentinelAddresses:
- "redis://127.0.0.1:6379"
- "redis://127.0.0.1:6479"
masterName: "redis-master"
# 集群模式
- schema: redis-cluster-1
settings:
threads: 16
nettyThreads: 32
# codec:
transportMode: "NIO"
lockWatchdogTimeout: 30000
clusterServersConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
# password: null
subscriptionsPerConnection: 5
clientName: null
# loadbalancer:
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 24
connectionPoolSize: 64
failedSlaveReconnectionInterval: 3000
failedSlaveCheckInterval: 60000
slaveConnectionMinimumIdleSize: 24
slaveConnectionPoolSize: 64
readMode: "SLAVE"
subscriptionMode: "SLAVE"
nodeAddresses:
- "redis://127.0.0.1:16379"
- "redis://127.0.0.1:16479"
- "redis://127.0.0.1:16579"
scanInterval: 1000
pingConnectionInterval: 30000
keepAlive: false
tcpNoDelay: true
如图可见,通过配置文件,可以配置多个数据源(即lock.redisson.list),在每个数据源中都可以为当前数据源选择不同的模式(single, sentinel, cluster),这最大程度满足了配置驱动的需求。事实上,正是Redisson提供了此类配置文件,才可以轻松使用。至于怎么解析,我们放在后面再说
2. 注解驱动
通过在启动类或配置类上标记注解,就能够进行自动配置, 可以启用分布式锁功能
@SpringBootApplication
@EnableRedisMutexLock
public class SpringRedisMutexLockApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRedisMutexLockApplication.class, args);
}
}
在@EnableRedisMutexLock标注的情况下,可以通过在方法上标记注解,来表明这个方法需要获取到分布式锁后执行。
@RedisMutexLock(schema = "redis-single-1", name = "test-lock-annotation", tryTimeout = 1000)
public void testAnnotation() {
doSomeThing();
}
3. API 驱动
即使已经实现了注解驱动的分布式锁,但是在某些场景下我们不能够添加注解,因此还需要手动通过API方式来获取分布式锁并进行上锁和解锁的功能
public void testApi() {
final RMLock lock = RedisMutexLockSupport.createLock("redis-single-1", "lock-1");
lock.lock(1000, TimeUnit.MILLISECONDS);
doSomeThing();
lock.unlock();
}
4. 总结
本文要从 配置文件、注解、API 三个方面实现Redis分布式锁,方便对Redis分布式锁进行配置和使用
Maven pom 依赖
本文主要以SpringBoot进行开发,严格上并不涉及Web等组件。因此在Pom文件上,只给出项目必要的依赖,其他依赖不做限制
<!-- SpringBoot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Redisson的自动配置包, 内部去除了jedis和lettuce -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
</dependency>
<!-- SpringBoot Aop依赖, 编写切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- SpringBoot 的注解配置处理器, 可以优化配置文件中的警告 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
三、实现
1. 锁接口实现
我们要操作分布式锁,自然要明确到底要操作什么功能。锁接口就是我们能操作功能的集合,事实上,通过AOP方式本质上也是获取了锁对象并调用锁的接口,只不过锁的持有者从用户变为了切面类。这里参考Redisson的RLock接口,设计了该分布式锁系统的锁接口RMLock
/**
* 实现了 {@link java.util.concurrent.locks.Lock} 和可重入锁的分布式锁
* @see RLock
*/
public interface RMLock extends RLock {
/**
* 获取锁对象名称
*
* @return name - 对象名称
*/
String getName();
/**
* 获取定义了 <code>leaseTime</code> 参数的锁.
* 一直等待锁直至获取, 并在获取锁后的 <code>leaseTime</code> 后自动释放锁
*
* @param leaseTime 获取锁后,在锁未通过 {@code unlock} 释放前的最大持有时间.
* 如果该值为 -1, 则锁会被一直持有直到显式地通过 {@code unlock} 释放
* @param unit 时间单位
* @throws InterruptedException - 线程发生中断
*/
void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 尝试获取定义了 <code>waitTime</code> 参数的锁.
* 等待最多 <code>waitTime</code> 时间直到获取锁.
* 在定义了 <code>leaseTime</code> 参数后, 锁会在不超过该时间前释放
*
* @param waitTime 获取锁的最大等待时间
* @param leaseTime 最大持有时间
* @param unit 时间单位
* @return <code>true</code> 成功获取锁,
* <code>false</code> 获取锁失败
* @throws InterruptedException - 线程发生中断
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 获取定义了 <code>leaseTime</code> 参数的锁.
* 一直等待锁直至获取, 并在获取锁后的 <code>leaseTime</code> 后自动释放锁
*
* @param leaseTime 获取锁后,在锁未通过 {@code unlock} 释放前的最大持有时间.
* 如果该值为 -1, 则锁会被一直持有直到显式地通过 {@code unlock} 释放
* @param unit 时间单位
*
*/
void lock(long leaseTime, TimeUnit unit);
/**
* 忽略锁的状态 <code>state</code> 强制释放锁
*
* @return <code>true</code> 锁存在并且成功释放, 否则 <code>false</code>
*/
boolean forceUnlock();
/**
* 检查该锁是否被任意线程持有
*
* @return <code>true</code> 锁被持有, 否则 <code>false</code>
*/
boolean isLocked();
/**
* 检查该锁是否被线程ID为 <code>threadId</code> 的线程持有
*
* @param threadId 要检验的线程ID
* @return <code>true</code> 该锁确实被该线程持有, 否则 <code>false</code>
*/
boolean isHeldByThread(long threadId);
/**
* 检查该锁是否被当前线程持有
*
* @return <code>true</code> 当前线程持有该锁, 否则 <code>false</code>
*/
boolean isHeldByCurrentThread();
/**
* 当前线程持有该锁的数量 (可重入锁)
*
* @return 如果当前线程持有锁, 则为持有数量, 否则为 0
*/
int getHoldCount();
/**
* 锁的剩余持有时间
*
* @return 锁的剩余持有时间(毫秒).
* -2 如果锁不存在.
* -1 如果锁存在但是没有过期时间.
*/
long remainTimeToLive();
}
有的朋友可能好奇为什么上面接口中没有 lock(), unlock(), tryLock() 这种经典的无参方法呢?这里剧透一下,实际上是 RMLock 继承的 RLock 接口继承自 java.util.concurrent.locks.Lock,这些经典的方法都在 Lock 中定义了,因此不用重复定义。如果需要,也可以显式地定义方法
2. 注解与切面的实现
在上面我们提到的各个方案中,只有注解是实现起来最简单的,我们首先按照需求分别创建两个注解
@EnableRedisMutexLock 启动配置注解
public @interface EnableRedisMutexLock { }
@RedisMutexLock 分布式锁注解
/**
* 注解在方法上, 获取分布式锁后执行, 否则为null
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisMutexLock {
/**
* 锁采用的配置模式
*/
String schema();
/**
* 锁的名称
*/
String name();
/**
* 是否使用公平锁
*/
boolean fair() default false;
/**
* 锁的最大超时时间(单位毫秒, 为0时为永久等待)
*/
long tryTimeout() default 0;
}
可以注意到 @EnableRedisMutexLock 注解实现细节很少,甚至没有 @Retention这样的元注解。请别着急,这涉及到了本次自动化的核心注解,我们一会再说,这里着重实现 注解及切面的AOP功能。
让我们创建@RedisMutexLock实现逻辑的Aspect类
/**
* 分布式锁切面
*/
@Aspect
public class RedisMutexLockAspect {
private static final Logger log = LoggerFactory.getLogger(RedisMutexLockAspect.class);
// 这个切面的意思即为寻找所有携带 @RedisMutexLock的方法, 进行拦截
@Pointcut("@annotation(com.example.springredismutexlock.annotation.RedisMutexLock)")
public void pointCut() {
}
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) {
final MethodSignature signature = (MethodSignature) pjp.getSignature();
final RedisMutexLock annotation = signature.getMethod().getAnnotation(RedisMutexLock.class);
final String schema = annotation.schema();
final String name = annotation.name();
final boolean fair = annotation.fair();
final long tryTimeout = annotation.tryTimeout();
// 这里通过显式调用API获取锁对象, 具体实现稍后再说
final RMLock lock = RedisMutexLockSupport.createLock(schema, name, fair);
if (lock == null)
return null;
Object result = null;
boolean lockAcquire = false;
try {
if (lock != null) {
if (tryTimeout <= 0) {
lock.lock();
lockAcquire = true;
} else {
lockAcquire = lock.tryLock(tryTimeout, timeUnit);
}
}
// 获取锁则执行方法
if (lockAcquire) {
result = pjp.proceed();
}
} catch (InterruptedException e) {
log.info("Occurs an interrupt when wait lock '{}'", name);
} catch (Throwable e) {
log.error("System running exception during business, distribute lock will be release, more details: ", e);
} finally {
if (lock.isHeldByCurrentThread()) {
// 执行完毕后解锁
lock.unlock();
}
}
return result;
}
}
OK, 这样我们就完成了大部分注解驱动的实现,毕竟注解驱动本质上是通过AOP的方式减少用户操作,把锁的持有者由用户转为切面类。接下来,如何实现获取锁才是代码能够执行的关键
3. 配置文件读取与解析实现
无论是API还是注解,我们获取和操作的都是锁对象,也就是说我们在使用时不关心锁到底是怎么生成的,只需要知道提供参数就能获取锁并使用就行了。但对于进行模块开发的我们来说,锁的生成才是最重要的一环,它涉及到了配置文件读取与解析,Bean的注册与管理等等。
提起配置文件读取,很多人第一时间想到的是 @ConfigurationProperties 或者 @Value 注解,这两个注解能够将配置文件中的属性和配置类中的字段进行绑定,使得在加载过程中将配置自动注入到配置类中,因此对于上面复杂的配置,很多人第一时间想到的是这样:
@ConfiguratonProperties("lock.redisson")
public class ConfigTop {
private List<ConfigInner1> list;
public static class ConfigInner1 {
private String schema;
private ConfigInner2 settings;
public static class ConfigInner2 {
private Integer threads;
private Integer nettyThreads;
private String transportMode;
private ConfigInner3 singleServerConfig;
public static class ConfigInner3 {
private Long idleConnectionTimeout;
private Long connectTimeout;
......
}
}
}
}
但如果我告诉你 ConfigInner3 实际上就是 org.redisson.config.SingleServerConfig 呢, ConfigInner2 实际上就是 org.redisson.config.Config 呢,你还会这么配置吗?
你可能想那就把参数换一下,然后解析时也能创建Config对象和SingleServerConfig对象不久可以了?
想法很好,但实际上在redissson中,SingleServerConfig是通过单独的文件解析的,在redisson官方文档的配置文件中,需要指定一个file作为详细的配置文件,在该文件中就可以选择为 SingleServerConfig 或 SentinelServersConfig 或其他。
此外,最重要的一个问题就是多数据源问题,默认的redisson也好还是其他类型数据源也好,都是单点的,这意味着最多配置一个数据源,对于多出来的配置,默认的解析类也不会处理,甚至都无法识别多数据源的情况。因此我们必须手动实现多数据源注册的问题,这就意味着不能够通过 @Bean等方式注入Bean来生成Bean了,毕竟注入的可不止一个,我们生成的也不是随便生成,而是一对一生成。因此,自定义Bean注册器就显得很重要
3.1 定义属性配置类
// @ConditionalOnProperty(RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX)
@ConfigurationProperties(RedisMutexLockProperties.REDIS_PROPERTY_PREFIX)
public class RedisMutexLockProperties {
public static final String REDIS_PROPERTY_PREFIX = "lock.redisson";
public static final String REDIS_PROPERTY_LIST_PREFIX = "lock.redisson.list";
private List<RedisMutexLockNodeProperties> list;
// 省略构造, get/set方法
public static class RedisMutexLockNodeProperties {
public static final String REDIS_MODE_PROPERTY_PREFIX = "settings";
private String schema;
private String type;
private Config settings;
// 省略构造, get/set方法
}
}
这里需要注意一点,就是上面的 @ConditionalOnProperty 注解被注释掉了,知道的朋友明白这是一个条件加载注解,但是 SpringBoot 中的配置条件加载是 配置本身是否有值来判断的,比如下面两个配置
lock:
redisson:
list: 'xxxx'
-------------------
lock:
redisson:
list:
a: 'xxx'
b: 'xxx'
c: 'xxx'
在上面的配置中, lock.redisson.list是一个实际存在的配置,它的值是 xxxx
而下面的配置中 lock.redisson.list 本身不存在,它只是一个前缀,真正存在的配置是 lock.redisson.list.a, lock.redisson.list.b, lock.redisson.list.c 这三个配置。因此当使用 @ConditionalOnProperties(“lock.redisson.list”) 时一定不会成功, 因为就不存在这个配置
3.2 定义配置解析器
public class RedisMutexLockPropertiesParser implements EnvironmentAware {
private Environment environment;
private Binder binder;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
this.binder = Binder.get(this.environment);
parseProperties();
}
}
解析器实现 EnvironmentAware接口可以将Environment注入到当前实例中,这个事件会在Spring应用启动不久,读取配置文件生成环境对象后,对容器中所有实现了该接口的Bean进行回调。上文提到过,无论是 Conditional 还是 Environment, 都是根据属性是否存在而不是层级是否存在来挑选配置的,因此就无法直接通过 environment 获取 lock.redisson.list 下的配置。这里可以使用另一个工具 Binder, Binder 允许我们使用集合或Map来接收这些层级属性配置。
/**
* 解析生成 Properties
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void parseProperties() {
List<Map> mapList;
try {
mapList = binder.bind(RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX, Bindable.listOf(Map.class)).get();
} catch (NoSuchElementException e) {
log.error("Failed To configure RedisMutexLock: '{}' attribute is not specified", RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX);
return;
}
List<RedisMutexLockProperties.RedisMutexLockNodeProperties> propertiesList = new ArrayList<>();
for (Map map : mapList) {
// 这里本质上解析出来的map是 Map<String, String> 类型
final Map<String, Object> propertyMap = (Map<String, Object>) map;
final String schema = (String) propertyMap.get("schema");
final String type = (String) propertyMap.get("type");
final Config config = configParse(propertyMap);
if (config == null)
continue;
RedisMutexLockProperties.RedisMutexLockNodeProperties node = new RedisMutexLockProperties.
RedisMutexLockNodeProperties(schema, type, config);
propertiesList.add(node);
}
properties = new RedisMutexLockProperties(Collections.unmodifiableList(propertiesList));
}
解析方法也很简单,就是通过 Binder 绑定一个键,然后选择合适的对象接收。这里因为不存在 “lock.redissson.list” 这个键,所以我使用了 List<Map> 来接收这个键的内容,表示这个键是集合,集合内部使用 Map 接收
重点放在怎么解析出Config上面,其实也很简单,只要翻阅Config源码,自然就能找到它解析的方式,这里直接将代码copy到类中
/**
* 解析生成 Config
*/
private Config configParse(Map<String, Object> parseMap) {
final Map<String, Object> settings = (Map<String, Object>) parseMap.get("settings");
final ConfigSupport support = new ConfigSupport();
Config config = null;
try {
config = support.fromYAML(yamlMapper.writeValueAsString(settings), Config.class);
} catch (IOException e) {
log.error("Failed to initialize redis mode configuration properties, reasons are ", e);
}
return config;
}
private final ObjectMapper yamlMapper = createMapper(new YAMLFactory(), null);
/**
* @see ConfigSupport#createMapper(JsonFactory, ClassLoader)
*/
private ObjectMapper createMapper(JsonFactory mapping, ClassLoader classLoader) {
ObjectMapper mapper = new ObjectMapper(mapping);
mapper.addMixIn(Config.class, ConfigSupport.ConfigMixIn.class);
mapper.addMixIn(ReferenceCodecProvider.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(AddressResolverGroupFactory.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(Codec.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(RedissonNodeInitializer.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(LoadBalancer.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(NatMapper.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(NameMapper.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(NettyHook.class, ConfigSupport.ClassMixIn.class);
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("classFilter", SimpleBeanPropertyFilter.filterOutAllExcept());
mapper.setFilterProvider(filterProvider);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
if (classLoader != null) {
TypeFactory tf = TypeFactory.defaultInstance()
.withClassLoader(classLoader);
mapper.setTypeFactory(tf);
}
return mapper;
}
OK,这样整个类的部分就完成了。在上面的方法中,我们把解析好的数据重新封装成了 Properties,方便其他类直接使用
3.3 自定义 Bean 注册器
前面提到过,针对多数据源一对一的情况,需要自己手动注册Bean,本文通过Bean注册器来为每个数据源注册相应的Beans。这个类的实现大家直接看代码吧,里面定义了通过 Config 生成了 RedissonClient 对象,并以此生成了 RedisTemplate 等对象。
在上面一节中我们介绍了解析器,解析器解析出来的属性需要暴露给外部使用,而在Bean注册阶段又不能使用注入Bean 的方式引用其他类型Bean的方法(原因下面说),因此选择将解析器作为注册器的内部类来进行声明,作为代价,EnvironmentAware接口从解析器冒泡给了注册器
public class RedisMutexLockBeanRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private static final Logger log = LoggerFactory.getLogger(RedisMutexLockBeanRegister.class);
private Environment environment;
private Binder binder;
private RedisMutexLockPropertiesParser parser;
private final Map<String, Object> registerMap = new ConcurrentHashMap<>();
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
this.binder = Binder.get(this.environment);
this.parser = new RedisMutexLockPropertiesParser(this.binder);
}
/**
* 注册多个 {@link RedissonClient}, 并为每个 RedissonClient 生成对应的 <br>
* {@link RedissonConnectionFactory}, <br>
* {@link StringRedisTemplate}, <br>
* {@link RedisTemplate}, <br>
* {@link RedissonReactiveClient}, <br>
* {@link RedissonRxClient} <br>
* Bean命名规则为 schema 命名空间和类名结合的驼峰式命名
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
final RedisMutexLockProperties properties = parser.getProperties();
boolean onPrimary = true;
for (RedisMutexLockProperties.RedisMutexLockNodeProperties node : properties.getList()) {
final String schema = node.getSchema();
final String type = node.getType();
final Config settings = node.getSettings();
if (settings == null) {
log.error("Missing create beans for schema and type [{}-{}], caused by org.redisson.config.Config is null", schema, type);
continue;
}
final String camelSchema = StringUtil.convertToCamel(schema);
final String clientName = camelSchema + RedissonClient.class.getSimpleName();
final Supplier<RedissonClient> clientSupplier = () -> {
RedissonClient client = (RedissonClient) registerMap.get(clientName);
if (client == null) {
client = Redisson.create(settings);
registerMap.put(clientName, client);
}
return client;
};
final RedissonClient client = clientSupplier.get();
final BeanDefinition clientBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(RedissonClient.class, clientSupplier).getBeanDefinition();
clientBeanDefinition.setPrimary(onPrimary);
registry.registerBeanDefinition(clientName, clientBeanDefinition);
final String factoryName = camelSchema + RedissonConnectionFactory.class.getSimpleName();
final Supplier<RedissonConnectionFactory> factorySupplier = () -> {
RedissonConnectionFactory fac = (RedissonConnectionFactory) registerMap.get(factoryName);
if (fac == null) {
fac = new RedissonConnectionFactory(client);
registerMap.put(factoryName, fac);
}
return fac;
};
final RedissonConnectionFactory factory = factorySupplier.get();
final BeanDefinition factoryBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(RedissonConnectionFactory.class, factorySupplier).getBeanDefinition();
factoryBeanDefinition.setPrimary(onPrimary);
registry.registerBeanDefinition(factoryName, factoryBeanDefinition);
final String stringRedisTemplateName = camelSchema + StringRedisTemplate.class.getSimpleName();
final BeanDefinition stringRedisTemplateBeanDefinition = stringRedisTemplateBeanDefinition(factory, onPrimary);
registry.registerBeanDefinition(stringRedisTemplateName, stringRedisTemplateBeanDefinition);
final String redisTemplateName = camelSchema + RedisTemplate.class.getSimpleName();
final BeanDefinition redisTemplateBeanDefinition = redisTemplateBeanDefinition(factory, onPrimary);
registry.registerBeanDefinition(redisTemplateName, redisTemplateBeanDefinition);
final String redissonReactiveName = camelSchema + RedissonReactiveClient.class.getSimpleName();
final BeanDefinition redissonReactiveBeanDefinition = redissonReactiveBeanDefinition(client, onPrimary);
registry.registerBeanDefinition(redissonReactiveName, redissonReactiveBeanDefinition);
final String redissonRxName = camelSchema + RedissonRxClient.class.getSimpleName();
final BeanDefinition redissonRxBeanDefinition = redissonRxBeanDefinition(client, onPrimary);
registry.registerBeanDefinition(redissonRxName, redissonRxBeanDefinition);
if (onPrimary) onPrimary = false;
// 下一小节——Bean的信息保存
RedisMutexLockInfoHolder.registerName(schema, RedissonClient.class, clientName);
RedisMutexLockInfoHolder.registerName(schema, RedisConnectionFactory.class, factoryName);
RedisMutexLockInfoHolder.registerName(schema, StringRedisTemplate.class, stringRedisTemplateName);
RedisMutexLockInfoHolder.registerName(schema, RedisTemplate.class, redisTemplateName);
RedisMutexLockInfoHolder.registerName(schema, RedissonReactiveClient.class, redissonReactiveName);
RedisMutexLockInfoHolder.registerName(schema, RedissonRxClient.class, redissonRxName);
}
}
/**
* 生成 {@link StringRedisTemplate}
*/
private BeanDefinition stringRedisTemplateBeanDefinition(RedisConnectionFactory factory, boolean onPrimary) {
final GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(StringRedisTemplate.class);
final ConstructorArgumentValues values = new ConstructorArgumentValues();
values.addIndexedArgumentValue(0, factory);
definition.setConstructorArgumentValues(values);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
definition.setPrimary(onPrimary);
return definition;
}
/**
* 生成 {@link RedisTemplate}
*/
private BeanDefinition redisTemplateBeanDefinition(RedisConnectionFactory factory, boolean onPrimary) {
final GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(RedisTemplate.class);
definition.getPropertyValues().add("connectionFactory", factory);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
definition.setPrimary(onPrimary);
return definition;
}
/**
* 懒加载生成 {@link RedissonReactiveClient}
*/
private BeanDefinition redissonReactiveBeanDefinition(RedissonClient client, boolean onPrimary) {
final GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(RedissonReactiveClient.class);
definition.setInstanceSupplier(client::reactive);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
definition.setPrimary(onPrimary);
definition.setLazyInit(true);
return definition;
}
/**
* 懒加载生成 {@link RedissonRxClient}
*/
private BeanDefinition redissonRxBeanDefinition(RedissonClient client, boolean onPrimary) {
final GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(RedissonRxClient.class);
definition.setInstanceSupplier(client::rxJava);
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);
definition.setPrimary(onPrimary);
definition.setLazyInit(true);
return definition;
}
static class RedisMutexLockPropertiesParser {
private Binder binder;
private RedisMutexLockProperties properties;
private final ObjectMapper yamlMapper = createMapper(new YAMLFactory(), null);
public RedisMutexLockPropertiesParser(Binder binder) {
this.binder = binder;
parseProperties();
}
public RedisMutexLockProperties getProperties() {
return properties;
}
/**
* 解析生成 Properties
*/
private void parseProperties() {
List<Map> mapList;
try {
mapList = binder.bind(RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX, Bindable.listOf(Map.class)).get();
} catch (NoSuchElementException e) {
log.error("Failed To configure RedisMutexLock: '{}' attribute is not specified", RedisMutexLockProperties.REDIS_PROPERTY_LIST_PREFIX);
return;
}
List<RedisMutexLockProperties.RedisMutexLockNodeProperties> propertiesList = new ArrayList<>();
for (Map map : mapList) {
final Map<String, Object> propertyMap = (Map<String, Object>) map;
final String schema = (String) propertyMap.get("schema");
final String type = (String) propertyMap.get("type");
final Config config = configParse(propertyMap);
if (config == null)
continue;
RedisMutexLockProperties.RedisMutexLockNodeProperties node = new RedisMutexLockProperties.
RedisMutexLockNodeProperties(schema, type, config);
propertiesList.add(node);
}
properties = new RedisMutexLockProperties(Collections.unmodifiableList(propertiesList));
}
/**
* 解析生成 Config
*/
private Config configParse(Map<String, Object> parseMap) {
final Map<String, Object> settings = (Map<String, Object>) parseMap.get("settings");
final ConfigSupport support = new ConfigSupport();
Config config = null;
try {
config = support.fromYAML(yamlMapper.writeValueAsString(settings), Config.class);
} catch (IOException e) {
log.error("Failed to initialize redis mode configuration properties, reasons are ", e);
}
return config;
}
/**
* @see ConfigSupport#createMapper(JsonFactory, ClassLoader)
*/
private ObjectMapper createMapper(JsonFactory mapping, ClassLoader classLoader) {
ObjectMapper mapper = new ObjectMapper(mapping);
mapper.addMixIn(Config.class, ConfigSupport.ConfigMixIn.class);
mapper.addMixIn(ReferenceCodecProvider.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(AddressResolverGroupFactory.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(Codec.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(RedissonNodeInitializer.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(LoadBalancer.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(NatMapper.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(NameMapper.class, ConfigSupport.ClassMixIn.class);
mapper.addMixIn(NettyHook.class, ConfigSupport.ClassMixIn.class);
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("classFilter", SimpleBeanPropertyFilter.filterOutAllExcept());
mapper.setFilterProvider(filterProvider);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
if (classLoader != null) {
TypeFactory tf = TypeFactory.defaultInstance()
.withClassLoader(classLoader);
mapper.setTypeFactory(tf);
}
return mapper;
}
}
}
4. 信息保存与API操作
终于,在我们的努力下,我们成功将Bean注册到了容器中,现在,我们可以使用了!嗯?我怎么不知道我保存的bean呢…
是的,在上面的流程中,我们成功的将Bean都注册到了容器中,也成功为锁的创建提供了充足的条件,但是当我们想要创建锁的时候,一个巨大的问题出现了:我的Client呢?
我们创建锁的前提条件 RedissonClient 被注入到了容器中,但是我们没有任何记录,这就导致我们根本无法使用我们创建的Bean,我们不可能知道每个Bean的名字,然后在代码中显式地指定,这样的代码耦合太深,侵入性太大,所以我们需要一个信息持有者RedisMutexLockInfoHolder
正如名字所言,RedisMutexLockInfoHolder专门记录配置过程中的Bean信息,我可以直接将代码贴出来,方便理解这个类究竟要干什么
public class RedisMutexLockInfoHolder implements ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(RedisMutexLockSupport.class);
private static ApplicationContext ctx;
private static final Map<String, Map<Class<?>, Object>> schemaClassBeanMap = new HashMap<>();
private static final Map<Class<?>, Map<String, Object>> classNameBeanMap = new HashMap<>();
private static final Map<String, String> nameSchemaMap = new HashMap<>();
private static final Map<String, Object> nameBeanMap = new HashMap<>();
private RedisMutexLockInfoHolder() {
}
@SuppressWarnings("unchecked")
public static <T> T getBeanBySchemaAndType(String schema, Class<T> type) {
final Map<Class<?>, Object> classObjectMap = schemaClassBeanMap.get(schema);
if (classObjectMap == null)
return null;
return (T) classObjectMap.get(type);
}
public static Map<Class<?>, Object> getClassBeanMapBySchema(String schema) {
return schemaClassBeanMap.get(schema);
}
public static Object getBeanByName(String name) {
return nameBeanMap.get(name);
}
public static Map<String, Object> getNameBeanMapByClass(Class<?> clazz) {
return classNameBeanMap.get(clazz);
}
static void registerName(String schema, Class<?> clazz, String beanName) {
if (!schemaClassBeanMap.containsKey(schema)) {
schemaClassBeanMap.put(schema, new HashMap<>());
}
if (!classNameBeanMap.containsKey(clazz)) {
classNameBeanMap.put(clazz, new HashMap<>());
}
final Map<Class<?>, Object> classMap = schemaClassBeanMap.get(schema);
if (classMap.containsKey(clazz)) {
log.warn("Failed to register for bean [{} {}]: class in schema has already existed", clazz, beanName);
return;
}
final Map<String, Object> nameMap = classNameBeanMap.get(clazz);
if (nameMap.containsKey(beanName)) {
log.warn("Failed to register for bean [{} {}]: name in class has already existed", clazz, beanName);
return;
}
if (nameSchemaMap.containsKey(beanName) || nameBeanMap.containsKey(beanName)) {
log.warn("Failed to register for bean [{} {}]: schema for name has already existed", clazz, beanName);
}
classMap.put(clazz, null);
nameMap.put(beanName, null);
nameSchemaMap.put(beanName, schema);
nameBeanMap.put(beanName, null);
}
static boolean disabled() {
return nameSchemaMap.isEmpty() && nameBeanMap.isEmpty();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RedisMutexLockInfoHolder.ctx = applicationContext;
for (Class<?> aClass : classNameBeanMap.keySet()) {
for (Map.Entry<String, ?> entry : ctx.getBeansOfType(aClass).entrySet()) {
final String beanName = entry.getKey();
final Object beanObject = entry.getValue();
final Map<String, Object> nameBeanMap = classNameBeanMap.get(aClass);
final String schema = nameSchemaMap.get(beanName);
if (null == nameBeanMap || null == schema) {
log.warn("Unregister bean for [{} {}]", aClass, beanName);
continue;
}
final Map<Class<?>, Object> classBeanMap = schemaClassBeanMap.get(schema);
if (null == classBeanMap) {
log.warn("Failed to store bean for [{} {}]: System Running Exception, schemaClassBeanMap not initialize", aClass, beanName);
continue;
}
nameBeanMap.put(beanName, beanObject);
classBeanMap.put(aClass, beanObject);
RedisMutexLockInfoHolder.nameBeanMap.put(beanName, beanObject);
}
}
}
}
很简单!它仅仅内部使用了几个Map来维护 schema, name, bean 之间的关系,比较显眼的是它实现了 ApplicationContextAware 接口,这是因为在上面的 注册阶段,我们注册的并不是Bean,而是 BeanDefinition。它只是提供了这个 Bean 究竟要怎样去定义,还没有完全生成(当然有的直接就生成了),而 ApplicationContextAware 在容器构建后就会触发回调,这时候容器的Bean已经全部生成,并注入了依赖。我们就可以通过名称来获取Bean,并添加到Map中,方便API调用。
这里的API指的是供内部使用的API,而用户API还是不建议直接使用
在 RedisMutexLockInfoHolder类定义完毕后,我们终于集齐了最后一块拼图,可以定义 RedisMutexLockSupport 这个 API 操作类了,其实真的很简单
public class RedisMutexLockSupport {
private static final Logger log = LoggerFactory.getLogger(RedisMutexLockSupport.class);
@SuppressWarnings("all")
public static boolean isLockEnabled() {
return !RedisMutexLockInfoHolder.disabled();
}
public static RMLock createLock(String schema, String lockName) {
if (!isLockEnabled()) {
log.warn("RedisMutexLock unable to use, getting lock[{}-{}] failure", schema, lockName);
return null;
}
final RedissonClient client = getClient(schema);
if (client == null) {
log.warn("Failed to get lock from client: schema [{}] not exist", schema);
return null;
}
return RedisMutexLockBuilder.fromILock(client.getLock(lockName));
}
public static RMLock createLock(String schema, String lockName, boolean fair) {
if (!isLockEnabled()) {
log.warn("RedisMutexLock unable to use, getting lock[{}-{}-{}] failure", schema, lockName, "fair");
return null;
}
final RedissonClient client = getClient(schema);
if (client == null) {
log.warn("Failed to get lock from client: schema [{}] not exist", schema);
return null;
}
if (fair)
return RedisMutexLockBuilder.fromILock(client.getLock(lockName));
else
return RedisMutexLockBuilder.fromFairLock((RedissonFairLock) client.getFairLock(lockName));
}
public static RedissonClient getClient(String schema) {
if (!isLockEnabled()) {
log.warn("RedisMutexLock unable to use, getting client[{}] failure", schema);
return null;
}
return RedisMutexLockInfoHolder.getBeanBySchemaAndType(schema, RedissonClient.class);
}
}
如你所见,它就是调用了 InfoHolder 类来获取Bean,并创建锁对象。代码中的 RedisMutexLockBuilder 则是为了解决兼容性问题,毕竟系统暴露的是 RMLock,而Redisson提供的则是RLock。这个最后再说
5. 自动化
终于到了文章重点中的重点:自动化/模块化
在学习 Spring 的时候,我们就知道将类实例注册为Bean的方式不仅有 @Bean, @Component, @Configuration,还有 @Import 这个注解,@Import注解才是真正的主角,王牌中的王牌
在上面我们定义的类如 BeanRegister, Aspect, InfoHolder 等都需要注册为 Bean 才能享用回调/启用AOP。我们当然可以直接通过在类上面打上 @Component 注解来将这个 Bean 注册到容器中。问题是这样做仅仅也就是将 类实例化注册为Bean了,它无法控制是否将类实例化注册。还记得我们前面提到过的 @EnableRedisMutexLock 吗?我们需要它来控制是否启用当前模块,而不是让它仅仅成为一个花瓶。
因此,我们需要创建 RedisMutexLockAutoConfiguration 类,让 @EnableRedisMutexLock 来 import 上面的类,从而控制整个模块的加载。
@Inherited
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RedisMutexLockAutoConfiguration.class})
@EnableAspectJAutoProxy
public @interface EnableRedisMutexLock {
}
其中可以看到在注解上打了 @EnableAspectJAutoProxy,因为我们要使用切面功能,所以主动加上这个注解,毕竟我们不知道项目究竟是否有该环境。
@ConditionalOnClass({Redisson.class, RedisOperations.class})
@AutoConfigureAfter({RedisAutoConfiguration.class})
@EnableConfigurationProperties({RedisMutexLockProperties.class})
@Import({RedisMutexLockInfoHolder.class})
public class RedisMutexLockAutoConfiguration {
@Bean
public RedisMutexLockBeanRegister redisMutexLockBeanRegister() {
return new RedisMutexLockBeanRegister();
}
}
上面就是 RedisMutexLockAutoConfiguration 的代码了,怎么是不是很失望,感觉没有期望中的功能那么多?
可以注意到这个类上面没有 @Configuration 注解,这是因为 @Import 注解可以让被import类按照配置类来加载,所以不用担心不加 @Configuration 会失效。反而加了不能控制是否自动装配了。
写到这里,就是整个自动化/模块化的配置了,一路走下来,其实最重要的就是 @Import,它引入别的类,使你看不到类真正注册到容器的过程,从而无感知自动化了。
6. 委托封装
最后一步,到了这一步其实已经不是很重要了。完全可以去除 RMLock 而直接使用 RLock 来操作 Redisson。不过有些人可能需要这样的功能,那就必须使用委托功能。
public abstract class RedisMutexBaseLock implements RMLock, RExpirable, RObject, RObjectAsync {
protected final RLock lock;
protected final RExpirable expirable;
RedisMutexBaseLock(RLock lock, RExpirable expirable) {
this.lock = lock;
this.expirable = expirable;
}
// 省略方法
@Override
public RFuture<Void> unlockAsync(long threadId) {
return lock.unlockAsync(threadId);
}
@Override
public long remainTimeToLive() {
return expirable.remainTimeToLive();
}
// 省略方法
}
上面选择两个有代表性的方法,因为这两个连续的方法刚好委托给了不同的对象。有人可能好奇明明操作的是 RedisssonLock,但是会有两个操作对象呢?这是因为 RedisssonBaseLock 继承自 RedissonExpirable, 实现了 RLock接口。而前者实现了 RExpirable接口,这两个接口没有任何关联,无法统一起来,因此只能构造两个参数进行分别委托。
接下来就简单了,仿照 RedissonBaseLock 实现 RedisMutexBaseLock,并委托实现所有方法。接下来对于每个 Lock 生成一个对应的 Lock 就可以了。如对于 RedissonFairLock,就创建 RedisMutexFairLock,继承 RedisMutexBaseLock即可。这里不再赘述
三、总结
本文使用 SpringBoot 对 Redissson 进行了封装,实现了 注解驱动的Redis分布式锁模块。通过在本文阅读的过程中,您能逐步了解SpringBoot模块化的背后,并对自己开发模块有了一定的理解。
本文以注解驱动实现Redis分布式锁的方式,介绍Spring Boot模块化开发。先进行技术选型,确定用Redisson框架;从配置文件、注解、API三方面设计方案。接着阐述实现过程,包括锁接口、注解与切面、配置文件读取解析等,最终完成自动化配置和委托封装。

295

被折叠的 条评论
为什么被折叠?



