又是一篇關於姿勢的文章,為什么是”又”呢?因為上個星期剛寫完一篇關於Apache Commons Pool的正確使用姿勢的文章,點擊此處閱讀。
Redis為我們提供便利的同時,我們也要善待里面的數據
Redis是我們數據的保管者,我們可以隨時存隨時取,大的小的,重要的不重要的,它都毫無怨言的幫我們保存着,甚至有些時候,我們變得很懶,存東西進去的時候順便還貼張紙:“過了一個星期就幫我扔了吧”,對於這些,Redis也都默默的接受了(誰叫Antirez把redis設計的這么好呢)。
這次要寫的就是關於這張留言紙的事。
Redis提供了一套“美好”的過期數據清理機制:
-
主動過期: Redis對數據是惰性過期,當一個key到了過期時間,Redis也不會馬上清理,但如果這個key過期后被再次訪問,Redis就會主動將它清理掉。
-
被動過期: 如果過期的Key一直沒被訪問,Redis也不會一直把它放那不管,它會每秒10次的執行以下的清理工作:
- 隨機從所有帶有過期時間的Key里取出20個
- 如果發現有過期的,就清理
- 如果這里有25%的Key都是過期的,就繼續回到第一步再來一次
這套過期機制設計的很贊,可以這樣理解:如果當前有超過1/4的Key是過期的話,就不停地清理,直到只剩下1/4不到的Key是要過期的為止,然后就慢慢地隨機抽查着清理。
現在我們再說說對待Redis數據的姿勢問題。
先看一個現象,某段程序每天24小時的跑着,但時不時地出現拿不到連接的錯誤:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool at redis.clients.util.Pool.getResource(Pool.java:50) at redis.clients.jedis.JedisPool.getResource(JedisPool.java:86) ... at sun.reflect.GeneratedMethodAccessor54.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85) at com.dianping.cat.annotation.CatTransaction.doServerAround(CatTransaction.java:70) at sun.reflect.GeneratedMethodAccessor52.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:621) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:610) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:68) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:168) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208) at com.sun.proxy.$Proxy102.getRecommendedMediaList(Unknown Source) at com.restful.MediaController.getMediaList(MediaController.java:139) at sun.reflect.GeneratedMethodAccessor53.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:222) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:814) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:737) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) at javax.servlet.http.HttpServlet.service(HttpServlet.java:624) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) at javax.servlet.http.HttpServlet.service(HttpServlet.java:731) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at com.filter.HttpHeaderValidator.doFilter(HttpHeaderValidator.java:198) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at com.filter.CorrelationFilter.doFilter(CorrelationFilter.java:40) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:218) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:956) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:442) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1082) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:623) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1756) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1715) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) Caused by: java.util.NoSuchElementException: Unable to validate object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:506) at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363) at redis.clients.util.Pool.getResource(Pool.java:48) ... 82 more
這個錯誤是因為JedisPool拿到一個連接后,調用 jedis.ping()
方法沒有得到正確的返回”pong”消息,而出現這個問題時,並不是traffic的高峰期,redis的cpu不高,於是程序員很快就懷疑到網絡上,難道有”丟包”?
但跑了幾天,發現出錯的時間固定在某幾個時間點(如晚上9點~10點),莫非是掃地阿姨每天定時在機房打掃衛生,碰着網線了?(有時候不得不佩服程序員的想象力)
然而在一個夜黑風高的晚上,程序員的腦子在這個時候一般是最清醒的,這時告警短信又來了,打開redis監控,像在唯品會上逛街一樣,對着redis的各項監控數據逛啊逛,突然,看到有幾條Expires的數據,當時的表情只能用看到關注了幾天的商品上標了大大”0.1折”時的表情來形容。
接下來的事情,想必大家也都猜到個七七八八了,當然是下單搶購支付了,腦子里一系列的運算,掐指一算: “這個時間點正好有一大批key過期”,而且都是大的Set集合,每個都有幾十萬的數據,再一算: “Redis里的大部分需要過期的key就是這些key”。
於是,答案有了,Redis進行被動過期清理時,發現怎么隨機,都有超過1/4的Key都是過期的,就忙着不停的刪啊刪,再加上又都是大集合的刪除,O(N)的操作復雜度,無奈,等redis刪完時,客戶端那邊的命令都等超時了。
原因找到了,如何解決?看業務場景吧,對於我們的場景,做法是將過期時間設長一點,然后把這些可以刪掉的Key標記一下,丟到一個后台線程那里分頁刪Set里的數據,這樣就算redis再做過期操作,也不會用太多的時間來刪除。
最后,稍稍總結下,對於大個的集合對象,放redis的時候需要額外注意,如果想依賴redis的過期,可能會造成過期時短暫的redis block。
所以,要善待redis數據,比如不用了就自己清理掉,不要等着redis來幫你。
http://neway6655.github.io/redis/2015/12/19/%E5%96%84%E5%BE%85Redis%E9%87%8C%E7%9A%84%E6%95%B0%E6%8D%AE.html