Trong quá trình tích hợp Redis với Spring Boot, dữ liệu (bao gồm cả key và value) đều được chuyển đổi thành mảng byte thông qua các RedisSerializer. Mặc định, RedisTemplate sử dụng JdkSerializationRedisSerializer, trong khi StringRedisTemplate dùng StringRedisSerializer. Tuy nhiên, các cấu hình mặc định này thường bộc lộ nhiều hạn chế về hiệu suất và tính linh hoạt trong các dự án thực tế.
Đánh giá các Serializer mặc định
- JdkSerializationRedisSerializer: Sử dụng cơ chế chuẩn của Java. Ưu điểm là không cần chỉ định class khi deserialize, nhưng bắt buộc object phải implement interface
Serializable. Nhược điểm lớn nhất là kích thước payload sau khi serialize rất lớn (gấp khoảng 5 lần JSON), gây lãng phí bộ nhớ Redis. - Jackson2JsonRedisSerializer: Chuyển đổi object sang chuỗi JSON. Tốc độ nhanh, payload nhỏ gọn. Tuy nhiên, nó yêu cầu phải truyền tham số kiểu dữ liệu (
.class) vào constructor và bắt buộc entity phải có đầy đủ getter/setter, nếu không sẽ phát sinh lỗi khi deserialize.
Khắc phục hạn chế của StringRedisSerializer cho Key
Khi sử dụng StringRedisSerializer cho key, nếu truyền vào một object không phải String (ví dụ khi dùng annotation @Cacheable), hệ thống sẽ ném ra ngoại lệ ép kiểu. Để giải quyết, ta có thể tự định nghĩa một serializer cho key chấp nhận kiểu Object và tự động xử lý việc chuyển đổi sang chuỗi.
public class FlexibleStringSerializer implements RedisSerializer<Object> {
private final Charset encoding = StandardCharsets.UTF_8;
@Override
public byte[] serialize(Object source) throws SerializationException {
if (source == null) {
return null;
}
String rawValue = JSON.toJSONString(source);
// Loại bỏ dấu ngoặc kép nếu giá trị là một chuỗi đơn thuần
if (rawValue.startsWith("\"") && rawValue.endsWith("\"")) {
rawValue = rawValue.substring(1, rawValue.length() - 1);
}
return rawValue.getBytes(encoding);
}
@Override
public Object deserialize(byte[] target) throws SerializationException {
return target == null ? null : new String(target, encoding);
}
}
Sử dụng FastJson thay thế Jackson cho Value
Để tránh lỗi thiếu setter khi dùng Jackson, FastJson là một lựa chọn thay thế xuất sắc nhờ khả năng serialize/deserialize linh hoạt hơn và không phụ thuộc vào các phương thức getter/setter truyền thống. Dưới đây là cách triển khai một serializer tùy chỉnh dựa trên FastJson.
public class FastJsonPayloadSerializer<T> implements RedisSerializer<T> {
private final Class<T> targetType;
private final Charset charset = StandardCharsets.UTF_8;
public FastJsonPayloadSerializer(Class<T> targetType) {
this.targetType = targetType;
}
@Override
public byte[] serialize(T payload) throws SerializationException {
if (payload == null) {
return new byte[0];
}
String jsonText = JSON.toJSONString(payload, SerializerFeature.WriteClassName);
return jsonText.getBytes(charset);
}
@Override
public T deserialize(byte[] rawData) throws SerializationException {
if (rawData == null || rawData.length == 0) {
return null;
}
String jsonText = new String(rawData, charset);
return JSON.parseObject(jsonText, targetType);
}
}
Tối ưu hiệu suất với Kryo Serializer
Nếu yêu cầu về tốc độ xử lý và tối ưu kích thước bộ nhớ là ưu tiên hàng đầu, Kryo là giải pháp serialize nhị phân cực nhanh. Kryo không tạo ra chuỗi JSON mà nén object thành mảng byte nhỏ gọn, giúp giảm thiểu đáng kể băng thông mạng và dung lượng lưu trữ.
public class KryoBinarySerializer<T> implements RedisSerializer<T> {
private static final Logger log = LoggerFactory.getLogger(KryoBinarySerializer.class);
private final Class<T> type;
private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false);
return kryo;
});
public KryoBinarySerializer(Class<T> type) {
this.type = type;
}
@Override
public byte[] serialize(T obj) throws SerializationException {
if (obj == null) return new byte[0];
Kryo kryo = kryoThreadLocal.get();
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
Output output = new Output(out)) {
kryo.writeObject(output, obj);
output.flush();
return out.toByteArray();
} catch (Exception ex) {
log.error("Kryo serialization failed", ex);
return new byte[0];
}
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) return null;
Kryo kryo = kryoThreadLocal.get();
try (Input input = new Input(bytes)) {
return kryo.readObject(input, type);
} catch (Exception ex) {
log.error("Kryo deserialization failed", ex);
return null;
}
}
}
Cấu hình RedisTemplate
Cuối cùng, chúng ta cần đăng ký các serializer tùy chỉnh này vào RedisTemplate thông qua một class cấu hình để Spring Boot tự động áp dụng trong toàn bộ ứng dụng.
@Configuration
public class RedisCacheConfig {
@Bean
public RedisTemplate<String, Object> customRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Cấu hình FastJson cho value
FastJsonPayloadSerializer<Object> valueSerializer = new FastJsonPayloadSerializer<>(Object.class);
// Chỉ định whitelist để bảo mật AutoType
ParserConfig.getGlobalInstance().addAccept("com.myproject.domain.");
// Cấu hình serializer tùy chỉnh cho key
FlexibleStringSerializer keySerializer = new FlexibleStringSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
}