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圣经