一次專案使用redis單機的坑
引入:最近的專案中,要用redis來做快取,優化一些與資料庫的互動,所以本人也開始了第一次正式的使用。
因為框架中已經把spring的一些特性關掉(例如注入等),然後做了封裝(為了避免一些無用的注入導致浪費資源以及其他一些特性),所以我當時想到的是沒法用spring管理,那就先用連線池jedisPool ,也在網上查閱了一些資料,直接開始著手,用法很普遍,一查一大堆,都是先初始化配置以及連線池,然後每次都從連線池中取出資源來和redis進行互動。
上程式碼:
public void init() throws FAPBusinessException{ Log.info("開始初始化jedis連線池"); try { if(jedisPool==null){ JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(MAX_ACTIVE); config.setMaxIdle(MAX_IDLE); config.setMaxWaitMillis(MAX_WAIT); config.setTestOnBorrow(TEST_ON_BORROW); if(!"-1".equals(PASSWORD)){ jedisPool = new JedisPool(config, HOST, PORT, TIMEOUT,PASSWORD); }else{ jedisPool = new JedisPool(config, HOST, PORT, TIMEOUT); } } } catch (Exception e) { Log.error("JedisInit.init: {}", e); throw new FAPBusinessException("初始化redis配置異常"); } Log.info("redis配置載入完畢"); } public static Jedis getJedis() { if (jedisPool == null) { Log.info("redis初始化失敗"); } Jedis jedis = null; try { if (jedisPool != null) { jedis = jedisPool.getResource(); } } catch (Exception e) { Log.error("getJedis() 方法出錯:" + e.getMessage()); } finally { close(jedis); } return jedis; } /** * 釋放jedis資源 * @param jedis */ public static void close(final Jedis jedis) { try { if (jedis != null && jedisPool != null) { jedis.close(); } } catch (Exception e) { Log.error("close() 方法出錯:" + e.getMessage()); } }
這個是很普遍的,在網上可以一查一大堆,然後按照自己的需要改就可以。
我的用法也是很基礎,在業務層每次都直接getResource拿到一個Jedis物件進行操作原生方法,在擼程式碼的過程中還是很和諧的。but,當進行壓測的時候,問題開始一個又一個的出了,java.lang.ClassCastException: [B cannot be cast to java.lang.Long,要麼就是一些轉化異常,當時看到轉化異常,又對著redis庫中存的內容發現是資料拿亂了,然後就開始想解決辦法,那我可不可以用完以後就關掉連線,然後讓他重新去連線池中拿,這樣不就不會有拿錯的問題了?於是實踐,我就把一些常用的原生方法進行封裝,執行完成之後就close掉。問題更多,大部分都是socket Closed這些問題,然後去確認了一下,連線池是不需要關閉的,只不過是用完就放回去等待下一個執行緒呼叫,然後就開始查資料,原因也查到了,說是多個執行緒同時呼叫了同一個jedis物件,導致記憶體資料被多個執行緒競爭,產生資料混亂 於是開始想解決辦法,如果是因為多個競爭的話,當前首先想到的是同步了唄。於是就把所有呼叫的方法加上了同步鎖。
上程式碼:
public synchronized static String hgetValue(String key,String field) { if (getJedis() == null || !getJedis().exists(key)) { return null; } return getJedis().hget(key,field); } public synchronized static void hsetValue(String key,String field,String value) { if(getJedis()!=null){ getJedis().hset(key,field,value); } } public synchronized static String getValue(String key) { if (getJedis() == null || !getJedis().exists(key)) { return null; } return getJedis().get(key); } public synchronized static void setValue(String key,String value) { if(getJedis()!=null){ getJedis().set(key,value); } } public synchronized static Long llen(String key) { if(getJedis()!=null){ return getJedis().llen(key); } return null; }
進行測試,哎?發現有效果,於是興致沖沖的去壓測,果然,問題少了很多,等等,嗯?怎麼回事,使用者量越大怎麼又不行了,感覺自己都要炸掉了,開始不停的查資料,諮詢。。。。,中間還試過了用JedisClustor叢集,想著用一臺機器來假裝叢集試下,之後就是按照叢集的方式運作,不僅資料沒有存進去,還把redis服務給搞宕機了。。。。。
經過一天的摧殘,早已不省人事,忽然腦門一亮,想到了RedisTemplate這個東西,專案中沒有spring的注入,我該怎麼使用呢?費盡九牛二虎之力終於把這個東西拿到了。不多說直接甩程式碼
新加類: public class JedisTempLate { private RedisTemplate redisTemplate; private RedisConnectionFactory redisConnectionFactory; public void Init(){ redisTemplate= new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); } public RedisTemplate getRedisTemplate(){ return redisTemplate; } public void setRedisConnectionFactory(RedisConnectionFactory redisConnectionFactory) { this.redisConnectionFactory = redisConnectionFactory; } }
public class SpringCtxHolder implements ApplicationContextAware { protected static ApplicationContext ctx; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringCtxHolder.ctx = applicationContext; } public static <T> T getBean(Class<T> cls) { if (SpringCtxHolder.ctx == null) { Log.warn("sss"); return null; } return ctx.getBean(cls); } }
spring配置:
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.maxIdle}"/> <property name="minIdle" value="${redis.minIdle}"/> <property name="maxTotal" value="${redis.maxTotal}"/> <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/> <property name="testOnBorrow" value="true"/> </bean> <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="poolConfig" ref="jedisPoolConfig" /> <property name="password" value="${redis.pass}"/> <property name="usePool" value="true"/> </bean> <bean id="jedisTempLate" class="com.rrtx.payment.util.JedisTempLate" init-method="Init" scope="singleton"> <property name="redisConnectionFactory" ref="jedisConnectionFactory"/> </bean> <bean class="com.rrtx.payment.util.SpringCtxHolder" scope="singleton"/> <bean id="redisTemplate" factory-bean="jedisTempLate" factory-method="getRedisTemplate"/>
業務層在spring啟動的時候初始化template:
public class IJedisUtilServiceImpl implements InitializingBean,IJedisUtilService { private RedisTemplate redisTemplate; @Override public void afterPropertiesSet() throws Exception { this.redisTemplate= SpringCtxHolder.getBean(RedisTemplate.class); } @Override public String hgetValue(String key, String field) { if (redisTemplate== null) { return null; } return (String) redisTemplate.opsForHash().get(key,field); }
}
思路也很簡單,SpringCtxHolder 繼承一個spring上下文,然後自己封裝一個可以獲取bean的方法,在配置檔案中初始化,以及初始化自己的這個JedisTemplate方法,對外一共一個方法用來獲取RedisTemplate。IJedisUtilServiceImpl 用來提供一些操作redis的方法。
最後解決了這個問題,直接拋棄了JedisPool連線池,之後查資料發現RedisTemplate內部對連線池進行了封裝,用ThreadLocal對執行緒的資源進行了繫結,解決這個多執行緒呼叫的問題,所以告誡大家如果是有這種併發場景的時候儘量不要去直接使用RedisPool,除非你能力很強可能解決它會出現的問題