適用場景
- 業務流程遇到大量異步操作,並且業務不是很複雜
- 業務的健壯型要求不高
- 對即時場景要求不高
原理介紹
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; } }