JavaWeb项目架构之Redis分布式日志队列

架构、分布式、日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个 Redis 做消息队列罢了。

前言

为什么需要消息队列?

当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消息队列,作为抽象层,弥合双方的差异。

比如我们系统中常见的邮件、短信发送,把这些不需要及时响应的功能写入队列,异步处理请求,减少响应时间。

如何实现?

成熟的 JMS 消息队列中间件产品市面上有很多,但是基于目前项目的架构以及部署情况,我们采用 Redis 做消息队列。

为什么用 Redis?

Redis 中 list 数据结构,具有“双端队列”的特性,同时 redis 具有持久数据的能力,因此 redis 实现分布式队列是非常安全可靠的。

它类似于 JMS 中的“Queue”,只不过功能和可靠性 (事务性) 并没有 JMS 严格。Redis 本身的高性能和 "便捷的" 分布式设计(replicas,sharding), 可以为实现 "分布式队列" 提供了良好的基础。

提供者端

项目采用第三方 redis 插件 spring-data-redis,不清楚如何使用的请自行谷歌或者百度。

redis.properties:

#redis 配置中心 
redis.host=192.168.1.180
redis.port=6379
redis.password=123456
redis.maxIdle=100 
redis.maxActive=300 
redis.maxWait=1000 
redis.testOnBorrow=true 
redis.timeout=100000

redis 配置:

    <!-- redis 配置 -->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" />
	<bean id="jedisConnectionFactory"
		class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<property name="hostName" value="${redis.host}" />
		<property name="port" value="${redis.port}" />
		<property name="password" value="${redis.password}" />
		<property name="timeout" value="${redis.timeout}" />
		<property name="poolConfig" ref="jedisPoolConfig" />
		<property name="usePool" value="true" />
	</bean>
	<bean id="redisTemplate"  class="org.springframework.data.redis.core.StringRedisTemplate">
		<property name="connectionFactory" ref="jedisConnectionFactory" />
	</bean>

切面日志配置 (伪代码):

/**
 * 系统日志,切面处理类
 * 创建者 张志朋
 * 创建时间	2018 年 1 月 15 日
 */
@Component
@Scope
@Aspect
public class SysLogAspect {
<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> RedisTemplate&lt;String, String&gt; redisTemplate;
<span class="hljs-comment">//注解是基于swagger的API,也可以自行定义</span>
<span class="hljs-meta">@Pointcut("@annotation(io.swagger.annotations.ApiOperation)")</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">logPointCut</span><span class="hljs-params">()</span> { 

}

<span class="hljs-meta">@Around("logPointCut()")</span>
<span class="hljs-keyword">public</span> Object <span class="hljs-title function_">around</span><span class="hljs-params">(ProceedingJoinPoint point)</span> <span class="hljs-keyword">throws</span> Throwable {
	<span class="hljs-type">Object</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> point.proceed();
	<span class="hljs-comment">//把日志消息写入itstyle_log频道</span>
	redisTemplate.convertAndSend(<span class="hljs-string">"itstyle_log"</span>,<span class="hljs-string">"日志数据,自行处理"</span>);
	<span class="hljs-keyword">return</span> result;
}

}

消费者端

Redis 配置:

    <!-- redis 配置 -->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" />
<span class="hljs-tag">&lt;<span class="hljs-name">bean</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"jedisConnectionFactory"</span>
	<span class="hljs-attr">class</span>=<span class="hljs-string">"org.springframework.data.redis.connection.jedis.JedisConnectionFactory"</span>&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"hostName"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${redis.host}"</span> /&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"port"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${redis.port}"</span> /&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${redis.password}"</span> /&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"timeout"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"${redis.timeout}"</span> /&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"poolConfig"</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"jedisPoolConfig"</span> /&gt;</span>
	<span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"usePool"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"true"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">bean</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">bean</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"redisTemplate"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"org.springframework.data.redis.core.RedisTemplate"</span>  
                <span class="hljs-attr">p:connection-factory-ref</span>=<span class="hljs-string">"jedisConnectionFactory"</span>&gt;</span>  
    <span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"keySerializer"</span>&gt;</span>  
        <span class="hljs-tag">&lt;<span class="hljs-name">bean</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"org.springframework.data.redis.serializer.StringRedisSerializer"</span> /&gt;</span>  
    <span class="hljs-tag">&lt;/<span class="hljs-name">property</span>&gt;</span>  
    <span class="hljs-tag">&lt;<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"hashKeySerializer"</span>&gt;</span>  
        <span class="hljs-tag">&lt;<span class="hljs-name">bean</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"org.springframework.data.redis.serializer.StringRedisSerializer"</span> /&gt;</span>  
    <span class="hljs-tag">&lt;/<span class="hljs-name">property</span>&gt;</span>  
<span class="hljs-tag">&lt;/<span class="hljs-name">bean</span>&gt;</span>

<span class="hljs-comment">&lt;!-- 监听实现类 --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">bean</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"listener"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"com.itstyle.market.common.listener.MessageDelegateListenerImpl"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">bean</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"stringRedisSerializer"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"org.springframework.data.redis.serializer.StringRedisSerializer"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">redis:listener-container</span> <span class="hljs-attr">connection-factory</span>=<span class="hljs-string">"jedisConnectionFactory"</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- topic代表监听的频道,是一个正规匹配  其实就是你要订阅的频道--&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">redis:listener</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"listener"</span> <span class="hljs-attr">serializer</span>=<span class="hljs-string">"stringRedisSerializer"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"handleLog"</span> <span class="hljs-attr">topic</span>=<span class="hljs-string">"itstyle_log"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">redis:listener-container</span>&gt;</span> 

监听接口:

public interface MessageDelegateListener {
    public void handleLog(Serializable message);
}

监听实现:

public class MessageDelegateListenerImpl implements MessageDelegateListener {
	    @Override
	    public void handleLog(Serializable message) {
	        if(message == null){
	            System.out.println("null");
	        }else {
	            // 处理日志数据
	        }
	    }
}

Q&A

  • 【问题一】为什么使用 Redis?
    上面其实已经有做说明,尽管市面上有许多很稳定的产品,比如可能大家会想到的 Kafka、RabbitMQ 以及 RocketMQ。但是由于项目本身使用了 Redis 做分布式缓存,基于省事可行的原则就选定了 Redis。

  • 【问题二】日志数据如何存储?
    原则上是不建议存储到关系数据库的,比如 MySql,毕竟产生的日志数量是巨大的,建议存储到 Elasticsearch 等非关系型数据库。

  • 【问题三】切面日志收集是如何实现的?
    切面日志需要引入 spring-aspects 相关 Jar 包,并且配置使 Spring 采用 CGLIB 代理 <aop:aspectj-autoproxy proxy-target-class="true" />。

开源项目源码 (参考):https://gitee.com/52itstyle/spring-boot-mail