黑马程序员Redis教程学习笔记(第4部分):缓存同步与批处理优化
课程概述
本篇笔记涵盖黑马程序员Redis教程中关于缓存同步与批处理优化的重要内容,主要包括Canal缓存同步、Redis键值设计最佳实践以及批处理优化策略。
1. Canal缓存同步实现
Canal简介
Canal是阿里巴巴开源的MySQL数据库增量日志解析中间件,它伪装成MySQL的Slave节点,监听MySQL的binlog变化,实现数据的实时同步。
Canal工作原理
- MySQL主从同步:MySQL主节点记录binlog文件,从节点读取并重放操作
- Canal伪装:Canal伪装成MySQL从节点,监听binlog变化
- 数据同步:当数据库发生变更时,Canal立即感知并通知缓存服务更新
Canal安装配置
1. 开启MySQL主从同步
修改MySQL配置文件:
# binlog文件位置和名称
log-bin=mysql-bin
# 监控的数据库
binlog-do-db=hmdp2. 创建Canal用户并授权
-- 创建Canal用户
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
-- 授权
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- 刷新权限
FLUSH PRIVILEGES;3. 创建Docker网络
# 创建网络
docker network create hmdp
# 将MySQL容器加入网络
docker network connect hmdp mysql4. 运行Canal容器
docker run -p 11111:11111 --name canal \
-e canal.destinations=hmdp \
-e canal.instance.master.address=mysql:3306 \
-e canal.instance.dbUsername=canal \
-e canal.instance.dbPassword=canal \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false \
--network=hmdp \
-d canal/canal-server:v1.1.5Canal客户端集成
1. 引入依赖
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>2. 配置文件
canal:
destination: hmdp # 与Canal服务器配置保持一致
server: 192.168.150.101:11111 # Canal服务器地址3. 实体类注解
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_item")
public class Item {
@TableId(type = IdType.ASSIGN_ID)
@Column
private Long id; // 主键映射
private String name;
private String title;
private Long price;
private String image;
private String category;
private String brand;
@TableField(exist = false)
@Transient
private Integer stock;
@TableField(exist = false)
@Transient
private Integer sold;
}4. Canal处理器实现
@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {
@Autowired
private RedisHandler redisHandler;
@Override
public void insert(Item item) {
// 新增数据到缓存
redisHandler.saveItem(item);
}
@Override
public void update(Item before, Item after) {
// 更新缓存数据
redisHandler.saveItem(after);
}
@Override
public void delete(Item item) {
// 删除缓存数据
redisHandler.deleteItemById(item.getId());
}
}5. Redis缓存操作封装
@Component
public class RedisHandler {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ObjectMapper objectMapper;
public void saveItem(Item item) {
try {
String json = objectMapper.writeValueAsString(item);
redisTemplate.opsForValue().set("item:" + item.getId(), json);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void deleteItemById(Long id) {
redisTemplate.delete("item:" + id);
}
}2. Redis键值设计最佳实践
Key设计规范
- 格式:使用冒号分隔的多段格式,如
业务名:数据名:ID - 示例:
login:user:1001表示登录业务的用户ID为1001的用户信息 - 长度:不超过44个字节,避免使用特殊字符
- 优势:
- 可读性强,便于理解
- 避免key冲突
- 便于管理,支持层级结构
Value设计考虑
1. BigKey问题识别
- 字符串类型:大小不超过10KB
- 集合类型:元素数量不超过1000个
- 检测命令:
MEMORY USAGE key:查看key占用内存STRLEN key:查看字符串长度LLEN key:查看列表长度HLEN key:查看哈希长度SCARD key:查看集合大小
2. BigKey问题危害
- 网络阻塞:大key占用带宽,影响其他请求
- 数据倾斜:导致Redis节点内存使用不均
- Redis阻塞:单线程处理大key操作时阻塞其他请求
- CPU占用:序列化/反序列化消耗大量CPU
3. BigKey检测方案
方案一:使用redis-cli –bigkeys
redis-cli -a 密码 --bigkeys方案二:使用SCAN命令遍历
@Test
void testScan(){
// 定义BigKey的阈值
int strMaxLen = 10 * 1024; // 10KB
int hashMaxLen = 500; // 500个field
long cursor = 0; // 游标
do {
// 扫描一批数据
ScanResult<String> result = stringRedisTemplate.execute(RedisServerCommands::scan,
new ScanOptions.ScanOptionsBuilder()
.match("*")
.count(100)
.build());
cursor = Long.parseLong(result.getCursor());
// 判断是否为BigKey
List<String> keys = result.getResult();
for (String key : keys) {
// 获取数据类型
DataType type = stringRedisTemplate.type(key);
switch (type) {
case STRING:
String value = stringRedisTemplate.opsForValue().get(key);
if (value != null && value.length() > strMaxLen) {
System.out.printf("发现BigKey: %s, 类型: %s, 长度: %d%n", key, type, value.length());
}
break;
case HASH:
Long hashLen = stringRedisTemplate.opsForHash().size(key);
if (hashLen > hashMaxLen) {
System.out.printf("发现BigKey: %s, 类型: %s, 长度: %d%n", key, type, hashLen);
}
break;
// 其他类型类似处理
}
}
} while (cursor > 0);
}4. BigKey解决方案
- 删除策略:
- Redis 4.0+:使用
UNLINK命令异步删除 - Redis 4.0以下:使用
SCAN遍历逐个删除集合元素,最后删除key
- Redis 4.0+:使用
- 拆分策略:
- 将大集合拆分为多个小集合
- 使用合适的分片策略,如按ID范围或哈希分片
3. 批处理优化
单个命令执行的问题
在Redis中执行单个命令的总时长包括: - 一次网络往返传输时间 - Redis执行命令时间
其中网络传输时间通常是主要的耗时部分,特别是当网络延迟较高时。
批处理优化策略
1. MSET命令优化
MSET命令可以一次性设置多个键值对:
// 批量设置键值对
String[] keysAndValues = new String[2000]; // 1000个键值对
for(int i = 0; i < 100000; i++){
int idx = (i % 1000) << 1; // 计算数组索引
keysAndValues[idx] = "test:" + i;
keysAndValues[idx + 1] = "value:" + i;
if(idx == 0){ // 每1000个键值对批量插入一次
jedis.mset(keysAndValues);
}
}2. Pipeline优化
Pipeline允许将多个命令一次性发送到Redis服务器,减少网络往返次数:
// 使用Pipeline进行批量操作
Pipeline pipeline = jedis.pipelined();
for(int i = 0; i < 100000; i++){
pipeline.set("test:" + i, "value:" + i);
if(i % 1000 == 0){
pipeline.sync(); // 每1000个命令同步一次
}
}
pipeline.sync(); // 最后同步剩余命令MSET vs Pipeline对比
| 特性 | MSET | Pipeline |
|---|---|---|
| 原子性 | 具备原子性,命令一次性执行 | 不具备原子性,命令按顺序执行 |
| 适用场景 | 相同数据类型,批量设置 | 任意命令组合,灵活度高 |
| 性能 | 非常高,内置原子操作 | 高,但略低于MSET |
| 限制 | 仅支持部分数据类型 | 支持所有Redis命令 |
批处理注意事项
- 不要一次传输太多命令:避免网络拥塞
- 合理控制批处理大小:通常建议1000-5000个命令为一批
- 选择合适的批处理方式:根据数据类型和业务需求选择MSET或Pipeline
4. 多级缓存架构总结
完整架构流程
浏览器/客户端 -> Nginx静态资源服务器 -> OpenResty负载均衡 -> Tomcat集群
-> OpenResty本地缓存
-> Redis缓存
-> Tomcat进程缓存
-> MySQL数据库各层缓存策略
- 浏览器客户端缓存:静态资源缓存,通过304状态码优化
- Nginx本地缓存:基于URL哈希的负载均衡,确保相同商品请求路由到同一服务器
- OpenResty本地缓存:使用shared_dict实现进程间共享缓存
- Redis缓存:分布式缓存,支持集群部署
- Tomcat进程缓存:JVM进程内缓存,使用Caffeine实现
缓存同步策略
- OpenResty本地缓存:使用过期时间策略,适合更新频率较低的数据
- Redis缓存:通过Canal监听MySQL binlog实现数据同步
- Tomcat进程缓存:同样通过Canal实现同步
5. Redis最佳实践总结
Key设计原则
- 使用冒号分隔的多段格式:
业务名:数据名:ID - 长度不超过44字节
- 避免特殊字符
- 便于管理和避免冲突
缓存同步策略
- 有效期控制:适用于更新频率低、实时性要求不高的数据
- 同步双写:适用于对一致性要求极高的数据
- 异步通知:通过Canal实现,适用于多服务同步场景
批处理优化
- 根据网络延迟和数据量选择合适的批处理大小
- 使用MSET进行相同数据类型的批量操作
- 使用Pipeline进行复杂命令的批量执行
这些最佳实践能够帮助我们在实际项目中更好地使用Redis,提高系统性能和稳定性。