Lettuce is a fully non-blocking Redis client built with netty providing Reactive, Asynchronous and Synchronous Data Access .
RedisURI [host=’localhost’, port=6379] 连接的封装,用于解析database password 等基础参数,还有集群,Sentinel 配置解析。
DefaultEndpoint 通信 channel 的封装。除了提供 channel.write,还具备重连和重试的能力。
RedisClient 负责构造和初始化Netty,thread-safe,reuse this instance as much as possible。
ClientResources 管理(初始化和持有) EventExecutorGroup, Timer, EventBus 等资源。不需要重复创建。
/**
* lettuce 与 redis-server 通信提供编解码(RESP 协议),监控的实现
* @see io.lettuce.core.ConnectionBuilder#buildHandlers()
*/
protected List<ChannelHandler> buildHandlers() {
List<ChannelHandler> handlers = new ArrayList<>();
handlers.add(new ChannelGroupListener(channelGroup));
// Encodes RedisMessage into bytes following
handlers.add(new CommandEncoder());
// writing redis commands and reading responses from the server. core!!!
handlers.add(new CommandHandler());
// 通过 eventBus 把连接状态相关的事件广播出去。
handlers.add(new ConnectionEventTrigger(connectionEvents, connection, clientResources.eventBus()));
// monitoring the channel and reconnecting when the connection is lost.
if (clientOptions.isAutoReconnect()) {
handlers.add(new ConnectionWatchdog());
}
return handlers;
}
Redis clients use a protocol called RESP (REdis Serialization Protocol) to communicate with the Redis server.
RESP can serialize different data types like integers, strings, and arrays.
RESP uses prefixed-length to transfer bulk data.
In RESP, different parts of the protocol are always terminated with “\r\n” (CRLF).
官方文档 详细介绍协议的定义和每种数据类型的格式。
协议规则非常简单,并且容易解析和阅读。
Simple Strings 用于系统内的回复等,可以理解为常量。
Bulk Strings 用于命令参数,使用binary-safe string,确保输入数据不会因为字符集,操作系统等问题,造成数据读写问题。
Simple String 表示“OK”为 "+OK\r\n"
Bulk Strings 表示“hello”为 "$5\r\nhello\r\n" = 数据类型字符 + 长度 + 间隔符 + 数据 + 间隔符。
Jedis is a Java client for Redis designed for performance and ease of use.
根据协议的规范,可以用任何语言编解码通信数据。如下就是Jedis 对协议规则的实现。
/**
* set("name", "foo") Example
*
* 支持的数据类型: Simple Strings, Errors, Integers, Bulk Strings and Arrays.
* 数据类型的表示:Simple Strings(+), Errors, Integers(:), Bulk Strings($) and Arrays(*)
* 不同的块之间一定要用CRLF间隔(\r\n)
* @see redis.clients.jedis.Protocol#sendCommand
*/
private static void sendCommand(final RedisOutputStream os, final byte[] command, final byte[]... args) {
try {
os.write(ASTERISK_BYTE);// 数组类型,"*" 代表Arrays
os.writeIntCrLf(args.length + 1);// 数组长度:参数(n) + 命令(1), writeInt,writeCrLf
os.write(DOLLAR_BYTE);// String,"$" 代表Bulk Strings
os.writeIntCrLf(command.length);// 长度
os.write(command);// 操作命令 SET/[83, 69, 84]
os.writeCrLf();// 间隔符号
// 操作参数, args[0]="name".bytes(), args[1]="foo".bytes()
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
A client connects to a Redis server by creating a TCP connection to the port 6379.
Jedis 是直接使用socket 来完成通信。
public void connect() {
// 每次使用前,检查 socket 是否可用
if (!isConnected()) {
try {
// socket 编程Example
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
socket.setSoTimeout(soTimeout);
if (ssl) {
// 启用ssl 模式
if (null == sslSocketFactory) {
sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
}
socket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, true);
}
// RedisOutputStream 提供buffer 和方便的协议写入方法
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
}
}
}
通过 SpringMVC Date 参数实例化的配置实例,梳理和分析类型转换在 Spring 框架中的实现过程。
Spring 内置的类型转换实现组件,可以通过以下接口追溯
org.springframework.core.convert.converter.Converter
java.beans.PropertyEditor
org.springframework.beans.TypeConverterDelegate Spring 内部类型转换工具类
org.springframework.core.convert.support.GenericConversionService 类型转换器默认实现,可以直接注入使用。
优化基础规则数据库的查询过程中,本来想着给部分慢查询启用二级缓存,简单方便。
结果却造成线上事故:namespace 下的
<cache>配置,让其他没有声明useCache="true"的 select 也默认走了二级缓存,又因为返回值实体类未实现Serializable抛出序列化异常。本文从事故出发,拆解
<cache>标签的装饰器实现、useCache默认值逻辑与二级缓存运行流程,最后给出修复方案与避坑建议。
基础规则数据库中有个慢查询 select1,本想通过 Mybatis 二级缓存优化,在 Mapper XML 中声明了 <cache>:
<!-- namespace 级别二级缓存:容量 1000,60 分钟刷新,LRU 淘汰,读写缓存 -->
<cache size="1000" flushInterval="3600000" readOnly="false" eviction="LRU" />
<!-- 启用缓存,优化查询 ① -->
<select id="select1" resultMap="ResultMap1" useCache="true" flushCache="false">
SELECT * FROM rule_a WHERE id = #{id}
</select>
<!-- 没想启用缓存,且 ResultMap2 对应的实体类没有实现 Serializable ② -->
<select id="select2" resultMap="ResultMap2">
SELECT * FROM rule_b WHERE biz_code = #{bizCode}
</select>
① select1 是目标查询,显式声明 useCache="true" 开启二级缓存。
② select2 未声明任何缓存属性,按直觉应该不走缓存。而且其返回值 ResultMap2 对应的实体类 RuleB 没有实现 java.io.Serializable。
上线后线上直接报错:
java.io.NotSerializableException: com.foo.model.RuleB
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:51)
🙉 select2 明明没有声明 useCache="true",为何也走了二级缓存?还触发了 SerializedCache 的序列化逻辑?
Mybatis 解析 <select> 标签时,会读取 useCache 属性,注意它的默认值:
/**
* 解析 select/insert/update/delete 语句节点
* @see org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
*/
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// flushCache:非 select(增删改)默认 true,select 默认 false
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// useCache:select 默认 true,非 select 默认 false 🎈
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
🎈 关键发现:useCache 的默认值就是 isSelect!
useCache 默认 true → 只要 namespace 下配置了 <cache>,所有 select 天然启用二级缓存useCache 默认 false所以 select2 虽然没有显式声明,但默认 useCache="true",依然走了二级缓存。这就是事故的直接原因 ❌
<cache> 的 readOnly 默认 false(即读写缓存),Mybatis 会为缓存实例包一层 SerializedCache:
/**
* 读写缓存会额外包装 SerializedCache
* @see org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
* @see org.apache.ibatis.cache.CacheBuilder#setStandardDecorators
*/
if (readWrite) {
cache = new SerializedCache(cache); // readWrite=true → 序列化拷贝 🎈
}
SerializedCache 在写入缓存时对对象做序列化,读缓存时反序列化拷贝:
/**
* 读写缓存写入:对象序列化后存储
* @see org.apache.ibatis.cache.decorators.SerializedCache#putObject
*/
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, object); // 可直接存储
} else {
byte[] bytes = serialize((Serializable) object); // 序列化拷贝 🎈 未实现 Serializable 在此报错
delegate.putObject(key, bytes);
}
}
select2 返回的实体 RuleB 未实现 Serializable,在序列化阶段直接抛异常 ❌
<cache> 标签的实现原理<cache> 标签由 XMLMapperBuilder#parse 解析,最终通过 MapperBuilderAssistant#useNewCache + CacheBuilder#build 构建出装饰器链:
graph LR
P["PerpetualCache<br/>基础存储 HashMap"]
L["LruCache<br/>LRU 淘汰"]
S1["ScheduledCache<br/>定时刷新"]
S2["SerializedCache<br/>序列化拷贝"]
L2["LoggingCache<br/>命中率日志"]
S3["SynchronizedCache<br/>线程安全"]
B["BlockingCache<br/>阻塞并发(可选)"]
P --> L --> S1 --> S2 --> L2 --> S3 --> B
各层装饰器职责与触发条件:
| 装饰器 | 职责 | 触发条件 |
|---|---|---|
PerpetualCache |
基础存储,底层 HashMap |
始终存在 |
LruCache |
LRU 淘汰 | eviction 默认 LRU |
ScheduledCache |
定时清空 | 配置了 flushInterval |
SerializedCache |
序列化/反序列化拷贝 | readOnly="false"(默认) |
LoggingCache |
命中率统计日志 | 始终存在 |
SynchronizedCache |
方法加锁,线程安全 | 始终存在 |
BlockingCache |
未命中时阻塞并发请求 | blocking="true" |
查询时由 CachingExecutor#query 判断 namespace 是否有缓存:
/**
* 二级缓存查询入口
* @see org.apache.ibatis.executor.CachingExecutor#query
*/
Cache cache = ms.getCache(); // ① namespace 是否有 <cache>
if (cache != null) {
flushCacheIfRequired(ms); // ② 按 flushCache 决定是否清空
if (ms.isUseCache() && resultHandler == null) { // ③ isUseCache() 即 useCache 属性 🎈
// 命中则返回缓存,否则查库并写入 tcm
}
}
sequenceDiagram
participant 业务 as 业务代码
participant CE as CachingExecutor
participant Cache as namespace缓存
participant DB as 数据库
业务->>CE: 1. query(select2)
CE->>Cache: 2. getCache() 非空?
Note over CE: ms.isUseCache() == true(select 默认值)🎈
CE->>Cache: 3. 尝试 getObject(key)
alt 命中缓存
Cache-->>业务: 4. 反序列化拷贝后返回
else 未命中
CE->>DB: 5. 查询数据库
DB-->>CE: 6. 结果集
CE->>Cache: 7. putObject(key, result) 序列化 🎈
Note over Cache: 实体未实现 Serializable → 抛异常 ❌
end
🎈 读写缓存(readWrite=true)每次返回的都是序列化拷贝出的新对象,多线程各自持有一份,避免共享同一引用互相修改产生脏数据;而只读缓存(readOnly=true)直接返回对象引用,性能更高,但共享同一实例存在被修改的风险。
java.io.Serializable,注意关联对象也要实现<cache readOnly="true"> 声明只读缓存,跳过序列化拷贝useCache="false" 关闭(默认值陷阱,必须显式声明才可关闭)⭐ 事故根因在于 select 默认 useCache="true":一旦配置 <cache>,所有 select 默认都走二级缓存,无论是否声明。
SerializableflushCache 覆盖不完整导致数据不一致<cache> 配置下,select 语句 useCache 默认 true,不声明也会走二级缓存,这是事故的直接原因。readOnly="false",会包一层 SerializedCache,缓存实体必须实现 Serializable,否则报 NotSerializableException。<cache> 采用装饰器设计模式构建,各自职责单一、可组合。记录 Mybatis 动态 SQL、字符串替换相关的实现,方便后续源码阅读和借鉴。
| [MyBatis 3 | Dynamic SQL](https://mybatis.org/mybatis-3/dynamic-sql.html) |
One of the most powerful features of MyBatis has always been its Dynamic SQL capabilities. OGNL based expressions.
<select id="findActiveBlogLike" resultType="Blog">
SELECT * FROM BLOG
<!-- 只有在包含的标签返回任何内容时才会插入"WHERE"子句。如果内容以"AND"或"OR"开头,自动会将其去掉。 -->
<where>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
// org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#initNodeHandlerMap
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
// dynamic SQL in annotated mapper class
@Select("select * from user where name = #{name} ORDER BY ${columnName}")
User findByName(@Param("name") String name);
使用 #{} 语法可以生成 PreparedStatement 属性,参数注解替换为 PreparedStatement参数(占位符?)。防止 SQL 拼接注入等安全问题。
使用 ${} 语法直接将未修改的字符串替换到SQL语句中,MyBatis不会修改或转义字符串。当 SQL 语句中的元数据(即表名或列名)是动态的时,字符串替换非常有用。