1. 程式人生 > >一次專案使用redis單機的坑

一次專案使用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,除非你能力很強可能解決它會出現的問題