如何实现一个统计文章访问信息和访问量的需求
今天来聊另一个需求,我们看到所有文章网站都有记录阅读量的这么个功能,现在我们也要来实现这个功能,因为我们前面聊过基于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的配置和使用更为复杂;同时消息的序列化、网络传输和持久化等操作会带来额外的性能开销;而且需要单独的消息队列服务器或服务,可能涉及额外的维护成本
- 无论使用什么技术首先要结合我们的实际业务需求来考量,满足当前或未来一段时间内的业务需求即可
- 技术本质是为服务业务的,所以并没有什么所谓的完美技术方案
- 当前技术方案在实现当前业务需求的前提下,业务同时应了解到可能存在的风险和问题并且能够接受,对于可能存在的问题和风险有相应的解决方案,也就是常说的兜底方案
- 本文标签: java AOP spring
- 本文链接: https://www.58cto.cn/article/45
- 版权声明: 本文由程序言原创发布, 非商业性可自由转载、引用,但需署名作者且注明文章出处:程序言 》 如何实现一个统计文章访问信息和访问量的需求 - https://www.58cto.cn/article/45