原创

如何实现一个统计文章访问信息和访问量的需求

温馨提示:
本文最后更新于 2024年02月27日,已超过 330 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

今天来聊另一个需求,我们看到所有文章网站都有记录阅读量的这么个功能,现在我们也要来实现这个功能,因为我们前面聊过基于Spring AOP实现自定义注解来解决一些共性需求,今天我们很容易想到使用自定义注解来实现这个需求,现学现用,就是这么easy,说干就干

1.首先自定义一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadLog {
}

2.实现切面逻辑

@Slf4j
@Aspect
@Component
public class ReadLogAspect {
    @Before("@annotation(readLog)")
    public void doBefore(JoinPoint joinPoint, ReadLog readLog) {
        log.info("记录浏览日志入库.....");
    }
}

那么今天的任务就这么愉快的水完了... (⊙﹏⊙)emm....

虽然这样确实记录了我们的文章阅读日志,请容我稍稍多考虑那么一点点,我们是否可以将日志入库操作异步化,提升文章接口并发访问速度呢?那么这里有哪些实现方式呢?

  • 使用@Async注解异步执行
  • 开启子线程异步执行
  • 使用 Redis 实现发布订阅模式
  • 使用本地阻塞队列实现
  • 使用消息队列中间件,请参考文章RabbitMQ快速入门
  • ....

使用 Redis 实现发布订阅模式

首先我们使用 Redis 发布订阅模式实现该功能:
1.创建生产者,使用RedisTemplate发送消息到Redis List:

@Component
public class ArticleReadLogPublisher {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void publishMessage(String channel, ArticleRead readLog) {
        redisTemplate.convertAndSend(channel, readLog);
    }
}

2.创建消息消费者,指定监听的List,并处理接收到的消息:

@Slf4j
@Component
public class ArticleReadLogSubscriber implements MessageListener {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(pattern);
        Object msg = redisTemplate.getValueSerializer().deserialize(message.getBody());
        log.info("处理接收到的消息 {} {}", channel, JSON.toJSONString(msg));
    }
}

3.在SpringBoot启动时注册监听器:

@Configuration
public class RedisConfiguration {
    @Autowired
    private ArticleReadLogSubscriber articleReadLogSubscriber;

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(new MessageListenerAdapter(articleReadLogSubscriber), new PatternTopic("article_read"));
        return container;
    }
}

4.在切面逻辑实现类里使用Redis发布订阅消息:

ArticleRead readLog = new ArticleRead();
readLog.setArticleId(articleId);
readLog.setUserIp(userIp);
articleReadLogPublisher.publishMessage("article_read", readLog);

使用本地阻塞队列实现

1.定义阻塞队列并分别实现数据添加和读取的方法

@Slf4j
@Component
public class ArticleReadLogTask {
    private final BlockingQueue<ArticleRead> articleQueue = new ArrayBlockingQueue<>(1024);

    public void addRecordToQueue(ArticleRead articleRead) {
        if (Objects.nonNull(articleRead)){
            articleQueue.offer(articleRead);
        }
    }

    public void log() {
        while (true) {
            try {
                ArticleRead read = articleQueue.take();
                log.info("获取阅读记录 {}", JSON.toJSONString(read));
            } catch (InterruptedException e) {
                log.error("获取阅读记录异常", e);
            }
        }
    }
}

2.在项目启动时添加自定义逻辑,这里是开启了一个单线程的线程池来处理队列消息

@Slf4j
@Component
public class ArticleListener implements ServletContextListener {
    @Autowired
    private ArticleReadLogTask articleReadLogTask;

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ThreadFactory factory = new ThreadFactoryBuilder().setNamePrefix("ARTICLE_READ_LOG-").build();

        ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1,
                365L, TimeUnit.DAYS,
                new LinkedBlockingQueue<>(1024),
                factory, new ThreadPoolExecutor.AbortPolicy());
        singleThreadPool.execute(() -> articleReadLogTask.log());
        singleThreadPool.shutdown();
    }
}

3.在切面逻辑实现类里往队列内添加消息数据:

ArticleRead readLog = new ArticleRead();
readLog.setArticleId(articleId);
readLog.setUserIp(userIp);
task.addRecordToQueue(readLog);

文章日志

总结

  • 今天的需求目标已经实现了,那么接下来我们对上面的技术选型做一下简单的总结

  • 从技术的实现难度来说,直接在切面逻辑内日志入库是最简单直观也是最好实现的方案,像我这种小破站这么干一点毛病没有,但对于有一定流量的网站来说肯定更推荐异步方案来实现

  • 下面我们对其它三种方案来做个简单分析:

  • 三个方案都有同样的优势:异步化处理,提高系统响应能力和吞吐量;将日志操作和主业务进行解耦,降低了模块间的耦合度

  • 本地阻塞队列:
    优点:该方案实现相对也比较简单,易于理解和使用;由于在内存中操作,数据传输速度快,延迟低等特性,性能相对来说还是很可观的;而且多线程环境下,可以保证数据的一致性和同步
    缺点:由于是内存中操作,所以受限于内存大小,不能存储大量数据;而且一旦系统崩溃,会造成数据丢失;如果是多服务或者机器之间通信不好实现,当然只是日志来说不存在这个问题

  • Redis发布订阅:
    优点: 消息发布后,订阅者几乎可以立刻收到所订阅的消息;相对于MQ来说,Redis作为内存数据库,更加轻量和快速;支持多个订阅者同时接收消息,且可以订阅多个频道
    缺点: 虽然Redis支持持久化,但发布订阅模式下的消息默认不进行持久化;如果在订阅者订阅之前就发布的消息,就会导致订阅者丢失自己所订阅的消息;而且消息的发送和接收不保证顺序,可能影响业务逻辑的正确性

  • MQ消息队列:
    优点: 消息队列相对前两种方案最大的优势就是提供消息持久化、ACK等机制,能够保证消息不会丢失
    缺点: 相比本地队列和Redis发布订阅,MQ的配置和使用更为复杂;同时消息的序列化、网络传输和持久化等操作会带来额外的性能开销;而且需要单独的消息队列服务器或服务,可能涉及额外的维护成本

  • 无论使用什么技术首先要结合我们的实际业务需求来考量,满足当前或未来一段时间内的业务需求即可
  • 技术本质是为服务业务的,所以并没有什么所谓的完美技术方案
  • 当前技术方案在实现当前业务需求的前提下,业务同时应了解到可能存在的风险和问题并且能够接受,对于可能存在的问题和风险有相应的解决方案,也就是常说的兜底方案
正文到此结束
本文目录