JavaRedis面试题
1. Redis是什么以及Redis为什么快?
Redis(Remote Dictionary Server)是一个开源的内存键值数据库,支持多种数据结构(如字符串、哈希、列表、集合等),并提供持久化、复制、事务等功能。
Redis为什么快?
(1) 内存存储 + 单线程模型
单线程优势:原子性操作无需加锁,避免线程切换和竟态条件。
因素 | 说明 |
---|---|
内存操作 | 数据完全存储在内存中,读写速度远超磁盘(纳秒级访问)。 |
单线程架构 | 避免多线程竞争锁和上下文切换开销(Redis 6.0 后引入多线程仅用于网络I/O)。 |
(2)高效的数据结构
Redis 针对不同场景优化了数据结构实现:
数据结构 | 底层实现 | 优化点 |
---|---|---|
字符串(String) | SDS(简单动态字符串) | 预分配内存、O(1) 时间复杂度获取长度。 |
哈希(Hash) | 哈希表 + 压缩列表(小数据) | 自动切换编码节省内存。 |
有序集合(ZSet) | 跳表(SkipList) + 哈希表 | 范围查询高效(O(log N))。 |
(3) I/O 多路复用
时间驱动模型:基于 epoll(Linux)或kqueue(Mac)实现高并发连接处理。
非阻塞I/O:单线程监听多个套接字,就绪事件通过队列顺序处理。
(4) 协议与编码优化
RESP 协议:简单文本协议,解析效率高。
紧凑数据存储:压缩列表、整数集合等节省内存。
(5) 避免慢操作
异步持久化:RDB(快照)和 AOF(日志)尽量不阻塞主线程。
管道(Pipeline):批量命令减少网络往返时间。
2.Redis可以用来做什么
1.缓存加速(Cache)
场景:减轻数据库压力,加速热点数据访问。
优势:内存读写速度是磁盘的 10万倍以上,支持过期时间自动淘汰(EXPIRE)。
2.会话存储(Session Storage)
场景:分布式系统中的用户会话共享。
优势:解决多服务器间 Session 不一致问题。 比数据库存储会话性能更高。
3.消息队列(Message Queue)
场景:异步任务处理,解耦系统组件。
4.实时排行榜(Leaderboard)
场景:游戏积分榜、热搜排名。
优势:有序集合(ZSet)自动排序,时间复杂度O(log N)。
5.计数器(Counter)
场景:文章阅读量、点赞数统计。
6.分布式锁(Distributed Lock)
场景:防止多节点重复处理任务。
7.发布订阅(Pub/Sub)
场景:实时通知(如聊天室、订单状态更新)
缺点:消息不持久化,客户端断开后消失。
8.地理位置(Geospatial)
场景:附近的人,店铺搜索。
3.Redis 持久化有几种方式?
Redis 提供两种持久化机制,确保数据在重启或故障后不丢失,两者可单独或组合使用。
1.RDB(Redis Database)
原理:定时生成内存数据的二进制快照(Snapshot),保存为 .rdb 文件。
触发方式:
自动触发:按配置规则定时保存(默认策略)。
手动触发:
SAVE:阻塞主线程,同步保存(生产环境慎用)。
BGSAVE:后台异步保存(通过 fork 子进程操作)。
优点:文件紧凑,恢复速度快。 适合备份和灾难恢复。
缺点:可能丢失最后一次快照的数据(取决于配置间隔)。大量数据时fork 子进程可能阻塞主线程(内存页复制开销)。
2.AOF(Append Only File)
原理:记录所有写操作命令(文本格式),通过重放命令恢复数据。
工作流程:
1.写入命令追加到AOF缓冲区。
2.根据策略同步到磁盘(appendfsync 配置):
always:每次写命令都同步(最安全,性能最低)。
everysec:每秒同步一次(推荐,平衡性能与安全)。
no:由操作系统决定(最快,可能丢失数据)。
文件重写(AOF Rewrite):压缩AOF文件(移除冗余命令),通过BGREWRITEAOF触发或自动配置。
优点:数据安全性高(最多丢失1秒数据)。可读性强(文本格式便于人工修复)。
缺点:文件体积较大,恢复速度慢。高频写入时对磁盘压力大。
对比项 | RDB | AOF |
---|---|---|
持久化方式 | 定时快照 | 记录所有写命令 |
数据安全性 | 可能丢失最后一次快照后的数据 | 根据配置最多丢失1秒数据 |
恢复速度 | 快(直接加载二进制文件) | 慢(需重放命令) |
文件大小 | 小(二进制压缩) | 大(需定期重写优化) |
适用场景 | 允许少量数据丢失,追求快速恢复 | 对数据安全性要求高 |
4.Redis 支持的数据类型有哪些?
1.String(字符串)
特点:最基本类型,可存储文本、数字(≤512MB)或二进制数据。
应用场景:缓存、计数器、分布式锁。
bash
SET user:1001 "Alice" # 存储字符串
GET user:1001 # 获取值
INCR article:1001:views # 数字自增
SETNX lock:order 1 EX 30 # 分布式锁(原子操作)
2.Hash(哈希表)
特点:键值对集合,适合存储对象。
应用场景:用户属性,商品详情。
bash
HSET user:1001 name "Alice" age 25 # 设置字段
HGET user:1001 name # 获取字段
HGETALL user:1001 # 获取所有字段
3.List(列表)
特点:有序元素集合,支持双向操作。
应用场景:消息队列、最新消息排行。
bash
LPUSH news:latest "news1" # 左侧插入
RPOP news:latest # 右侧弹出
LRANGE news:latest 0 4 # 获取前5条
4.Set(集合)
特点:无序且元素唯一,支持交并差运算。
应用场景:标签系统,共同好友。
bash
SADD tags:1001 "tech" "redis" # 添加元素
SINTER tags:1001 tags:1002 # 求交集
SMEMBERS tags:1001 # 获取所有元素
5.ZSET(有序集合)
特点:元素按分数排序,且唯一。
应用场景:排行榜,优先级队列。
java
ZADD leaderboard 100 "PlayerA" 85 "PlayerB" # 添加带分数元素
ZREVRANGE leaderboard 0 2 WITHSCORES # 获取Top 3
ZRANK leaderboard "PlayerA" # 查询排名
6.Bitmap(位图)
特点:通过位操作存储布尔值,极致节省空间。
应用场景:用户签到、活跃统计。
bash
SETBIT sign:2023:10 1001 1 # 用户1001在10月签到
BITCOUNT sign:2023:10 # 统计当月签到人数
7.HyperLogLog(基数统计)
特点:估算集合不重复元素数量(误差 误差≤1%)。
应用场景:UV(独立访客)统计。
bash
PFADD uv:2023:10:01 "user1" "user2" # 记录用户
PFCOUNT uv:2023:10:01 # 估算UV
8.Stream(流)
特点:消息队列,支持消费者和多播。
应用场景:事件溯源、日志收集。
bash
XADD orders * user 1001 item "book" # 添加消息
XREAD COUNT 10 STREAMS orders 0 # 读取消息
XGROUP CREATE orders group1 0 # 创建消费者组
需求 | 推荐类型 | 原因 |
---|---|---|
简单键值存储 | String | 直接读写,功能简单。 |
对象存储 | Hash | 字段独立操作,节省内存。 |
先进先出/后进先出队列 | List | 支持双向操作。 |
去重集合 | Set | 自动去重,支持集合运算。 |
带权重的唯一集合 | ZSet | 按分数排序,范围查询高效。 |
布尔值大量存储 | Bitmap | 极致节省空间(1亿用户仅需12MB)。 |
大数据量去重统计 | HyperLogLog | 固定12KB内存,误差可控。 |
消息队列 | Stream | 支持消费者组和历史消息回溯。 |
5.Redis 淘汰策略有哪些?
当 Redis 内存达到 maxmemory 限制时 (通过 maxmemory-policy 配置),会根据指定策略淘汰数据以释放空间。
1.不淘汰策略
策略 | 行为 | 适用场景 |
---|---|---|
noeviction (默认) |
内存满时拒绝所有写入操作(返回OOM错误),读请求正常处理。 | 数据绝对不允许丢失,且需人工干预的场景。 |
2.基于TTL的淘汰策略
策略 | 行为 | 特点 |
---|---|---|
volatile-ttl |
从设置了过期时间 的键中,淘汰剩余时间最短的键。 | 优先淘汰即将过期的数据,保留热点数据。 |
3.基于LRU(最近最少使用)的淘汰策略
策略 | 行为 | 特点 |
---|---|---|
allkeys-lru |
从所有键 中淘汰最近最少使用的键。 | 适合热点数据分布明显的场景(如缓存)。 |
volatile-lru |
从设置了过期时间 的键中淘汰最近最少使用的键。 | 需手动设置过期时间,灵活性较低。 |
4.基于LFU(最不经常使用)的淘汰策略
策略 | 行为 | 特点 |
---|---|---|
allkeys-lfu |
从所有键 中淘汰访问频率最低的键。 | 适合长期运行且访问模式变化的场景。 |
volatile-lfu |
从设置了过期时间 的键中淘汰访问频率最低的键。 | 需手动设置过期时间。 |
5.随机淘汰策略
策略 | 行为 | 特点 |
---|---|---|
allkeys-random |
从所有键中随机淘汰。 | 简单但可能误删热点数据。 |
volatile-random |
从设置了过期时间的键中随机淘汰。 | 需手动设置过期时间。 |
6.Redis如何解决单机故障
Redis单击故障可能导致数据丢失或服务中断。
1.数据持久化(单机基础保障)
原理:将内存数据保存到磁盘中,故障后恢复。
方案:RDB快照,定时全量备份。 AOF日志:记录所有写操作。
优点:简单易用,适合数据量小的场景。
缺点:恢复事件较长,可能丢失部分数据。
2.主从复制(Replication)
原理:主节点(Master)异步复制数据到从节点(Slave)。
优点:读写分离,主节点写,从节点读。 故障时可手动切换从节点为主节点。
缺点:异步复制可能导致数据丢失(主节点宕机时)。需人工干预故障转移。
配置:
bash
# 从节点配置文件
replicaof 192.168.1.100 6379 # 指定主节点IP和端口
replica-read-only yes # 从节点只读
3.Redis Sentinel(哨兵)
原理:哨兵集群监控主从节点,自动故障转移。
架构:至少3个哨兵节点(避免脑裂)。哨兵通过投票机制选举新主节点。
优点:自动故障检测与转移。客户端可通过哨兵获取新主节点地址。
缺点:切换期间短暂不可用,从节点提升为主节点后需重配其他从节点。
配置:
bash
# sentinel.conf
sentinel monitor mymaster 192.168.1.100 6379 2 # 监控主节点,2表示需2个哨兵同意
sentinel down-after-milliseconds mymaster 5000 # 5秒无响应判定为主观下线
4.Redis Cluster(集群)
原理:数据分片(16364个槽)存储在多节点,自动故障转移。
架构:至少3主3从(6节点)。每个主节点对应一个从节点(备份)。
优点:数据分片+高可用,支持水平扩展。自动故障转移(主节点宕机时从节点接替)。
缺点:客户端需支持集群协议,跨槽操作需使用{} 强制路由。
配置:
bash
# 启动集群节点
redis-server --cluster-enabled yes --cluster-config-file nodes.conf
# 创建集群
redis-cli --cluster create 192.168.1.100:6379 192.168.1.101:6379 ... --cluster-replicas 1
7.Redis 怎么实现分布式锁?
1.SETNX+EXPIRE
原理:通过SETNX(Key不存在时设置)和 EXPIRE(设置过期时间)组合实现。
风险:非原子操作,若SETNX后宕机,锁永不释放。
实现:
bash
SET lock:order 1 NX EX 30 # 原子化实现(Redis 2.6.12+)
2.唯一值验证(防误删)
问题:锁可能被其他客户端误删。
解决:为每个客户端设置唯一值(如UUID),删除时验证。
3.锁续期(WatchDog)
问题:业务未完成但锁已过期。
解决:后台线程定期续期(如每10秒检测一次)。
Java实现(Redission):
java
RLock lock = redisson.getLock("lock:order");
lock.lock(); // 默认30秒,看门狗自动续期
try {
// 业务逻辑
} finally {
lock.unlock();
}
4.Redlock 算法(多节点容错)
场景:Redis集群环境,避免单点故障。
流程:
1.向N个独立Redis节点顺序请求加锁(SET NX EX)。
2.当多数节点(≥ N/2 +1)加锁成功,且总耗时小于锁有效期,则成功。
3.失败时向所有节点发送解锁请求。
Java实现:
java
Config config = new Config();
config.useClusterServers().addNodeAddress("redis://node1:6379", "redis://node2:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("lock:order");
5.分段锁 (提升并发)
场景:高并发下减少锁粒度(如库存扣减)
示例:
bash
# 将库存拆分为10个分段
SET inventory:item1:seg1 100
SET inventory:item1:seg2 100
# 随机选择一个分段加锁
SETNX lock:item1:seg1 1
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
SETNX + EXPIRE | 简单 | 非原子操作风险 | 低要求临时方案 |
SET NX EX + Lua | 原子操作,防误删 | 需处理续期 | 中小规模应用 |
Redisson | 自动续期,可重入 | 依赖Java客户端 | Java生态 |
Redlock | 高可用,防单点故障 | 性能较低,实现复杂 | 金融级关键业务 |
分段锁 | 高并发 | 业务逻辑复杂 | 秒杀、库存扣减 |
8.Redis分布式锁有什么缺陷?
1.锁过期与业务执行冲突
问题:锁自动过期后业务仍在执行,导致多客户端同时进入临界区。
解决方案:锁续期(WatchDog)。合理设置超时:根据压测结果设置超时时间(如业务平均耗时 x 3)。
2.主从切换导致锁失效
问题:主节点宕机时,未同步到从节点的锁丢失。
解决方案:Redlock算法。使用Zookeeper/Etcd。
3.客户端阻塞导致锁失效
问题:客户端因GC或网络阻塞未及时续期,锁过期后其他客户端获取锁。
解决方案:心跳检测:客户端定期向Redis发送心跳。 熔断机制:检测到长时间阻塞主动释放锁。
4.锁误删(非持有者释放锁)
问题:客户端A释放了客户端B的锁。
解决方案:Lua脚本原子化验证。
5.锁不可重入
问题:同一线程多次获取同一把锁导致死锁。
解决方案:Redisson可重入锁。
6.锁竞争性能瓶颈
问题:高并发下大量线程重试抢锁,导致CPU和网络压力。
优化方案:分段锁。 排队队列。
9.什么是缓存穿透,如何解决?
缓存穿透是指查询一个数据库中不存在的数据。导致请求直接绕过缓存(未命中),每次都会访问数据库。这种情况通常由恶意攻击或业务逻辑缺陷引发。
示例场景:
用户请求一个不存在的商品ID(如 -1
或超大ID)。缓存无记录(未命中),直接查询数据库。数据库同样无结果,无法回填缓存,导致后续相同请求重复穿透。
解决方案:
(1) 缓存空对象(Null Caching)
原理:及时数据库查询为空,仍将空结果(如null)存入缓存,并设置较短的过期时间。
优点:简单有效,适合大多数场景。
缺点:内存浪费(需存储大量空值键)。短期数据不一致(如新增数据后仍需等待空缓存过期)。
(2) 布隆过滤器(Bloom Filter)
原理:使用概率型数据结构预先存储所有合法Key,快速判断请求的Key是否存在。
实现步骤:1.初始化布隆过滤器:将所有有效ID(如商品ID)存入过滤器。2.请求拦截:查询缓存前,先用布隆过滤器检查key是否存在。
优点:内存占用极低(1亿元素仅需12MB,误判率1%)。拦截效率高(时间复杂度O(1))
缺点:无法删除数据(需结合计数布隆过滤器或定期重建)。误判率存在。
(3) 接口层校验
原理:在业务逻辑层或API网关对请求参数进行合法性校验。
使用场景:ID必须为正整数。参数需符合特定格式(如手机号、邮箱)。
(4) 热点数据预加载
原理:系统启动或定期任务预先加载所有有效Key到缓存
使用场景:数据量可控(如身份场景、品类信息)。数据变更不频繁。
(5) 限流与熔断
原理:对异常请求进行限流,防止数据库被击穿。
工具:Redis计数器:限制同一Key的查询频率。Sentinel/Hystrix:触发熔断时返回降级效果。
10.什么是缓存击穿,如何解决?
缓存击穿是指某个热点Key在缓存中过期时,大量并发请求直接穿透到数据库,导致数据库瞬时压力激增。
典型场景:
明星离婚新闻的缓存Key过期,瞬间千万请求涌入数据库。
解决方案:
(1) 互斥锁(Mutex Lock)
原理:只允许一个线程重建缓存,其他线程等待或返回旧数据。
优点:强一致性,避免重复查询。
缺点:锁竞争可能增加延迟。
(2) 逻辑过期(Logical Expiration)
原理:缓存永不过期,但存储包含时间戳的封装对象,由业务逻辑判断是否需异步更新。
实现步骤:1.从缓存获取数据,检查是否逻辑过期。2.若过期,提交异步任务更新缓存,当前线程返回旧数据。
优点:无锁设计,高并发性能好。
缺点:短期数据不一致。
(3) 热点数据永不过期
原理:对极热点Key设置较长的过期时间或后台定时更新。
适用场景:高频访问且更新不频繁的数据(如首页推荐)。
风险:需确保最终数据一致性。
(4) 多级缓存(分层缓存)
架构:
L1(本地缓存):Caffeine/Guava Cache,过期时间短(如1分钟)。
L2(分布式缓存):Redis、过期时间长(如1小时)。
请求流程:
1.先查本地缓存,未命中则查Redis。
2.Redis未命中时,由单个节点查数据库并回填,其他节点短暂等待。
优点:减少Redis压力,避免集中击穿。
(5) 限流降级
原理:缓存失效时,限制数据库查询的并发量。
工具:Semaphore:控制并发线程数。Redis+Lua:分布式限流。
11.什么是缓存雪崩,如何解决?
缓存雪崩是指大量缓存Key在同一时间集中失效或Redis服务宕机,导致所有请求直接涌向数据库,引发数据库瞬时压力过载甚至崩溃的现象。
典型场景:
缓存中大量Key设置相同的过期时间(如午夜零点同时失效)。
Redis集群整体宕机,流量直接压垮数据库。
解决方案:
(1) 差异化过期时间
原理:为Key的过期时间添加随机值,避免同时失效。
适用场景:批量预加载的缓存数据(如商品列表)。
(2) 永不过期 + 后台更新
原理:缓存不设过期时间,通过异步任务定期更新。
优点:彻底避免集中失效。
缺点:需要维护额外的更新逻辑。
(3) 缓存预热
原理:系统启动时预先加载热点数据到缓存。
关键点:预热时需分散过期时间。
(4) Redis高可用部署
架构方案:Redis Cluster:数据分片+多副本,单节点故障不影响整体。 Sentinel模式:主从切换自动故障转移。
预防雪崩:确保集群节点分散在不同物理机/机柜。
12.Redis的哨兵模式工作原理是什么?
Redis 哨兵模式(Sentinel)用于实现高可用性,监控主从节点并在主节点故障时自动进行故障转移。
1.工作流程:
监控:哨兵节点定期检测主从节点是否存活(PING命令)。
故障判定:多个哨兵确认主节点下线(主观下线+客观下线)。
选举Leader:哨兵集群通过Raft算法选出一个Leader执行故障转移。
故障转移:
1.从节点中选出新主节点(基于优先级、复制偏移量)。
2.通知其他从节点复制新主节点。
3.通知客户端连接新主节点。
2.客户端通知:通过发布订阅机制向客户端推送主节点变更信息。
关键点:
至少需要3个哨兵节点避免脑裂。
不保证数据强一致性(原主节点恢复后成为从节点,可能丢失部分数据)。
Java数据库面试题
1.什么是事务?
事务(Transaction)是数据库操作的一个逻辑单元,它包含一组操作,这些操作要么全部成功,要么全部回滚。事务确保数据的一致性,即使在系统故障或并发访问时也能保证正确状态。
事务特性(ACID):
1.原子性:事务是不可分割的最小单元,要么全部执行,要么全部不执行。
2.一致性:事务执行的结果必须是数据库从一个一致性状态到另一个一致性状态。
3.隔离性:并发事务之间互补干扰。
4.持久性:事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失。
2.mysql事务的隔离级别有哪些?
MySQL支持四种标准的事务隔离级别,用户控制并发事务之间的可见性和影响,从低到高依次为:
1.读未提交(Read Uncommitted)
事务可以读取其他事务未提交的修改。
可能问题:脏读、不可重复读、幻读。
性能最高,但数据一致性最差。
2.读已提交(Read Commited)
事务只能读取其他事务已提交的修改。
问题:不可重复读、幻读。
3.可重复读(Repeatable Read)
MySQL默认级别。
事务内多次读取同一数据的结果一致。
问题:可能发生幻读。
4.串行化(Serializable)
最高隔离级别,所有事务串行执行,完全避免脏读、不可重复读、幻读问题。
问题:性能最低。
3.不可重复读和幻读有什么区别?
不可重复读和幻读都是并发事务导致的一致性问题,但它们的表现和影响不同:
不可重复读:
1.同一事务两次读取同一行数据,结果不同(数据被其他事务修改或删除)。
2.针对已存在的行更新或删除。
幻读:
1.同一事务内两次查询同一范围数据,结果集的行数不同(其他事务插入或删除了符合条件的数据)
2.针对符合查询条件的新增行或删除行。
区别 | 不可重复读 | 幻读 |
---|---|---|
现象 | 同一事务内两次读取同一行数据,结果不同(数据被其他事务修改或删除)。 | 同一事务内两次查询同一范围数据,结果集的行数不同(其他事务插入或删除了符合条件的数据)。 |
操作类型 | 针对已存在的行的更新(Update)或删除(Delete)。 | 针对符合查询条件的新增行(Insert)或删除行(Delete)。 |
示例 | 事务A读取某用户年龄为20,事务B将年龄改为30并提交,事务A再次读取年龄变为30。 | 事务A查询年龄>20的用户有5条,事务B插入一条年龄=25的记录并提交,事务A再次查询得到6条。 |
解决隔离级别 | Read Committed 及以上可避免。 |
Serializable 或MySQL的Repeatable Read (通过间隙锁部分解决)。 |
4.spring实现事务的步骤
1.声明式事务(基于注解@Transactional)
步骤:
1.启用事务管理
在配置类添加@EnableTransactionManagement(Spring Boot默认已启用)。
2.配置事务管理器
Spring Boot自动配置DataSourceTransactionManager,无需手动配置。
3.在方法或类上添加@Transactional
java
@Service
public class UserService {
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// 数据库操作(如扣钱、加钱)
}
}
关键参数(可选):
isolation:隔离级别(如Isolation.READ_COMMITTED)。
propagation:传播行为(如Propagation.REQUIRED)。
rollbackFor:指定回滚的异常类型(默认回滚RuntimeException)。
5.MySQL存储金额用什么类型?
在MySQL中存储金额时,推荐使用 DECIMAL 类型。
类型 | 特点 | 适用场景 |
---|---|---|
DECIMAL | 精确存储小数,避免浮点数精度丢失(如 DECIMAL(10,2) 表示总位数10,小数位2) |
金额、财务数据(必须精确) |
FLOAT/DOUBLE | 近似存储,可能有精度问题(如 0.1 + 0.2 ≠ 0.3 ) |
科学计算、非精确数据 |
INT/BIGINT | 将金额转为整数存储(如"分"为单位,100 表示 1.00 元) |
需要高性能计算的场景 |
DECIMAL示例:
sql
CREATE TABLE orders (
id INT PRIMARY KEY,
amount DECIMAL(10, 2) -- 最大存储 99999999.99
);
-- 插入数据
INSERT INTO orders VALUES (1, 1234.56);
INT存储分(推荐优化方案)
sql
CREATE TABLE orders (
id INT PRIMARY KEY,
amount BIGINT -- 以分为单位,100 表示 1.00 元
);
-- 插入数据
INSERT INTO orders VALUES (1, 123456); -- 实际表示 1234.56 元
选择建议:
严格精确 -> DECIMAL:适合财政系统、交易系统,确保 0.01 元不会丢失。
高性能需求 -> INT/BIGINT:以分为单位存储(如 1元=100),避免小数运算,提升计算速度(需在代码中处理单位转换)。
6.char和varchar的区别?
1.存储机制
char(10):
存入 "abc" -> 存储为 "abc "(补7个空格)
始终占用10个字节(单字节字符集)或 10x字符集字节数(如 UTF=3字节/字符)。
varchar(10):
存入"abc" -> 存储为 "3abc"(长度前缀 + 实际数据)
占用实际长度 + 1字节(≤255)或2字节(>255)。
2.性能对比
场景 | CHAR | VARCHAR |
---|---|---|
读取速度 | ⚡️ 更快(固定偏移量) | ⏳ 稍慢(需解析长度) |
存储效率 | ❌ 浪费空间(短数据) | ✅ 更节省空间 |
索引效率 | 适合作为主键(定长优化) | 适合非主键字段 |
3.使用示例
sql
CREATE TABLE users (
id CHAR(36), -- 固定长度(如UUID)
username VARCHAR(50), -- 可变长度(如用户名)
gender CHAR(1) -- 固定长度(如'M'/'F')
);
sql
-- CHAR(5) 存入 "hi" → 实际存储 "hi "(补3空格)
INSERT INTO test VALUES ('hi', 'hi');
-- VARCHAR(5) 存入 "hi" → 实际存储 "2hi"(长度+数据)
对比项 | CHAR | VARCHAR |
---|---|---|
存储方式 | 固定长度,不足部分用空格填充 | 可变长度,仅占用实际数据长度 + 长度标识(1-2字节) |
存储空间 | 固定分配(如 CHAR(10) 始终占10字符) |
动态分配(如 VARCHAR(10) 存 "abc" 占3+1=4字节) |
检索速度 | 更快(固定长度,无需计算偏移量) | 稍慢(需读取长度标识) |
适用场景 | 存储长度固定的数据(如MD5、UUID) | 存储长度变化的数据(如用户名、地址) |
空格处理 | 检索时会自动去除尾部填充的空格 | 保留存储时的空格 |
最大长度 | 255字符(MySQL) | 65535字节(受行大小限制) |
7.where 和 having 的区别?
1.基本区别
where示例
sql
-- 先筛选price>100的产品,再计算各类别的平均价格
SELECT category, AVG(price) as avg_price
FROM products
WHERE price > 100 -- 对原始数据过滤
GROUP BY category;
having示例
sql
-- 先计算各类别平均价格,再筛选avg_price>200的类别
SELECT category, AVG(price) as avg_price
FROM products
GROUP BY category
HAVING avg_price > 200; -- 对聚合结果过滤
2.关键使用场景
WHERE 适用场景
过滤原始纪律(如日期范围、状态条件)
需要利用索引提高查询效率时
在连接查询中先过滤减少关联数据量
sql
-- 查询2023年订单量超过5次的客户
SELECT customer_id, COUNT(*) as order_count
FROM orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31' -- 先按日期过滤
GROUP BY customer_id
HAVING COUNT(*) > 5; -- 再筛选聚合结果
Having 适用场景
基于聚合结果进行筛选(如总和、平均值、计数)
需要对分组后的数据再过滤
sql
-- 查询总销售额超过1万元的销售员
SELECT salesperson_id, SUM(amount) as total_sales
FROM sales
GROUP BY salesperson_id
HAVING SUM(amount) > 10000; -- 筛选聚合结果
特性 | WHERE 子句 | HAVING 子句 |
---|---|---|
执行时机 | 在 分组前(GROUP BY) 过滤数据 | 在 分组后(GROUP BY) 过滤结果 |
适用对象 | 过滤 原始行数据 | 过滤 分组后的聚合结果 |
聚合函数 | 不能直接使用聚合函数作为条件 | 可以使用聚合函数作为条件 |
性能 | 通常更高效(先过滤后分组) | 相对低效(先分组后过滤) |
索引使用 | 可以使用索引优化 | 不能使用索引(因为操作的是分组后结果) |
8.select *和select 全部字段有什么区别?
1.性能差异
数据库层面:
select *:
需要读取所有列数据(包括不需要的列)
无法利用覆盖索引(Covering Index)
SELECT 列名:
只读取指定列数据
当查询的列都包含在索引中时,可以直接从索引获取数据(不需要回表)
sql
-- 假设有索引 idx_name_age (name, age)
-- 低效写法(需要回表查所有字段)
SELECT * FROM users WHERE name = '张三';
-- 高效写法(使用覆盖索引)
SELECT name, age FROM users WHERE name = '张三';
2.开发问题
(1) 表结构变更风险
sql
-- 原始表结构:users(id, name, age)
SELECT * FROM users; -- 返回 id, name, age
-- 新增字段后:users(id, name, age, address, phone)
-- 同样的查询现在会返回更多字段,可能导致:
-- 1. 应用程序解析错误
-- 2. 数据传输量意外增加
(2) JOIN查询字段冲突
sql
-- 两个表都有id字段时
SELECT * FROM users JOIN orders ON users.id = orders.user_id;
-- 结果集中会出现两个id字段,应用程序可能无法正确解析
-- 正确写法
SELECT
users.id AS user_id,
users.name,
orders.id AS order_id,
orders.amount
FROM users
JOIN orders ON users.id = orders.user_id;
对比维度 | SELECT * |
SELECT 全部字段 |
---|---|---|
可读性 | ❌ 差(无法直观看到返回的字段) | ✅ 好(明确列出所有字段) |
性能影响 | ⚠️ 可能较差(特别是表结构变更时) | ✅ 更优(只传输需要的字段) |
表结构变更适应性 | ❌ 差(新增字段会自动返回) | ✅ 好(明确字段列表不受影响) |
索引利用 | ❌ 可能无法有效利用索引 | ✅ 可以针对性优化 |
网络传输 | ❌ 传输不必要的数据 | ✅ 只传输需要的字段 |
JOIN查询 | ❌ 容易产生字段冲突 | ✅ 可以明确指定来源表 |
存储过程/视图 | ❌ 维护困难 | ✅ 更容易维护 |
9.mysql是怎么完成分页的?
MySQL主要通过 LIMIT 字句实现分页查询。
1.基础分页语法
offset:跳过多少条记录(从0开始)
page_size:每页显示多少条记录
sql
SELECT * FROM table_name
LIMIT offset, page_size;
示例:查询第二页,每页10条
sql
-- 偏移量 = (页码 - 1) * 每页大小
SELECT * FROM users
ORDER BY create_time DESC
LIMIT 10, 10; -- 第2页(跳过前10条,取10条)-- 偏移量 = (页码 - 1) * 每页大小
SELECT * FROM users
ORDER BY create_time DESC
LIMIT 10, 10; -- 第2页(跳过前10条,取10条)
10.索引的类型有哪些?
索引是数据库优化查询性能的核心机制,不同数据库系统支持的索引类型各有特点。
1.按数据结构分类
(1) B-Tree索引(平衡树索引)
适用场景:等值查询、返回查询、排序操作
支持数据库:MySQL(InnoDB)、PostgreSQL、Oracle、SQL Server
特点:数据有序存储(支持ORDER BY优化),适合高基数(唯一值多)列,时间复杂度O(log n)
(2) Hash索引
适用场景:精确匹配查询(不支持范围查询)
支持数据库:MySQL(Memory引擎)、Redis
特点:查询时间复杂度 O(1),不支持部分键查询和排序,易发生哈希冲突。
(3) 全文索引(Full-Text)
适用场景:文本内容的关键词搜索。
支持数据库:MySQL、PostgreSQL、Elasticsearch(专业全文检索引擎)
特点:支持自然语言搜索和布尔搜索,使用倒排索引实现。
(4)R-Tree索引(空间索引)
适用场景:地理空间数据(GIS)
支持数据库:MySQL、PostgreSQL(PostGIS)、Oracle
(5) Bitmap索引
适用场景:低基数(唯一值少)的列(如性别、状态)
支持数据库:Oracle、SQL Server
特点:存储效率极高,适合数据仓库场景,锁粒度大,不适合高并发写入。
2.按物理存储分类
(1) 聚簇索引(Clustered Index)
特点:
索引与数据行物理存储顺序一致(InnoDB的主键索引)
一个表只能有一个聚簇索引
优势:
范围查询效率高,减少随机I/O。
(2) 非聚簇索引(Secondary Index)
特点:
索引与数据分离存储(如MyISAM的所有索引)
需要回表查询
3.按逻辑功能分类
(1) 主键索引(PRIMARY KEY)
特点:
唯一且非空。
InnoDB中自动成为聚簇索引。
sql
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键索引
name VARCHAR(50)
);
(2) 唯一索引(UNIQUE)
特点:
保证列值唯一(允许NULL值)
常用于业务唯一约束
sql
CREATE UNIQUE INDEX idx_email ON users(email);
(3) 普通索引 (INDEX)
特点:
最基本的索引类型
无唯一性约束
sql
CREATE INDEX idx_name ON users(name);
(4) 复合索引(联合索引)
特点:
多列组合的索引
遵循最左前缀法则
sql
CREATE INDEX idx_name_age ON users(last_name, age);
-- 有效查询
SELECT * FROM users WHERE last_name = 'Smith';
SELECT * FROM users WHERE last_name = 'Smith' AND age = 30;
-- 无效查询(未使用最左列)
SELECT * FROM users WHERE age = 30;
(5) 覆盖索引(Covering Index)
特点:
索引包含查询所需的所有字段
避免回表查询
sql
CREATE INDEX idx_cover ON users(id, name);
-- 覆盖索引查询
SELECT id, name FROM users WHERE id = 100;
4.特殊类型索引
(1) 函数索引
适用场景:对列进行函数计算后的查询
支持数据库:Oracle、PostgreSQL、MySQL 8.0+
sql
-- MySQL 8.0+
CREATE INDEX idx_year ON users((YEAR(birth_date)));
(2) 部分索引(Partial Index)
适用场景:只为部分数据创建索引
支持数据库:PostgreSQL、SQL Server
11.MySQL索引底层原理
MySQL索引的核心目的是加速数据检索,其底层实现因存储引擎而异。
1.核心数据结构
(1) B+树与B树的对比
特性 | B+树 | B树 |
---|---|---|
数据存储位置 | 仅叶子节点存储数据 | 所有节点都可能存储数据 |
叶子节点链接 | 通过指针形成有序链表 | 无 |
查询稳定性 | 任何查询都需要到叶子层 | 可能在非叶子节点命中 |
范围查询效率 | 极高(链表遍历) | 需要中序遍历 |
(2) InnoDB的B+树特点
非叶子节点:存储键值+子节点指针(6字节)
叶子节点:存储完整数据行(聚簇索引),存储主键值(二级索引)
2.聚簇索引(Clustered Index)
(1) 存储规则
InnoDB必须有且只有一个聚簇索引
(2)物理存储方式
叶子节点直接存储完整数据行数据(称为"索引组织表")
数据按主键顺序物理存储(插入时可能导致页分裂)
(3)优势
范围查询效率高(如 where id >100)
避免回表(覆盖索引场景)
3.二级索引(Secondary Index)
(1) 存储结构
叶子节点存储:索引列值 + 主键值
查询需要回表:通过主键回到聚簇索引查询完整数据
(2) 回表示例
查询流程:
1.在 idx_name B+树种找到 name = 'Alice' 的叶子节点
2.获取对应的主键值(如 id =123)
3.用 id = 123 到聚簇索引查询完整行数据
sql
-- 假设有索引 idx_name(name)
SELECT * FROM users WHERE name = 'Alice';
(3) 覆盖索引优化
sql
-- 只查询索引列可避免回表
SELECT id, name FROM users WHERE name = 'Alice';
4.InnoDB与MyISAM索引对比
特性 | InnoDB | MyISAM |
---|---|---|
索引类型 | 聚簇索引+二级索引 | 非聚簇索引(索引与数据分离) |
数据存储 | 主键索引包含完整数据 | 索引仅存储数据文件指针 |
并发控制 | 行级锁 | 表级锁 |
事务支持 | 支持 | 不支持 |
12.聚簇索引与非聚簇索引有什么区别
聚簇索引(Clustered Index)
定义与特点
数据即索引:表数据按照聚集索引的键值物理排序存储
唯一性:每个表只能有一个聚集索引(InnoDB中通常是主键)
存储结构:B+树的叶子节点直接包含完整的数据行
查询优势:通过聚集索引访问数据非常高效,特别是范围查询
InnoDB的实现:
如果定义了主键,则主键就是聚集索引
如果没有主键,则选择第一个非空的唯一索引作为聚集索引
如果都没有,InnoDB会隐式创建一个6字节的ROWID作为聚集索引
sql
-- InnoDB表,id是聚集索引
CREATE TABLE users (
id INT PRIMARY KEY, -- 聚集索引
name VARCHAR(50),
age INT
);
非聚簇索引(Non-clustered Index)
定义与特点
独立结构:索引结构与数据存储分离
多索引支持:一个表可以有多个非聚集索引
存储结构:B+树的叶子节点存储的是指向数据行的指针(主键值或行指针)
查询过程:需要"回表"操作,先查索引再查数据
实现方式:
InnoDB:二级索引叶子节点存储主键值,需要回表查询
MyISAM:存储数据行的物理地址(文件偏移量)
sql
-- name列上的非聚集索引
CREATE INDEX idx_name ON users(name);
特性 | 聚簇索引(Clustered Index) | 非聚簇索引(Non-Clustered Index) |
---|---|---|
数据存储方式 | 索引与数据行物理存储在一起 | 索引与数据分离存储 |
叶子节点内容 | 存储完整数据行 | 存储主键值或数据行指针 |
数量限制 | 每表仅能有一个 | 每表可创建多个(MySQL默认16个) |
查询效率 | 主键查询极快 | 需要回表查询 |
插入性能影响 | 页分裂可能影响性能 | 影响相对较小 |
典型实现引擎 | InnoDB | MyISAM、MongoDB等 |
13.MySQL的回表查询原理是什么?
1.什么是回表查询?
回表查询是指在使用非聚簇索引(二级索引)进行查询时,MySQL需要二次访问聚簇索引才能获取完整数据的过程。这种查询方式比直接使用聚簇索引多一次I/O操作,是常见的性能瓶颈来源
2.回表查询的核心原理
(1) InnoDB索引结构对比
索引类型 | 叶子节点存储内容 | 查询特点 |
---|---|---|
聚簇索引 | 完整数据行 | 直接获取数据,无需回表 |
二级索引 | 索引列值 + 主键值 | 需通过主键回表查完整数据 |
(2) 回表流程示意图
步骤1: 查找索引 步骤2: 回表查询 步骤3: 获取数据 二级索引B+树 获取主键值 聚簇索引B+树 完整数据行
3.回表查询示例
sql
-- 表结构
CREATE TABLE `users` (
`id` INT PRIMARY KEY,
`name` VARCHAR(50),
`age` INT,
INDEX `idx_name` (`name`)
);
-- 触发回表的查询
SELECT * FROM users WHERE name = 'Alice';
执行流程:
1.通过 idx_name 索引找到 name = 'Alice' 的叶子节点
2.获取对应的主键值(如 id = 101)
3. 用 id=101 到聚簇索引种查找完整行数据
(2) 分页查询种的回表陷阱
问题:先通过 idx_name 排序获取 1010 条主键,再回表查询1010次完整数据,最后丢弃前1000条。
sql
-- 深度分页导致大量回表
SELECT * FROM users ORDER BY name LIMIT 100000, 10;
4.如何避免回表查询
(1) 使用覆盖索引
关键点:索引包含所有查询字段
sql
-- 创建覆盖索引
CREATE INDEX idx_cover ON users(name, age);
-- 优化后的查询(无需回表)
SELECT name, age FROM users WHERE name = 'Alice';
(2) 强制使用聚簇索引
sql
-- 对于主键查询不会回表
SELECT * FROM users WHERE id = 101;
-- 使用USE INDEX提示
SELECT * FROM users USE INDEX(PRIMARY) WHERE name = 'Alice';
14.索引失效的情况有哪些?
1.违反最左前缀原则
解决方案:
确保查询条件包含最左列
调整索引顺序或创建新索引
sql
-- 联合索引: INDEX(col1, col2, col3)
SELECT * FROM table WHERE col2 = 'value'; -- 失效
SELECT * FROM table WHERE col3 = 'value'; -- 失效
2.对索引列使用函数或运算
失效场景:
sql
-- 索引: INDEX(create_time)
SELECT * FROM users WHERE YEAR(create_time) = 2023; -- 失效
SELECT * FROM products WHERE price + 10 > 100; -- 失效
解决方案:
使用范围查询替代函数:
sql
SELECT * FROM users
WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31';
MySQL8.0+可使用函数索引
sql
CREATE INDEX idx_year ON users((YEAR(create_time)));
3.隐式类型转换
失效场景:
sql
-- 索引: INDEX(phone) phone是varchar类型
SELECT * FROM users WHERE phone = 13800138000; -- 失效(数字转字符串)
解决方案:
保持类型一致
sql
SELECT * FROM users WHERE phone = '13800138000';
4.使用前导通配符LIKE
失效场景:
sql
-- 索引: INDEX(name)
SELECT * FROM users WHERE name LIKE '%Alice%'; -- 失效
SELECT * FROM users WHERE name LIKE '%Alice'; -- 失效
解决方案:
尽量使用后置通配符:
sql
SELECT * FROM users WHERE name LIKE 'Alice%'; -- 使用索引
5.OR连接非索引列
失效场景:
sql
-- 索引: INDEX(age)
SELECT * FROM users WHERE age = 20 OR name = 'Alice'; -- name无索引则全表扫描
解决方案:
为所有OR条件列建索引
改用UNION ALL:
sql
SELECT * FROM users WHERE age = 20
UNION ALL
SELECT * FROM users WHERE name = 'Alice';
6.使用NOT、!=、<>操作符
失效场景:
sql
-- 索引: INDEX(status)
SELECT * FROM orders WHERE status != 'completed'; -- 可能失效
解决方案:
改写为范围查询:
sql
SELECT * FROM orders WHERE status < 'completed' OR status > 'completed';
7.索引列参与计算
失效场景:
sql
-- 索引: INDEX(age)
SELECT * FROM users WHERE age + 1 > 20; -- 失效
解决方案:
将计算移到等式另一边:
sql
SELECT * FROM users WHERE age > 19;
8.使用IS NULL/IS NOT NULL
失效场景:
sql
-- 索引: INDEX(name)
SELECT * FROM users WHERE name IS NULL; -- 可能失效
解决方案:
sql
-- 添加默认值替代NULL
ALTER TABLE users MODIFY name VARCHAR(50) DEFAULT '' NOT NULL;
9.数据量过少时优化器放弃索引
失效场景:
sql
-- 表只有100行数据时
SELECT * FROM small_table WHERE indexed_col = 'value'; -- 可能全表扫描
解决方案:
使用FORCE INDEX提示:
sql
SELECT * FROM small_table FORCE INDEX(idx_col) WHERE indexed_col = 'value';
10.索引列顺序与排序方向不一致
失效场景:
sql
-- 索引: INDEX(col1 ASC, col2 DESC)
SELECT * FROM table ORDER BY col1 DESC, col2 ASC; -- 排序失效
解决方案:
创建匹配的索引:
sql
CREATE INDEX idx_mixed ON table(col1 DESC, col2 ASC);
15.什么是最左匹配原则?
最左匹配原则:在使用联合索引(复合索引)时,MySQL会从左到右依次匹配索引列,只有查询条件包含联合索引的最左列,索引才会被使用。
关键特性
前缀匹配:类似于字典的字符排序查找
中断停止:若中间某列未在查询中出现,则其后的索引列不生效。
顺序敏感:索引列的顺序决定索引的有效性。
联合索引的结构原理
索引存储方式 INDEX(a,b,c),索引在B+树种的排列顺序为:
1.先按 a 列排序
2.a 相同则按 b 排序。
3.a 和 b 都相同再按 c 排序。
a=1 b=2 b=3 c=4 c=5
(2) 索引查找过程
有效查找: a -> b -> c (从左到右)
无效查找: 直接查找 b 或 c (无法利用索引有序性)
16.SQL优化方案
1.索引优化
合理创建索引:在 WHERE、JOIN、ORDER BY 和 GROUP BY 涉及的列上创建索引
联合索引设计:遵循最左前缀原则,将高频查询和高选择性列放在前面
避免冗余索引:删除不再使用或重复的索引
覆盖索引:让索引包含查询所需的所有字段,避免回表操作
2.查询语句优化
避免SELECT * :只查询需要的列
合理使用JOIN:小表驱动大表,确保JOIN字段有索引
优化子查询:能用 JOIN 代替的子查询尽量用 JOIN
避免全表扫描:确保 WHERE 条件能使用索引
3.数据库设计优化
合理的数据类型:使用最小满足需求的数据类型
分区表:大数据表按时间、范围等维度分区
垂直拆分:将不常用的大字段拆分到单独表
4.执行计划分析
使用EXPLAIN:分析查询执行计划,找出性能瓶颈
识别全表扫描:避免出现 ALL 类型的访问方式
5.高级优化技巧
使用临时表:复杂查询可拆为多个简单查询
读写分离:查询走从库,减轻主库压力。
缓存策略:合理使用应用层缓存和数据库缓存。
JavaMyBatis面试题
1.ResultType 和 ResultMap 的区别是什么?
ReusltType:
自动将查询结果的列名与Java对象的属性名匹配(忽略大小写)。
要求数据库字段名和java属性名完全一致(如user_name -> userName 需开启驼峰映射)
xml
<select id="getUser" resultType="com.example.User">
SELECT id, user_name, age FROM user
</select>
ResultMap:
显示定义字段与属性的映射关系,支持负载场景(如一对多、类型转化)
xml
<resultMap id="userMap" type="com.example.User">
<id property="id" column="id"/>
<result property="name" column="user_name"/>
<result property="age" column="age"/>
</resultMap>
<select id="getUser" resultMap="userMap">
SELECT id, user_name, age FROM user
</select>
对比项 | ResultType | ResultMap |
---|---|---|
作用 | 直接指定返回结果的Java类型(自动映射)。 | 自定义字段与属性的映射关系(复杂映射场景)。 |
灵活性 | 低(要求数据库字段与Java属性名严格一致)。 | 高(支持字段别名、嵌套对象、集合映射等)。 |
2.MyBatis中 #{} 和 ${} 的区别是什么?
#{}:MyBatis 会将其转换为 PreparedStatement 的参数占位符 ?,通过JDBC 预编译执行。
sql
-- 最终SQL(安全)
SELECT * FROM user WHERE id = ?
${}:直接拼接到字符串到SQL中,不会预编译。。
sql
-- 最终SQL(危险!)
SELECT * FROM user WHERE id = 1001
SQL注入风险:
${}的风险示例:
若参数值为 "1 OR 1=1",拼接后SQL变为:
sql
SELECT * FROM user WHERE id = 1 OR 1=1 -- 查询所有数据!
#{}的防护:
即使参数值为 "1 OR 1=1",实际执行的是:
sql
SELECT * FROM user WHERE id = '1 OR 1=1' -- 作为普通字符串处理
对比项 | #{} (预编译占位符) |
${} (字符串拼接) |
---|---|---|
处理方式 | 生成预编译SQL(? 占位符),防止SQL注入。 |
直接替换为参数值,存在SQL注入风险。 |
安全性 | ✅ 安全 | ❌ 不安全 |
适用场景 | 动态参数值(如 WHERE id = #{id} )。 |
动态表名、列名(如 ORDER BY ${column} )。 |
数据类型转换 | 自动根据Java类型转换(如日期格式化)。 | 原样替换,不处理类型转换。 |
性能 | 预编译SQL可复用,性能更优。 | 每次生成新SQL,性能略低。 |
3.MyBatis动态标签都有哪些?
1.基础条件控制标签
标签 | 作用 | 示例 |
---|---|---|
<if> |
条件判断,满足条件时包含 SQL 片段。 | xml <if test="name != null"> AND name = #{name} </if> |
<choose> |
多条件选择(类似 Java 的 switch-case )。 |
xml <choose> <when test="id != null">AND id = #{id}</when> <otherwise>AND status = 1</otherwise> </choose> |
<when> |
在 <choose> 中使用,表示一个条件分支。 |
见上例 |
<otherwise> |
在 <choose> 中使用,表示默认分支。 |
见上例 |
2.循环遍历标签
标签 | 作用 | 示例 |
---|---|---|
<foreach> |
遍历集合(如 IN 查询、批量插入)。 | xml <foreach item="item" collection="list" open="(" separator="," close=")"> #{item} </foreach> |
参数说明 | ||
collection |
集合参数名(如 list 、array 或 @Param("ids") 指定的名称)。 |
|
item |
当前元素的变量名。 | |
open/close |
循环开始/结束时添加的字符串(如括号)。 | |
separator |
元素间的分隔符(如逗号)。 |
3.字符串处理标签
标签 | 作用 | 示例 |
---|---|---|
<trim> |
自定义字符串修剪(去除多余前缀/后缀)。 | `xml <trim prefix="WHERE" prefixOverrides="AND |
<where> |
自动处理 WHERE 关键字,去除开头多余的 AND /OR 。 |
xml <where> <if test="name != null">AND name = #{name}</if> </where> |
<set> |
自动处理 SET 关键字,去除结尾多余的逗号。 |
xml <set> <if test="name != null">name = #{name},</if> </set> |
4.特殊用途标签
标签 | 作用 | 示例 |
---|---|---|
<bind> |
创建变量并绑定到上下文,用于模糊查询或复杂表达式。 | xml <bind name="pattern" value="'%' + name + '%'" /> SELECT * FROM user WHERE name LIKE #{pattern} |
<sql> |
定义可重用的 SQL 片段。 | xml <sql id="userColumns">id, name, age</sql> SELECT <include refid="userColumns"/> FROM user |
<include> |
引用 <sql> 定义的片段。 |
见上例 |
4.当实体类的属性名和表中的字段名不一致时如何处理?
当Java实体类的属性名与数据库表的字段名不一致时,MyBatis无法自动映射数据。
1.使用Result 注解(注解方式)
适用场景:简单字段映射,无需修改SQL。
java
public interface UserMapper {
@Results({
@Result(property = "userId", column = "user_id"), // 属性名 → 字段名
@Result(property = "userName", column = "username")
})
@Select("SELECT * FROM t_user")
List<User> getAllUsers();
}
2.定义 resultMap>(XML方式)
适用场景:复杂映射或需要复用配置。
java
<!-- 在Mapper XML中定义 -->
<resultMap id="userMap" type="User">
<id property="userId" column="user_id"/>
<result property="userName" column="username"/>
<result property="createTime" column="create_time"/>
</resultMap>
<!-- 引用resultMap -->
<select id="getUserById" resultMap="userMap">
SELECT * FROM t_user WHERE user_id = #{id}
</select>
3.开启驼峰命名自动映射
适用场景:字段名木盒下划线驼峰规则(如 user_name -> userName)。
配置:在 mybatis-config.xml 或 SpringBoot 配置文件中启用:
yml
# application.yml(Spring Boot)
mybatis:
configuration:
map-underscore-to-camel-case: true
xml
<!-- mybatis-config.xml -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
4.SQL 别名(直接修改查询语句)
适用场景:临时调整或简单查询。
xml
<select id="getUser" resultType="User">
SELECT
user_id AS userId,
username AS userName
FROM t_user
</select>
5.在mapper中如何传递多个参数?
1.使用 @Param 注解(推荐)
使用场景:方法参数较少(≤3个),代码可读性高。
java
public interface UserMapper {
// 通过@Param指定参数名,XML中直接使用
List<User> selectByCondition(
@Param("name") String name,
@Param("age") Integer age,
@Param("status") Integer status
);
}
XML中使用:
xml
<select id="selectByCondition" resultType="User">
SELECT * FROM user
WHERE name = #{name}
AND age = #{age}
AND status = #{status}
</select>
2.通过 Map 传递参数
适用场景:动态参数或参数数量不确定时。
缺点:参数名需硬编码,类型不安全。
java
List<User> selectByMap(Map<String, Object> params);
java
Map<String, Object> params = new HashMap<>();
params.put("name", "Alice");
params.put("age", 25);
userMapper.selectByMap(params);
XML中使用:
xml
<select id="selectByMap" resultType="User">
SELECT * FROM user
WHERE name = #{name}
AND age = #{age}
</select>
3.使用 JavaBean 对象封装参数
适用场景:参数逻辑相关且需要复用。
优点:类型安全,易于扩展。
java
// 定义参数对象
@Data
public class UserQuery {
private String name;
private Integer age;
private Integer status;
}
// Mapper接口
List<User> selectByBean(UserQuery query);
XML中使用:
xml
<select id="selectByBean" resultType="User" parameterType="com.example.UserQuery">
SELECT * FROM user
WHERE name = #{name}
AND age = #{age}
AND status = #{status}
</select>
4.混合使用 @Param 和 JavaBean
适用场景:部分参数需要复用,部分参数为临时参数。
java
List<User> selectByMixed(
@Param("query") UserQuery query,
@Param("role") String role
);
XML中使用:
xml
<select id="selectByMixed" resultType="User">
SELECT * FROM user
WHERE name = #{query.name}
AND age = #{query.age}
AND role = #{role}
</select>
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
@Param 注解 |
简单直观,类型安全 | 参数较多时方法签名冗长 | 参数少(≤3个) |
Map 传递 | 灵活,适合动态参数 | 需硬编码key,类型不安全 | 参数名或数量不确定时 |
JavaBean 封装 | 类型安全,易于扩展 | 需额外定义类 | 参数逻辑相关且需复用 |
混合使用 | 兼顾灵活性和复用性 | 复杂度较高 | 部分参数需复用的场景 |
6.MyBatis分页插件的原理
MyBatis分页插件(如PageHelper)的核心原理是通过拦截器(interceptor)动态修改SQL语句实现物理分页。
核心实现原理
拦截器机制
拦截目标:StatementHandler.prepare() 方法
触发时机:在SQL执行前拦截并改写
java
@Intercepts({
@Signature(type= StatementHandler.class,
method="prepare",
args={Connection.class, Integer.class})
})
public class PageInterceptor implements Interceptor {
// 拦截SQL执行流程
}
分页流程
1.检测是否需要分页:通过ThreadLocal保存分页参数(页码、每页条数)
java
PageHelper.startPage(1, 10); // 设置分页参数
2.改写原始SQL
MySQL:添加 LIMIT offset,size
sql
-- 原始SQL
SELECT * FROM user;
-- 改写后
SELECT * FROM user LIMIT 0, 10;
3.执行修改后的SQL
通过反射调用原 StatementHandler 继续执行
4.获取总记录数(可选)
执行COUNT(*) 查询获取总数。