Java 秋招面试之数据库与缓存话题
第8章 数据库与缓存
面试重要程度:⭐⭐⭐⭐⭐
常见提问方式:MySQL索引优化、Redis数据结构、缓存穿透
预计阅读时间:50分钟
开场白
兄弟,数据库和缓存绝对是后端面试的重头戏!我敢说,95%的Java面试都会深入考察MySQL和Redis。这不仅是技术基础,更能体现你对系统性能优化的理解深度。
今天我们就把数据库优化和缓存设计的核心知识点彻底搞透,让你在面试中展现出扎实的数据存储功底。
🗄️ 8.1 MySQL性能优化
索引设计原则与B+树结构
面试必问:
面试官:"说说MySQL的索引原理,为什么使用B+树而不是B树或红黑树?"
B+树结构优势:
-- B+树的特点分析
-- 1. 所有数据都存储在叶子节点
-- 2. 叶子节点之间有指针连接,便于范围查询
-- 3. 非叶子节点只存储键值,不存储数据,可以存储更多索引项
-- 4. 树的高度更低,减少磁盘IO次数
-- 示例:用户表索引设计
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 主键索引(聚簇索引)
username VARCHAR(50) NOT NULL, -- 普通索引
email VARCHAR(100) NOT NULL, -- 唯一索引
age INT, -- 普通索引
city VARCHAR(50), -- 组合索引的一部分
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 索引创建
UNIQUE KEY uk_email (email), -- 唯一索引
KEY idx_username (username), -- 单列索引
KEY idx_age (age), -- 单列索引
KEY idx_city_age (city, age), -- 组合索引
KEY idx_create_time (create_time) -- 时间索引
);
索引设计最佳实践:
-- 1. 最左前缀原则 -- 组合索引 idx_city_age (city, age) 可以支持以下查询: SELECT * FROM user WHERE city = '北京'; -- ✅ 使用索引 SELECT * FROM user WHERE city = '北京' AND age = 25; -- ✅ 使用索引 SELECT * FROM user WHERE age = 25; -- ❌ 不使用索引 -- 2. 覆盖索引优化 -- 创建覆盖索引,避免回表查询 CREATE INDEX idx_username_email ON user(username, email); -- 这个查询只需要访问索引,不需要回表 SELECT username, email FROM user WHERE username = 'zhangsan'; -- 3. 前缀索引 -- 对于长字符串字段,使用前缀索引节省空间 CREATE INDEX idx_email_prefix ON user(email(10)); -- 4. 函数索引(MySQL 8.0+) -- 为经常使用函数的查询创建函数索引 CREATE INDEX idx_upper_username ON user((UPPER(username))); SELECT * FROM user WHERE UPPER(username) = 'ZHANGSAN';
执行计划分析
面试重点:
面试官:"如何分析SQL的执行计划?各个字段代表什么意思?"
EXPLAIN详解:
-- 分析复杂查询的执行计划
EXPLAIN SELECT
u.username,
u.email,
o.order_no,
o.total_amount
FROM user u
INNER JOIN order_info o ON u.id = o.user_id
WHERE u.city = '北京'
AND u.age BETWEEN 20 AND 30
AND o.create_time >= '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 10;
-- 执行计划字段解析:
/*
+----+-------------+-------+------------+-------+------------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+------------------+---------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | u | NULL | range | idx_city_age | idx_... | 158 | NULL | 1000 | 100.00 | Using where |
| 1 | SIMPLE | o | NULL | ref | idx_user_id_time | idx_... | 8 | u.id | 10 | 33.33 | Using where |
+----+-------------+-------+------------+-------+------------------+---------+---------+-------+------+----------+-------------+
字段含义:
- id: 查询序列号,数字越大越先执行
- select_type: 查询类型(SIMPLE、PRIMARY、SUBQUERY等)
- table: 表名
- type: 访问类型(system > const > eq_ref > ref > range > index > ALL)
- possible_keys: 可能使用的索引
- key: 实际使用的索引
- key_len: 索引长度
- ref: 索引的哪一列被使用了
- rows: 预估扫描的行数
- filtered: 过滤的百分比
- Extra: 额外信息
*/
性能优化案例:
-- 慢查询优化实例 -- 原始慢查询(耗时3秒) SELECT * FROM order_info WHERE status = 1 AND create_time >= '2024-01-01' AND total_amount > 100 ORDER BY create_time DESC LIMIT 20; -- 问题分析: -- 1. 没有合适的索引 -- 2. 全表扫描 -- 3. 文件排序 -- 优化方案1:创建组合索引 CREATE INDEX idx_status_time_amount ON order_info(status, create_time, total_amount); -- 优化方案2:调整查询条件顺序 SELECT * FROM order_info WHERE status = 1 AND create_time >= '2024-01-01' AND total_amount > 100 ORDER BY create_time DESC LIMIT 20; -- 优化后执行计划: -- type: range(范围扫描) -- key: idx_status_time_amount -- rows: 100(从原来的10万行降到100行) -- Extra: Using index condition(使用索引条件下推)
慢查询优化
慢查询监控配置:
-- 开启慢查询日志 SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录到慢查询日志 SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 记录未使用索引的查询 -- 查看慢查询配置 SHOW VARIABLES LIKE '%slow_query%'; SHOW VARIABLES LIKE 'long_query_time';
常见慢查询优化技巧:
-- 1. 避免SELECT * -- ❌ 不好的写法 SELECT * FROM user WHERE username = 'zhangsan'; -- ✅ 好的写法 SELECT id, username, email FROM user WHERE username = 'zhangsan'; -- 2. 使用LIMIT优化大结果集 -- ❌ 不好的写法 SELECT * FROM order_info ORDER BY create_time DESC; -- ✅ 好的写法 SELECT * FROM order_info ORDER BY create_time DESC LIMIT 20; -- 3. 优化深分页查询 -- ❌ 传统分页(OFFSET很大时性能差) SELECT * FROM order_info ORDER BY id LIMIT 100000, 20; -- ✅ 使用游标分页 SELECT * FROM order_info WHERE id > 100000 ORDER BY id LIMIT 20; -- 4. 避免在WHERE子句中使用函数 -- ❌ 不好的写法 SELECT * FROM user WHERE YEAR(create_time) = 2024; -- ✅ 好的写法 SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'; -- 5. 使用EXISTS替代IN -- ❌ 性能较差(子查询结果集很大时) SELECT * FROM user WHERE id IN (SELECT user_id FROM order_info WHERE status = 1); -- ✅ 性能更好 SELECT * FROM user u WHERE EXISTS (SELECT 1 FROM order_info o WHERE o.user_id = u.id AND o.status = 1);
主从复制与读写分离
主从复制原理:
-- 主库配置(my.cnf)
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
-- 从库配置(my.cnf)
[mysqld]
server-id = 2
relay-log = mysql-relay-bin
read_only = 1
super_read_only = 1
-- 主库创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 从库配置主从关系
CHANGE MASTER TO
MASTER_HOST='192.168.1.100',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154;
START SLAVE;
SHOW SLAVE STATUS\G
读写分离实现:
// 数据源配置
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource masterDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://master:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("password");
dataSource.setMaximumPoolSize(20);
return dataSource;
}
@Bean
public DataSource slaveDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://slave:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("password");
dataSource.setMaximumPoolSize(20);
return dataSource;
}
@Bean
public DataSource routingDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource());
dataSourceMap.put("slave", slaveDataSource());
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
}
// 动态数据源路由
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// 数据源上下文
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
// 读写分离注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
// AOP切面实现读写分离
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void setReadDataSourceType(ReadOnly readOnly) {
DataSourceContextHolder.setDataSourceType("slave");
}
@Before("execution(* com.example.service.*.select*(..)) || " +
"execution(* com.example.service.*.find*(..)) || " +
"execution(* com.example.service.*.get*(..))")
public void setReadDataSourceTypeForQuery() {
if (DataSourceContextHolder.getDataSourceType() == null) {
DataSourceContextHolder.setDataSourceType("slave");
}
}
@After("@annotation(readOnly) || " +
"execution(* com.example.service.*.select*(..)) || " +
"execution(* com.example.service.*.find*(..)) || " +
"execution(* com.example.service.*.get*(..))")
public void clearDataSourceType() {
DataSourceContextHolder.clearDataSourceType();
}
}
🚀 8.2 Redis核心数据结构
String、Hash、List、Set、ZSet底层实现
面试高频:
面试官:"Redis的五种数据结构底层是如何实现的?各自的使用场景是什么?"
String类型:
// String的底层实现:SDS(Simple Dynamic String)
// 相比C字符串的优势:
// 1. O(1)时间复杂度获取字符串长度
// 2. 杜绝缓冲区溢出
// 3. 减少修改字符串时带来的内存重分配次数
// 4. 二进制安全
// 使用场景和命令
@Service
public class RedisStringService {
@Autowired
private StringRedisTemplate redisTemplate;
// 1. 缓存对象
public void cacheUser(User user) {
String key = "user:" + user.getId();
String value = JSON.toJSONString(user);
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
}
// 2. 计数器
public Long incrementViewCount(String articleId) {
String key = "article:view:" + articleId;
return redisTemplate.opsForValue().increment(key);
}
// 3. 分布式锁
public boolean tryLock(String lockKey, String requestId, int expireTime) {
String result = redisTemplate.execute((RedisCallback<String>) connection -> {
return connection.set(lockKey.getBytes(), requestId.getBytes(),
Expiration.seconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT);
});
return "OK".equals(result);
}
// 4. 限流器(令牌桶)
public boolean isAllowed(String userId, int limit, int window) {
String key = "rate_limit:" + userId + ":" + (System.currentTimeMillis() / window);
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, window, TimeUnit.SECONDS);
}
return count <= limit;
}
}
Hash类型:
// Hash的底层实现:
// 1. 压缩列表(ziplist):当元素数量少且值较小时使用
// 2. 哈希表(hashtable):当元素数量多或值较大时使用
@Service
public class RedisHashService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1. 存储对象属性
public void saveUserInfo(Long userId, Map<String, String> userInfo) {
String key = "user:info:" + userId;
redisTemplate.opsForHash().putAll(key, userInfo);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
// 2. 购物车实现
public void addToCart(Long userId, Long productId, Integer quantity) {
String key = "cart:" + userId;
redisTemplate.opsForHash().put(key, productId.toString(), quantity.toString());
}
public Map<Object, Object> getCart(Long userId) {
String key = "cart:" + userId;
return redisTemplate.opsForHash().entries(key);
}
// 3. 统计信息
public void updateStatistics(String date, String metric, Long value) {
String key = "stats:" + date;
redisTemplate.opsForHash().increment(key, metric, value);
}
}
List类型:
// List的底层实现:
// 1. 压缩列表(ziplist):元素数量少且值较小时
// 2. 双向链表(linkedlist):元素数量多或值较大时
// 3. 快速列表(quicklist):Redis 3.2+,结合了ziplist和linkedlist的优点
@Service
public class RedisListService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1. 消息队列
public void sendMessage(String queue, String message) {
redisTemplate.opsForList().leftPush(queue, message);
}
public String receiveMessage(String queue) {
return (String) redisTemplate.opsForList().rightPop(queue);
}
// 2. 最新动态列表
public void addActivity(Long userId, String activity) {
String key = "user:activities:" + userId;
redisTemplate.opsForList().leftPush(key, activity);
// 只保留最新的100条
redisTemplate.opsForList().trim(key, 0, 99);
}
// 3. 阻塞队列
public String blockingReceive(String queue, int timeout) {
List<Object> result = redisTemplate.opsForList().rightPop(queue, timeout, TimeUnit.SECONDS);
return result != null && !result.isEmpty() ? (String) result.get(1) : null;
}
}
Set类型:
// Set的底层实现:
// 1. 整数集合(intset):当所有元素都是整数且数量较少时
// 2. 哈希表(hashtable):其他情况
@Service
public class RedisSetService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1. 标签系统
public void addUserTags(Long userId, String... tags) {
String key = "user:tags:" + userId;
redisTemplate.opsForSet().add(key, (Object[]) tags);
}
public Set<Object> getUserTags(Long userId) {
String key = "user:tags:" + userId;
return redisTemplate.opsForSet().members(key);
}
// 2. 共同关注
public Set<Object> getCommonFollows(Long userId1, Long userId2) {
String key1 = "user:follows:" + userId1;
String key2 = "user:follows:" + userId2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
// 3. 去重统计
public void addUniqueVisitor(String date, Long userId) {
String key = "unique:visitors:" + date;
redisTemplate.opsForSet().add(key, userId);
}
public Long getUniqueVisitorCount(String date) {
String key = "unique:visitors:" + date;
return redisTemplate.opsForSet().size(key);
}
// 4. 抽奖系统
public Object randomDraw(String activityId) {
String key = "lottery:" + activityId;
return redisTemplate.opsForSet().randomMember(key);
}
}
ZSet类型:
// ZSet的底层实现:
// 1. 压缩列表(ziplist):元素数量少时
// 2. 跳跃表(skiplist)+ 哈希表:元素数量多时
@Service
public class RedisZSetService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 1. 排行榜
public void updateScore(String leaderboard, String player, double score) {
redisTemplate.opsForZSet().add(leaderboard, player, score);
}
public Set<ZSetOperations.TypedTuple<Object>> getTopPlayers(String leaderboard, int count) {
return redisTemplate.opsForZSet().reverseRangeWithScores(leaderboard, 0, count - 1);
}
public Long getPlayerRank(String leaderboard, String player) {
return redisTemplate.opsForZSet().reverseRank(leaderboard, player);
}
// 2. 延时队列
public void addDelayedTask(String queue, String task, long delaySeconds) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
redisTemplate.opsForZSet().add(queue, task, executeTime);
}
public Set<Object> getReadyTasks(String queue) {
long now = System.currentTimeMillis();
return r
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经
查看24道真题和解析