上一篇 基于Redis实现的日志记录组件——超实用(2)类图和基本结构
三、核心类介绍
1、RedisLogComponent
RedisComponent是日志组件类,用于存储组件信息,使用了lombok的注解,来减少编码量:
这里的@NoArgsConstructor增加了一个无参构造,因为redis反序列化json为java类时,要求这个java类具有无参构造函数,并且还要有对应属性的getter和setter。
@NoArgsConstructor
@Setter
@Getter
@ToString(exclude = {"logs", "start", "end"})
public class RedisLogComponent {
/**
* 组件名称
*/
private String name;
/**
* 组件描述
*/
private String description;
/**
* 组件过期毫秒数
*/
private long expireMillis;
/**
* 组件日志量最大大小
*/
private long sizeLimit;
/**
* 达到组件最大大小的多少占比时开始进行过期清除
*/
private float checkProportion;
/**
* 组件创建日期时间
*/
private Date createDate;
/**
* 组件更新日期时间
*/
private Date updateDate;
/**
* 组件的日志内容
*/
private List<String> logs;
/**
* 组件的日志的开始下标
*/
private long start;
/**
* 组件的日志的结束下标
*/
private long end;
public RedisLogComponent(String name, String description, long expireMillis,
long sizeLimit, float checkProportion,
Date createDate, Date updateDate) {
this.name = name;
this.description = description;
this.expireMillis = expireMillis;
this.sizeLimit = sizeLimit;
this.checkProportion = checkProportion;
this.createDate = createDate;
this.updateDate = updateDate;
}
}
2、RedisLogger
RedisLogger用于操作redis,进行组件的注册、清空、删除,以及日志查询、日志写入。
每个组件维护三个Redis 对应的缓存,一个存储组件的基本信息,一个存储组件的日志内容(列表),一个存储组件的日志过期时间(列表)。
每个组件维护的一个timestamp队列,其索引指向的值为组件日志的记录时间时间戳。
按日志一样的顺序填充入其入库时间timestamp,当达到数量最大值的某个占比时将会优先删除这些过期的日志。
若达到数量最大限制时,这些日志都是未过期的,那么则会优先删除离现在时间最久的日志,也就是执行lpop。
listReset 根据组件配置,重置列表,清除过期数据,或者裁剪大小。
getComponentLog(String component, long start, long end)
查询组件日志,其中component指定了组件,start、end指定了查询的日志区间段。
log(String component, String opt, String description)
用于日志记录,其中component指定了组件,opt指定了操作,description为日志的详细内容。
RedisLogComponent registerComponent(String name, String description, Long expire, TimeUnit timeUnit, long sizeLimit, float checkProportion)
用于组件的注册,若组件不存在,则会向redis中写入基本信息,并进行初始化,如果组件已存在,则会用传入的配置信息去更新redis的组件信息。
clearComponent(String component)
用于清空组件。
deleteComponent(String component)
用于删除组件,此方法执行要谨慎,因为组件删除之后,后续的日志记录会因为找不到组件而报错。
/**
* 一个简单的Redis日志记录工具
*/
@Slf4j
public class RedisLogger {
@Resource
private RedisUtil redisUtil;
/**
* 组件的KEY前缀
*/
private final static String LOG_COMPONENT_PRE = "LOG_COMPONENT_";
/**
* 组件日志的KEY前缀,每个组件独立维护一份的日志队列
*/
private final static String LOG_PRE = "LOG_";
private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss SSS]");
/**
* 组件的timestamp队列的KEY前缀
* 每个组件维护的一个timestamp队列,其索引指向的值为组件日志的记录时间时间戳
* 按日志一样的顺序填充入其入库时间timestamp,当达到数量最大值的一半时将会优先删除这些过期的日志
* 若达到数量最大限制时,这些日志都是未过期的,那么则会优先删除离现在时间最久的日志,也就是执行lpop
*/
private final static String LOG_EXPIRE_LIST_PRE = "LOG_EXPIRE_LIST_";
/**
* 生成组件的redis key
* @param name 组件名称
* @return 组件的redis key
*/
public String getComponentKey(String name) {
return LOG_COMPONENT_PRE + name;
}
/**
* 生成组件的日志列表的redis key
* @param name 组件名称
* @return 组件的redis key
*/
public String getLogListKey(String name) {
return LOG_PRE + name;
}
/**
* 生成组件的日志过期时间列表的redis key
* @param name 组件名称
* @return 组件的redis key
*/
public String getLogExpireListKey(String name) {
return LOG_EXPIRE_LIST_PRE + name;
}
/**
* 查询组件日志
*
* @param component 组件名称
* @param start 开始下标,-1时直到最右,0时最左
* @param end 结束下标,-1时直到最右,0时最左,-1,-1获取最右的一个
* @return 查询到的日志信息
* @author hengyumo
* @since 20210616
*/
public RedisLogComponent getComponentLog(String component, long start, long end) {
String componentKey = getComponentKey(component);
Object obj = redisUtil.getObject(componentKey);
String logListKey = getLogListKey(component);
if (obj instanceof RedisLogComponent redisLogComponent) {
List<String> logs = redisUtil.lrange(logListKey, start, end)
.stream().map(Object::toString)
.collect(Collectors.toList());
redisLogComponent.setLogs(logs);
redisLogComponent.setStart(start);
redisLogComponent.setEnd(start + logs.size() - 1);
return redisLogComponent;
} else {
throw new RuntimeException("不存在组件" + component + ",请先注册");
}
}
/**
* 执行日志记录
*
* @param component 所属组件
* @param opt 操作类型,操作类型不能包含':'号!
* @param description 描述
* @author hengyumo
* @since 20210616
*/
public void log(String component, String opt, String description) {
Assert.isTrue(StringUtils.hasLength(component), "component组件名称不能为空!");
Assert.isTrue(StringUtils.hasLength(opt), "操作类型不能为空!");
Assert.isTrue(!opt.contains(":"), "操作类型不能包含':'号!");
String componentKey = getComponentKey(component);
Object obj = redisUtil.getObject(componentKey);
if (obj instanceof RedisLogComponent redisLogComponent) {
String logListKey = getLogListKey(component);
String logExpireListKey = getLogExpireListKey(component);
String value = DATE_FORMAT.format(new Date()) + ":" + opt + ":" + description;
// 此处用线程同步锁,如果后续升级分布式服务需改用分布式锁
synchronized (componentKey.intern()) {
redisUtil.rpush(logListKey, value);
redisUtil.rpush(logExpireListKey, System.currentTimeMillis());
listReset(redisLogComponent);
}
} else {
throw new RuntimeException("不存在组件" + component + ",请先注册");
}
}
/**
* 根据组件配置,重置列表,清除过期数据,或者裁剪大小
*
* @author hengyumo
* @since 20210616
*/
private void listReset(RedisLogComponent redisLogComponent) {
String componentName = redisLogComponent.getName();
long sizeLimit = redisLogComponent.getSizeLimit();
float checkProportion = redisLogComponent.getCheckProportion();
long expireMillis = redisLogComponent.getExpireMillis();
String logListKey = getLogListKey(componentName);
String logExpireListKey = getLogExpireListKey(componentName);
// 此处用线程同步锁,如果后续升级分布式服务需改用分布式锁
synchronized (componentName.intern()) {
Long logListSize = redisUtil.llen(logListKey);
Long logExpireListSize = redisUtil.llen(logExpireListKey);
if (!logListSize.equals(logExpireListSize)) {
log.error("不同步,请检查RedisLog组件{}:logListSize:{},logExpireListSize:{}",
componentName, logListSize, logExpireListSize);
if (logListSize > logExpireListSize) {
redisUtil.ltrim(logListKey, logListSize - logExpireListSize, logListSize);
logListSize = logExpireListSize;
} else {
redisUtil.ltrim(logExpireListKey, logExpireListSize - logListSize, logExpireListSize);
logExpireListSize = logListSize;
}
}
long checkNum = sizeLimit == 0 ? 0 : (long) (sizeLimit * checkProportion);
if (checkNum != 0 && logListSize > checkNum) {
// 队列达到检查数,尝试进行清除,清除过期的日志
Object obj = redisUtil.lindex(logExpireListKey, 0L);
// 最左过期才开始进行检查,因为最左的数据是最久的
if (obj instanceof Long timeStampLeftFirst) {
boolean isExpire = (System.currentTimeMillis() - timeStampLeftFirst) > expireMillis;
if (isExpire) {
// 尝试用折半查找找到过期的临界点
long left = 0, right = logExpireListSize, href = logExpireListSize / 2;
do {
obj = redisUtil.lindex(logExpireListKey, href);
isExpire = false;
if (obj instanceof Long timestamp) {
isExpire = (System.currentTimeMillis() - timestamp) > expireMillis;
}
if (isExpire) {
left = href;
} else {
right = href;
}
href = left + (right - left) / 2;
}
while (right - left > 1);
// 从临界点到终点切分
redisUtil.ltrim(logListKey, href, logListSize);
redisUtil.ltrim(logExpireListKey, href, logExpireListSize);
logListSize = logListSize - href;
logExpireListSize = logExpireListSize - href;
}
} else {
log.error("无法读取最左的时间戳:{}", componentName);
}
}
if (sizeLimit != 0 && logExpireListSize > sizeLimit) {
// 队列已满从左优先开始剔除
long left = logExpireListSize - sizeLimit;
redisUtil.ltrim(logListKey, left, logListSize);
redisUtil.ltrim(logExpireListKey, left, logExpireListSize);
}
}
}
/**
* 注册组件,初始化组件的列表数据,并设置组件的日志过期时间,如果该组件已经注册过则会覆盖其对应的属性
*
* @param name 组件名称
* @param description 组件描述
* @param expire 组件的日志的过期时间,组件的所有日志共用一个过期时间
* @param timeUnit 组件日志的过期时间的单位
* @param sizeLimit 组件的日志的大小限制
* @param checkProportion 组件的日志占最大限制占比达到多少时开始进行过期检查
* @return 组件
* @author hengyumo
* @since 20210616
*/
public RedisLogComponent registerComponent(String name, String description, Long expire,
TimeUnit timeUnit, long sizeLimit, float checkProportion) {
Assert.isTrue(0 <= checkProportion && checkProportion <= 1,
"checkProportion 组件的日志占最大限制占比应该在0-1之间!");
Assert.isTrue(StringUtils.hasLength(name), "component组件名称不能为空!");
String componentKey = getComponentKey(name);
Object obj = redisUtil.getObject(componentKey);
if (expire <= 0) {
log.warn("设置RedisLogger日志永不过期,请您慎重:组件{}", name);
expire = 0L;
}
if (sizeLimit <= 0) {
log.warn("设置RedisLogger日志大小无限制,请您慎重:组件{}", name);
sizeLimit = 0L;
}
RedisLogComponent newComponent = new RedisLogComponent(
name, description, timeUnit.toMillis(expire), sizeLimit, checkProportion, new Date(), new Date());
if (obj instanceof RedisLogComponent redisLogComponent) {
newComponent.setCreateDate(redisLogComponent.getCreateDate());
// 已存在则更新
log.info("已存在组件:{},属性{},覆盖:{}", name, redisLogComponent.toString(), newComponent.toString());
}
// 设置组件的属性,0为永不过期
redisUtil.setObject(componentKey, newComponent, 0, TimeUnit.SECONDS);
return newComponent;
}
/**
* 清空组件的日志信息
*
* @param component 组件名称
* @return 是否成功
* @author hengyumo
* @since 20210616
*/
public boolean clearComponent(String component) {
// 此处用线程同步锁,如果后续升级分布式服务需改用分布式锁
synchronized (component.intern()) {
return redisUtil.del(getLogListKey(component)) && redisUtil.del(getLogExpireListKey(component));
}
}
/**
* 删除组件的信息,并清空所有日志
*
* @param component 组件名称
* @return 是否成功
* @author hengyumo
* @since 20210616
*/
public boolean deleteComponent(String component) {
// 此处用线程同步锁,如果后续升级分布式服务需改用分布式锁
synchronized (component.intern()) {
return clearComponent(component) && redisUtil.del(getComponentKey(component));
}
}
}
未完待续~