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

全部评论

相关推荐

评论
2
6
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务