适用场景
- 业务流程遇到大量异步操作,并且业务不是很复杂
- 业务的健壮型要求不高
- 对即时场景要求不高
原理介绍
redis官网文档:https://redis.io/topics/notifications#configuration
spring集成订阅发布:https://docs.spring.io/spring-data/redis/docs/1.7.1.RELEASE/reference/html/#redis:pubsub:subscribe
相关demo
业务发布.java
// 异步通知邮件 String expiredEmail = RedisConstants.REDIS_EXPIRE_Email_Send.getExpired()+ uuid; ValueOperations<Serializable, Object> operations = redisTemplate3.opsForValue(); //由于使用的org.springframework.data.redis.core.StringRedisTemplate,所以value必须是String类型 operations.set(expiredEmail, "1", email_expire_time, TimeUnit.SECONDS);
EmailSyncEventListener.java
package com.redis.listeners; import com.carapi.services.order.FxjTCarInvoiceOrderService; import com.common.constant.RedisConstants; import com.exception.ErrorException; import com.model.fxjTCarInvoiceOrder.FxjTCarInvoiceOrder; import com.model.fxjTCarOrderList.FxjTCarOrderList; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.listener.KeyExpirationEventMessageListener; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import java.nio.charset.StandardCharsets; import java.util.List; /** * 邮件发送 */ public class EmailSyncEventListener extends KeyExpirationEventMessageListener { private static final Logger log = LoggerFactory.getLogger(EmailSyncEventListener.class); //可以使用自动注入,或者xml配置 public EmailSyncEventListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Autowired private FxjTCarInvoiceOrderService tCarInvoiceOrderService; // @Autowired // public EmailSyncEventListener(RedisMessageListenerContainer listenerContainer) { // super(listenerContainer); // } @Override public void onMessage(Message message, byte[] pattern) { String channel = new String(message.getChannel(), StandardCharsets.UTF_8); //过期的key String key = new String(message.getBody(),StandardCharsets.UTF_8); if(StringUtils.isNotEmpty(key) && key.indexOf(RedisConstants.REDIS_EXPIRE_Email_Send.getExpired()) != -1){ System.out.println(key); key = key.substring(key.indexOf(RedisConstants.REDIS_EXPIRE_Email_Send.getExpired())+RedisConstants.REDIS_EXPIRE_Email_Send.getExpired().length()); log.info(key); try { FxjTCarInvoiceOrder invoiceOrder = tCarInvoiceOrderService.selectByPrimaryKey(key); if(invoiceOrder!=null){ tCarInvoiceOrderService.resendEmail(invoiceOrder.getEmail(),invoiceOrder.getInvoiceReqSerialNo()); } } catch (ErrorException e) { log.info("异步发送邮寄失败,验证失败" ); } catch (Exception e) { log.info("异步发送邮件失败"); e.printStackTrace(); } log.info("异步发送邮寄成功"); log.info("redis key 过期:pattern={},channel={},key={}",new String(pattern),channel,key); } } }
spring-cache.xml
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate"> <property name="connectionFactory" ref="jedisConnFactory"></property> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean> </property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/> </property> <property name="hashValueSerializer"> <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/> </property> <property name="stringSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> </bean> <bean id="jedisConnFactory" 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="database" value="2" /> <property name="timeout" value="${redis.timeout}" /> <property name="poolConfig" ref="jedisPoolConfig" /> <property name="usePool" value="true" /> </bean> <!--去掉redis client的CONFIG--> <util:constant static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/> <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="3600" /> </bean> <!-- 将监听实现类注册到spring容器中 --> <bean id="emailSyncEventListener" class="com.redis.listeners.EmailSyncEventListener"> <constructor-arg ref="redisMessageListenerContainer"></constructor-arg> </bean>
出现的问题
- 用户的session存入redis后,redis的负载不平衡,出现了ttl为0的key删除延迟较长
Redis 并不保证生存时间(TTL)变为 0 的键会立即被删除:如果程序没有访问这个过期键, 或者带有生存时间的键非常多的话,那么在键的生存时间变为 0 , 直到键真正被删除这中间,可能会有一段比较显著的时间间隔。
因此,Redis 产生 expired 通知的时间为过期键被删除的时候,而不是键的生存时间变为 0 的时候。如果 Redis 正确配置且负载合理的,延时不会超超过 1s。
RedisExpiredQuartz.java
package com.redis.quart; import com.common.constant.RedisConstants; import com.redis.listeners.EmailSyncEventListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import javax.annotation.Resource; import java.util.Set; /** * Redis 并不保证生存时间(TTL)变为 0 的键会立即被删除: * 如果程序没有访问这个过期键, 或者带有生存时间的键非常多的话, * 那么在键的生存时间变为 0 , * 直到键真正被删除这中间,可能会有一段比较显著的时间间隔。 * * so加个定时器 */ public class RedisExpiredQuartz { private static final Logger log = LoggerFactory.getLogger(EmailSyncEventListener.class); @Resource private RedisTemplate redisTemplate3; public synchronized void onRedisExpiredQuartz(){ log.trace("------------------------------------------"); for (RedisConstants value : RedisConstants.values()) { Set keys = redisTemplate3.keys(value.getExpired() + "*"); log.debug("业务需要正常通知的keys:{}",keys); } } }
RedisConstants.java
package com.common.constant; public enum RedisConstants { /** * 月卡过期取消key前缀 */ REDIS_EXPIRE_Sub_Card("redisExpiredSubCard_"), /** * 延时邮件发送key前缀 */ REDIS_EXPIRE_Email_Send("redisExpiredEmail_Send_"), ; private String expired; RedisConstants(String expired) { this.expired = expired; } public String getExpired() { return expired; } public void setExpired(String expired) { this.expired = expired; } // 获得 enum 对象 public static RedisConstants get(String expired) { for (RedisConstants item : values()) { if (expired == item.getExpired()) { return item; } } return null; } }