基于Redis实现的日志记录组件——超实用(3)核心类描述

收录于墨的2020~2021开发经验总结

上一篇 基于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));
        }
    }
}

未完待续~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值