大白話講解呼叫Redis的increment失敗原因及推薦使用

哎喔別走發表於2021-11-10

        大家在專案中基本都會接觸到redis,在spring-data-redis-2.*.*.RELEASE.jar中提供了兩個Helper class,可以讓我們更方便的操作redis中儲存的資料。這兩個Helper class分別是RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate在儲存String型別的時候的一個擴充套件子類。所以大家在使用redis的時候:

1、如果操作的是String型別,優先考慮用StringRedisTemplate;

2、如果是複雜物件型別,則有限考慮RedisTemplate。

       如果大家在使用redis來進行計數場景,比如記錄呼叫次數、記錄介面的呼叫閾值等,用RedisTemplate出現了以下錯誤ERR value is not an integer or out of range,那麼先說下解決方案,如下:

//類中直接注入StringRedisTemplate 
@Autowired
private StringRedisTemplate stringRedisTemplate;

//方法體中用以下方式進行計數
stringRedisTemplate.opsForValue().increment("check:incr:str");

即用StringRedisTemplate代替RedisTemplate。

        出現上面的原因,就是因為序列化導致的。我們知道資料是以二進位制形式儲存在redis的,那麼就必然涉及到序列化和反向序列化,上面提到的這兩個Helper class就可以自動的幫我們實現序列化和反向序列,其中二者的主要區別就是序列化機制,

1、StringRedisTemplate的序列化機制是通過StringRedisSerializer來實現的;

2、RedisTemplate的序列化機制是通過JdkSerializationRedisSerializer來實現的。

 

        increment操作底層就是讀取資料,然後+1,然後set,只不過這三個步驟被redis加了原子操作保證,所以我們從StringRedisTemplate和RedisTemplate的set方法來分析。先看StringRedisTemplate的原始碼如下:

1、stringRedisTemplate.opsForValue().set("check:incr:str", "1");
2、檢視第一步的set方法,進入DefaultValueOperations類的set方法
@Override
public void set(K key, V value) {
  byte[] rawValue = rawValue(value);
  execute(new ValueDeserializingRedisCallback(key) {
    @Override
    protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
      connection.set(rawKey, rawValue);
      return null;
    }
  }, true);
}
3、檢視第二步的處理value的rawValue方法,進入AbstractOperations.rawValue方法
@SuppressWarnings("unchecked")
byte[] rawValue(Object value) {
    if (valueSerializer() == null && value instanceof byte[]) {
        return (byte[]) value;
    }
    return valueSerializer().serialize(value);
}
4、看到第三步最終呼叫了序列化類對value做了序列化處理,我們知道StringRedisTemplate的序列化類是StringRedisSerializer,
可知第三步的最後一行serialize最後呼叫瞭如下方法
@Override
public byte[] serialize(@Nullable String string) {
    return (string == null ? null : string.getBytes(charset));
}

5、到此我們知道了,value最後存在redis的最底層原理就是第四步的return返回的byte[]。那麼就可以這樣認為,如果呼叫了
stringRedisTemplate.opsForValue().set(
"check:incr:str", "1");就好比是在redis儲存了"1".getBytes("UTF-8")

public static void main(String[] args) throws UnsupportedEncodingException {
  byte[] str = "1".getBytes("UTF-8");
  for(int i = 0 ; i< str.length; i++) {
    System.out.println(str[i]);
  }
}

結論:

上面main方法列印出來了49,瞭解了set方法後,可以猜測呼叫increment時相當於對49直接加1,變成50,get資料時再反向序列化可知對應是數字2

(注意標綠的是我的猜測,可以比較容易理解為什麼可以直接increment,事實是否這樣需要看redis原始碼,若有同學確認了,可以回覆下我),

所以可以理解為什麼StringRedisTemplate支援increment。

(注意這裡並不是說RedisTemplate不支援,而是說RedisTemplate的預設配置的序列化實現機制,會導致RedisTemplate不支援increment)

        接下來以同樣的方式,看RedisTemplate的原始碼,如下:

 

1、根據redisTemplate.opsForValue().set("check:incr:obj", 1);檢視set方法
2、檢視第一步的set方法,進入DefaultValueOperations類的set方法
@Override
public void set(K key, V value) {
  byte[] rawValue = rawValue(value);
  execute(new ValueDeserializingRedisCallback(key) {
    @Override
    protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
      connection.set(rawKey, rawValue);
      return null;
    }
  }, true);
}
3、檢視第二步的處理value的rawValue方法,進入AbstractOperations.rawValue方法
@SuppressWarnings("unchecked")
byte[] rawValue(Object value) {
    if (valueSerializer() == null && value instanceof byte[]) {
        return (byte[]) value;
    }
    return valueSerializer().serialize(value);
}
4、看到第三步最終呼叫了序列化類對value做了序列化處理,我們知道RedisTemplate的序列化類是JdkSerializationRedisSerializer,
可知第三步的最後一行serialize最後呼叫了JdkSerializationRedisSerializer的如下方法
@Override
public byte[] serialize(@Nullable Object object) {
    if (object == null) {
        return SerializationUtils.EMPTY_ARRAY;
    }
    try {
        return serializer.convert(object);
    } catch (Exception ex) {
        throw new SerializationException("Cannot serialize", ex);
    }
}
這裡的serializer物件是SerializingConverter,可以知道實際呼叫的是SerializingConverter.convert(new Integer(1))
5、到此我們知道了,value最後存在redis的最底層原理就是第四步的return返回的byte[]。那麼就可以這樣認為,如果呼叫了
RedisTemplate.opsForValue().set("check:incr:obj", 1);就好比是在redis儲存了下面方法的返回
public static void main(String[] args) throws UnsupportedEncodingException {
    ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
    try  {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
        objectOutputStream.writeObject(1);
        objectOutputStream.flush();
        byte[] obj = byteStream.toByteArray();
        for(int i=0; i<obj.length; i++) {
            System.out.println(obj[i]);
        }
    }catch (Throwable ex) {
        ex.printStackTrace();
    }
}
結論:
列印出來好多資料,但是我們儲存的只是一個整數1而已,並且根據序列化過程中的類ObjectOutputStream
的描述(見下)可知序列化後會包含N多資訊
/**
     * Write the specified object to the ObjectOutputStream.  The class of the
     * object, the signature of the class, and the values of the non-transient
     * and non-static fields of the class and all of its supertypes are
     * written.******
*/

 

相關文章