MySQL 性能优化最佳实践

1. 为查询缓存优化查询

详细解释:

在 MySQL 5.7 及以前版本中,查询缓存可以存储 SELECT 语句及其结果集。当完全相同的查询再次执行时,MySQL 直接从缓存返回结果,避免了解析、优化和执行的开销。

具体示例:

sql 复制代码
-- 不会使用查询缓存的写法(因为 CURDATE() 是动态函数)
SELECT username FROM user WHERE signup_date >= CURDATE();

-- 会使用查询缓存的写法
SET @today = DATE_FORMAT(NOW(), '%Y-%m-%d');
SELECT username FROM user WHERE signup_date >= @today;

现代说明:

MySQL 8.0 已移除查询缓存,因为:

  • 高并发下缓存失效竞争严重

  • 现代应用更适合使用应用层缓存(如 Redis)

  • 但理解此原理有助于编写更高效的 SQL


2. 使用 EXPLAIN 分析查询

详细解释:

EXPLAIN 显示 MySQL 如何执行查询,包括:

  • 使用的索引

  • 表连接顺序

  • 扫描的行数

  • 是否使用临时表

具体示例:

sql 复制代码
-- 分析查询执行计划
EXPLAIN SELECT u.name, o.order_date 
FROM users u 
JOIN orders o ON u.id = o.user_id 
WHERE u.country = 'China' 
ORDER BY o.order_date DESC;

-- 输出结果示例:
-- id: 1
-- select_type: SIMPLE
-- table: u
-- type: ref
-- key: idx_country
-- rows: 1000
-- Extra: Using where; Using temporary; Using filesort

关键指标解读:

  • typeconst > eq_ref > ref > range > index > ALL

  • key:实际使用的索引

  • rows:预估扫描行数(越小越好)

  • Extra :避免出现 Using filesortUsing temporary


3. 当只要一行数据时使用 LIMIT 1

详细解释:

明确告知优化器只需找到第一条匹配记录即可停止搜索。

具体示例:

sql 复制代码
-- 检查用户是否存在(效率低)
SELECT * FROM users WHERE email = 'user@example.com';
-- 即使找到记录也会继续扫描

-- 检查用户是否存在(效率高)
SELECT 1 FROM users WHERE email = 'user@example.com' LIMIT 1;
-- 找到第一条匹配记录立即返回

性能对比:

sql 复制代码
-- 表有 100万条数据,目标记录在第 10条
-- 无 LIMIT 1:扫描 10行
-- 有 LIMIT 1:扫描 1行

4. 为搜索字段建索引

详细解释:

索引就像书籍的目录,可以快速定位数据。

具体示例:

sql 复制代码
-- 创建前(0.25秒)
SELECT COUNT(*) FROM users WHERE last_name LIKE 'Zhang%';
-- 全表扫描 100万行

-- 创建索引
ALTER TABLE users ADD INDEX idx_last_name (last_name);

-- 创建后(0.02秒)
SELECT COUNT(*) FROM users WHERE last_name LIKE 'Zhang%';
-- 只扫描 'Zhang' 开头的记录

索引类型选择:

sql 复制代码
-- 单列索引
ALTER TABLE users ADD INDEX idx_email (email);

-- 复合索引(最左前缀原则)
ALTER TABLE users ADD INDEX idx_country_city (country, city);

-- 唯一索引
ALTER TABLE users ADD UNIQUE INDEX uk_email (email);

-- 全文索引(用于文本搜索)
ALTER TABLE articles ADD FULLTEXT INDEX ft_content (title, content);

5. 在 JOIN 时使用相同类型的字段

详细解释:

类型不匹配会导致索引失效,MySQL 需要隐式类型转换。

具体示例:

sql 复制代码
-- 错误示例:类型不匹配
CREATE TABLE users (
    id INT PRIMARY KEY,
    state_code VARCHAR(10)
);

CREATE TABLE states (
    state_code INT,  -- 这里是 INT 类型
    state_name VARCHAR(50)
);

-- JOIN 时索引失效(需要类型转换)
SELECT u.name, s.state_name 
FROM users u 
JOIN states s ON u.state_code = s.state_code;  -- VARCHAR 与 INT 比较

-- 正确示例:类型一致
CREATE TABLE states (
    state_code VARCHAR(10),  -- 改为 VARCHAR
    state_name VARCHAR(50),
    INDEX idx_state_code (state_code)
);

字符集也要一致:

sql 复制代码
-- 检查字符集
SHOW CREATE TABLE users;
SHOW CREATE TABLE companies;

-- 如果不一致,修改字符集
ALTER TABLE companies CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

6. 避免 ORDER BY RAND()

详细解释:

ORDER BY RAND() 会为每行生成随机数并排序,性能极差。

具体示例:

sql 复制代码
-- 错误做法:性能灾难
SELECT username FROM users ORDER BY RAND() LIMIT 1;
-- 对 100万行数据每行调用 RAND() 并排序

-- 正确做法:应用层生成随机偏移量
-- 第一步:获取总行数
SELECT COUNT(*) AS total FROM users;

-- 第二步:应用层生成随机数(假设 total=1000000)
SET @random_offset = FLOOR(RAND() * 1000000);

-- 第三步:使用偏移量查询
SELECT username FROM users LIMIT @random_offset, 1;

更高效的随机采样:

sql 复制代码
-- 对于大表,可以使用条件随机采样
SELECT username FROM users 
WHERE id >= (SELECT FLOOR(MAX(id) * RAND()) FROM users) 
LIMIT 1;

**7. 避免 SELECT ***

详细解释:

只选择需要的列可以减少:

  • 网络传输量

  • 内存占用

  • 磁盘 I/O

具体示例:

sql 复制代码
-- 不推荐:读取所有列
SELECT * FROM users WHERE user_id = 1;
-- 返回 id, username, email, password, created_at, updated_at, profile...

-- 推荐:只读取需要的列
SELECT username, email FROM users WHERE user_id = 1;
-- 只返回 username, email

真实性能对比:

sql 复制代码
-- 用户表有 50个字段,但页面只需要用户名和头像
-- SELECT *:传输 10KB 数据
-- SELECT username, avatar:传输 2KB 数据
-- 节省 80% 的网络传输和内存占用

8. 永远为每张表设置 ID 主键

详细解释:

InnoDB 使用聚簇索引,数据按主键顺序存储。

具体示例:

sql 复制代码
-- 错误做法:使用业务字段作为主键
CREATE TABLE users (
    email VARCHAR(255) PRIMARY KEY,  -- VARCHAR 主键性能差
    username VARCHAR(50)
);

-- 正确做法:使用自增 INT 主键
CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    username VARCHAR(50) NOT NULL,
    INDEX idx_email (email)
);

主键选择的影响:

sql 复制代码
-- UUID 主键 vs INT 主键
CREATE TABLE orders_uuid (
    id CHAR(36) PRIMARY KEY,  -- UUID,随机写入,页分裂严重
    amount DECIMAL(10,2)
);

CREATE TABLE orders_int (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,  -- 顺序写入,性能好
    amount DECIMAL(10,2)
);

9. 使用 ENUM 代替 VARCHAR

详细解释:

ENUM 在内部存储为整数,但显示为字符串。

具体示例:

sql 复制代码
-- VARCHAR 方式(每个值占用变长空间)
CREATE TABLE users (
    id INT PRIMARY KEY,
    gender VARCHAR(10),  -- 'male', 'female', 'unknown'
    status VARCHAR(20)   -- 'active', 'inactive', 'pending', 'banned'
);

-- ENUM 方式(每个值只占 1-2 字节)
CREATE TABLE users (
    id INT PRIMARY KEY,
    gender ENUM('male', 'female', 'unknown') NOT NULL,
    status ENUM('active', 'inactive', 'pending', 'banned') NOT NULL
);

存储空间对比:

sql 复制代码
-- 100万条记录
-- VARCHAR(10) 性别字段:平均 5字节 × 100万 = 5MB
-- ENUM 性别字段:1字节 × 100万 = 1MB
-- 节省 80% 存储空间

10. 从 PROCEDURE ANALYSE() 获取建议

详细解释:

分析表中实际数据分布,给出数据类型优化建议。

具体示例:

sql 复制代码
-- 获取字段优化建议
SELECT * FROM users PROCEDURE ANALYSE(256, 1024);

-- 输出示例:
-- Field_name: users.email
-- Min_value: a@test.com
-- Max_value: zzz@example.org  
-- Min_length: 8
-- Max_length: 32
-- Empties_or_zeros: 0
-- Nulls: 0
-- Avg_value_or_avg_length: 18.23
-- Std: NULL
-- Optimal_fieldtype: VARCHAR(32) NOT NULL

实际应用:

sql 复制代码
-- 根据建议优化表结构
ALTER TABLE users 
MODIFY COLUMN email VARCHAR(32) NOT NULL,
MODIFY COLUMN age TINYINT UNSIGNED NOT NULL;

11. 尽可能使用 NOT NULL

详细解释:

NULL 需要额外存储空间和特殊处理逻辑。

具体示例:

sql 复制代码
-- 允许 NULL 的表
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    description TEXT,  -- 可能为 NULL
    price DECIMAL(10,2)  -- 可能为 NULL
);

-- 使用 NOT NULL 和默认值
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT NOT NULL DEFAULT '',
    price DECIMAL(10,2) NOT NULL DEFAULT 0.00
);
查询性能影响:
sql 复制代码
-- NULL 值查询需要特殊处理
SELECT * FROM products WHERE description IS NULL;
SELECT * FROM products WHERE description IS NOT NULL;

-- NOT NULL 字段查询更简单
SELECT * FROM products WHERE description = '';

12. 使用 Prepared Statements

详细解释:

预处理语句提供性能和安全性双重好处。

具体示例:

java 复制代码
import java.sql.*;

public class UserDAO {
    
    public String getUserByEmailAndStatus(String email, String status) {
        String username = null;
        
        // 使用 try-with-resources 自动关闭资源
        try (Connection conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/test", "username", "password");
             PreparedStatement stmt = conn.prepareStatement(
                "SELECT username FROM users WHERE email = ? AND status = ?")) {
            
            // 绑定参数
            stmt.setString(1, email);
            stmt.setString(2, status);
            
            // 执行并获取结果
            try (ResultSet rs = stmt.executeQuery()) {
                if (rs.next()) {
                    username = rs.getString("username");
                }
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return username;
    }
}

性能优势:

sql 复制代码
-- 相同结构的查询只需要解析一次
-- 第一次:解析、优化、执行
-- 后续:直接使用缓存的执行计划

13. 无缓冲查询

详细解释:

逐行获取结果,不等待所有数据加载到内存。

具体示例:

java 复制代码
import java.sql.*;

public class StreamingPreparedStatement {
    
    public void processLargeTableWithCondition(String condition) {
        String url = "jdbc:mysql://localhost:3306/test?useCursorFetch=true";
        
        try (Connection conn = DriverManager.getConnection(url, "username", "password");
             PreparedStatement stmt = conn.prepareStatement(
                 "SELECT * FROM large_table WHERE status = ?",
                 ResultSet.TYPE_FORWARD_ONLY,
                 ResultSet.CONCUR_READ_ONLY)) {
            
            // 设置参数
            stmt.setString(1, condition);
            
            // 启用流式读取
            stmt.setFetchSize(Integer.MIN_VALUE);
            
            try (ResultSet rs = stmt.executeQuery()) {
                
                // 逐行处理
                while (rs.next()) {
                    Map<String, Object> row = new HashMap<>();
                    ResultSetMetaData metaData = rs.getMetaData();
                    
                    for (int i = 1; i <= metaData.getColumnCount(); i++) {
                        String columnName = metaData.getColumnName(i);
                        Object value = rs.getObject(i);
                        row.put(columnName, value);
                    }
                    
                    // 处理整行数据
                    processRow(row);
                }
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    private void processRow(Map<String, Object> row) {
        // 处理单行数据
        System.out.println("Processing row: " + row);
    }
}

适用场景:

  • 处理大型结果集(如导出数据)

  • 内存有限的服务器环境

  • 需要快速获取第一行数据的场景


14. IP 地址存为 UNSIGNED INT

详细解释:

IP地址本质是32位无符号整数。

具体示例:

sql 复制代码
-- 创建表
CREATE TABLE access_logs (
    id INT PRIMARY KEY AUTO_INCREMENT,
    ip_address INT UNSIGNED NOT NULL,  -- 存储为整数
    access_time DATETIME,
    INDEX idx_ip (ip_address)
);

-- 插入数据(字符串转整数)
INSERT INTO access_logs (ip_address, access_time) 
VALUES (INET_ATON('192.168.1.100'), NOW());

-- 查询数据(整数转字符串)
SELECT id, INET_NTOA(ip_address) AS ip, access_time 
FROM access_logs 
WHERE ip_address BETWEEN INET_ATON('192.168.1.0') AND INET_ATON('192.168.1.255');

存储空间对比:

sql 复制代码
-- VARCHAR(15):15字节
-- INT UNSIGNED:4字节
-- 节省 73% 存储空间

15. 固定长度的表更快

详细解释:

定长记录可以快速计算偏移量,随机访问性能好。

具体示例:

sql 复制代码
-- 变长表(包含 VARCHAR, TEXT, BLOB)
CREATE TABLE user_profiles (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    bio TEXT,           -- 变长字段
    avatar BLOB         -- 变长字段
);

-- 定长表(所有字段长度固定)
CREATE TABLE user_stats (
    id INT PRIMARY KEY,
    login_count INT,
    last_login DATE,    -- 固定3字节
    status TINYINT      -- 固定1字节
);

性能优势:

sql 复制代码
-- 计算第N条记录的位置:
-- 定长表:offset = (N-1) × record_size
-- 变长表:需要遍历前面的所有记录

16. 垂直分割

详细解释:

将宽表按列拆分为多个窄表。

具体示例:

sql 复制代码
-- 原始宽表(经常查询和更新的字段混在一起)
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(255),
    email VARCHAR(100),
    last_login DATETIME,  -- 频繁更新,会清空查询缓存
    bio TEXT,             -- 很少读取的大字段
    preferences JSON      -- 很少读取的复杂字段
);

-- 垂直分割后
CREATE TABLE users_core (  -- 核心表,频繁访问
    id INT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(255),
    email VARCHAR(100)
);

CREATE TABLE users_activity (  -- 活动表,频繁更新
    user_id INT PRIMARY KEY,
    last_login DATETIME,
    login_count INT
);

CREATE TABLE users_profile (  -- 资料表,很少访问
    user_id INT PRIMARY KEY,
    bio TEXT,
    preferences JSON
);

查询模式:

sql 复制代码
-- 登录验证(只访问核心表)
SELECT id, username, password FROM users_core WHERE username = ?;

-- 更新最后登录时间(只更新活动表)
UPDATE users_activity SET last_login = NOW() WHERE user_id = ?;

-- 查看用户资料(需要JOIN)
SELECT u.username, p.bio 
FROM users_core u 
JOIN users_profile p ON u.id = p.user_id 
WHERE u.id = ?;

17. 拆分大的 DELETE 或 INSERT

详细解释:

大事务会长时间锁表,阻塞其他操作。

具体示例:

java 复制代码
import java.sql.*;

public class BatchDeleteExample {
    
    public void batchDeleteOldLogs() {
        Connection conn = null;
        PreparedStatement stmt = null;
        
        try {
            // 1. 创建连接
            conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/test", 
                "username", 
                "password"
            );
            
            // 2. 关闭自动提交,可选(如果需要事务)
            conn.setAutoCommit(false);
            
            // 3. 准备删除语句
            String sql = "DELETE FROM logs WHERE log_date < ? LIMIT 1000";
            stmt = conn.prepareStatement(sql);
            stmt.setString(1, "2023-01-01");
            
            int totalDeleted = 0;
            int affectedRows;
            
            // 4. 循环分批删除
            do {
                // 执行删除
                affectedRows = stmt.executeUpdate();
                totalDeleted += affectedRows;
                
                // 提交当前批次
                conn.commit();
                
                System.out.println("删除 " + affectedRows + " 条记录,总计: " + totalDeleted);
                
                // 如果删除了数据,休息一会儿
                if (affectedRows > 0) {
                    try {
                        Thread.sleep(100); // 休息 100ms
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
                
            } while (affectedRows > 0);
            
            System.out.println("删除完成,总共删除 " + totalDeleted + " 条记录");
            
        } catch (SQLException e) {
            e.printStackTrace();
            // 回滚事务
            try {
                if (conn != null) conn.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            // 5. 关闭资源
            try {
                if (stmt != null) stmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

批量插入优化:

sql 复制代码
-- 大量插入也建议分批
INSERT INTO audit_log (user_id, action, timestamp) VALUES
(1, 'login', NOW()),
(2, 'logout', NOW()),
-- ... 每次插入 100-1000条
(100, 'view', NOW());

18. 越小的列越快

详细解释:

小数据类型占用更少存储空间,处理速度更快。

具体示例:

java 复制代码
-- 过度设计:使用过大的数据类型
CREATE TABLE config (
    id BIGINT PRIMARY KEY,          -- 实际只有100条记录
    value VARCHAR(1000),            -- 实际平均长度20字符
    created_at DATETIME,            -- 不需要时间精度
    flags BIGINT                    -- 实际只用了8个标志位
);

-- 优化后:使用合适的最小类型
CREATE TABLE config (
    id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,  -- 0-65535
    value VARCHAR(50) NOT NULL DEFAULT '',
    created_at DATE,                 -- 只需要日期
    flags TINYINT UNSIGNED           -- 0-255,足够存储8个标志位
);

存储空间计算:

sql 复制代码
-- 原始设计:8 + 1000 + 8 + 8 = 1024字节/行
-- 优化后:2 + 50 + 3 + 1 = 56字节/行
-- 节省 95% 存储空间

19. 选择正确的存储引擎

详细解释:

不同存储引擎适合不同场景。

MyISAM vs InnoDB 对比:

特性 MyISAM InnoDB
事务 不支持 支持
行级锁 表级锁 行级锁
外键 不支持 支持
崩溃恢复 较差 优秀
COUNT(*) 极快 需要扫描
全文索引 支持 MySQL 5.6+ 支持

具体示例:

sql 复制代码
-- 读多写少的日志表(适合 MyISAM)
CREATE TABLE access_log (
    id INT PRIMARY KEY,
    ip VARCHAR(15),
    access_time DATETIME
) ENGINE=MyISAM;

-- 需要事务的用户表(必须 InnoDB)
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE,
    balance DECIMAL(10,2)
) ENGINE=InnoDB;

-- 会话表(适合 MEMORY)
CREATE TABLE sessions (
    session_id VARCHAR(128) PRIMARY KEY,
    user_id INT,
    data TEXT,
    expires DATETIME
) ENGINE=MEMORY;

20. 使用 ORM 的注意事项

详细解释:

ORM 提供便利但可能产生性能问题。

N+1 查询问题示例:

java 复制代码
import java.sql.*;
import java.util.*;

public class NPlusOneProblemExample {
    
    // 错误的做法:N+1 查询
    public List<User> getUsersWithPostsNPlusOne() {
        List<User> users = new ArrayList<>();
        
        // 第一次查询:获取所有用户
        String userSql = "SELECT id, username, email FROM users";
        
        try (Connection conn = getConnection();
             PreparedStatement userStmt = conn.prepareStatement(userSql);
             ResultSet userRs = userStmt.executeQuery()) {
            
            while (userRs.next()) {
                User user = new User();
                user.setId(userRs.getLong("id"));
                user.setUsername(userRs.getString("username"));
                user.setEmail(userRs.getString("email"));
                
                // 为每个用户单独查询帖子(N+1 问题!)
                List<Post> posts = getPostsByUserId(user.getId());
                user.setPosts(posts);
                
                users.add(user);
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return users;
    }
    
    // 单独查询用户帖子的方法(导致 N+1 问题)
    private List<Post> getPostsByUserId(Long userId) {
        List<Post> posts = new ArrayList<>();
        String sql = "SELECT id, title, content, user_id FROM posts WHERE user_id = ?";
        
        try (Connection conn = getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            
            stmt.setLong(1, userId);
            
            try (ResultSet rs = stmt.executeQuery()) {
                while (rs.next()) {
                    Post post = new Post();
                    post.setId(rs.getLong("id"));
                    post.setTitle(rs.getString("title"));
                    post.setContent(rs.getString("content"));
                    post.setUserId(rs.getLong("user_id"));
                    posts.add(post);
                }
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return posts;
    }
    
    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/test", "username", "password");
    }
}

ORM 优化技巧:

java 复制代码
import java.sql.*;
import java.util.*;

public class EagerLoadingExample {
    
    // 正确的做法:使用 JOIN 预加载
    public List<User> getUsersWithPostsEagerLoading() {
        List<User> users = new ArrayList<>();
        Map<Long, User> userMap = new HashMap<>();
        
        String sql = "SELECT u.id as user_id, u.username, u.email, " +
                    "p.id as post_id, p.title, p.content, p.user_id " +
                    "FROM users u " +
                    "LEFT JOIN posts p ON u.id = p.user_id " +
                    "ORDER BY u.id";
        
        try (Connection conn = getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql);
             ResultSet rs = stmt.executeQuery()) {
            
            while (rs.next()) {
                Long userId = rs.getLong("user_id");
                
                // 如果用户还不存在,创建用户对象
                User user = userMap.get(userId);
                if (user == null) {
                    user = new User();
                    user.setId(userId);
                    user.setUsername(rs.getString("username"));
                    user.setEmail(rs.getString("email"));
                    user.setPosts(new ArrayList<>());
                    userMap.put(userId, user);
                    users.add(user);
                }
                
                // 处理帖子数据(可能为 null,因为 LEFT JOIN)
                Long postId = rs.getLong("post_id");
                if (!rs.wasNull()) { // 检查帖子是否存在
                    Post post = new Post();
                    post.setId(postId);
                    post.setTitle(rs.getString("title"));
                    post.setContent(rs.getString("content"));
                    post.setUserId(userId);
                    
                    user.getPosts().add(post);
                }
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        return users;
    }
}

21. 小心永久链接

详细解释:

持久连接可以减少连接建立开销,但可能造成连接堆积。

问题示例:

java 复制代码
import java.sql.*;

public class BadConnectionExample {
    private static Connection connection; // 静态持久连接
    
    // 错误的做法:使用静态持久连接
    public static Connection getConnection() throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/database", 
                "user", 
                "password"
            );
        }
        return connection;
    }
    
    // 使用示例 - 存在问题!
    public void queryUsers() {
        try {
            Connection conn = getConnection(); // 获取持久连接
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM users");
            
            // 处理结果...
            while (rs.next()) {
                System.out.println(rs.getString("username"));
            }
            
            // 注意:这里没有关闭连接!
            // rs.close();
            // stmt.close();
            // 连接保持打开状态
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    // 另一个方法也使用同一个连接
    public void updateUser(int userId) {
        try {
            Connection conn = getConnection(); // 同一个持久连接
            PreparedStatement stmt = conn.prepareStatement(
                "UPDATE users SET last_login = NOW() WHERE id = ?");
            stmt.setInt(1, userId);
            stmt.executeUpdate();
            
            // 同样不关闭语句和连接
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
java 复制代码
public class ConnectionProblems {
    public static void main(String[] args) {
        BadConnectionExample example = new BadConnectionExample();
        
        // 在多线程环境下会产生严重问题
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                example.queryUsers();
                example.updateUser(1);
            }).start();
        }
        
        // 问题1: 连接状态混乱
        // 一个线程设置的事务隔离级别会影响其他线程
        // 一个线程开启事务会影响其他线程
        
        // 问题2: 连接耗尽
        // 每个应用实例保持一个连接,高并发时不够用
        
        // 问题3: 资源泄漏
        // 连接永远不会关闭,直到应用重启
    }
}

现代解决方案:

使用 HikariCP

java 复制代码
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.sql.*;

public class ConnectionPoolExample {
    private static DataSource dataSource;
    
    static {
        // 初始化连接池
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/database");
        config.setUsername("user");
        config.setPassword("password");
        
        // 连接池配置
        config.setMaximumPoolSize(20);          // 最大连接数
        config.setMinimumIdle(5);               // 最小空闲连接
        config.setConnectionTimeout(30000);     // 连接超时时间(ms)
        config.setIdleTimeout(600000);          // 空闲连接超时时间(ms)
        config.setMaxLifetime(1800000);         // 连接最大生命周期(ms)
        config.setLeakDetectionThreshold(60000); // 泄漏检测阈值(ms)
        
        // MySQL 特定配置
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        
        dataSource = new HikariDataSource(config);
    }
    
    // 正确的做法:从连接池获取连接,使用后关闭
    public void queryUsers() {
        // 使用 try-with-resources 自动关闭连接
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
            
            while (rs.next()) {
                System.out.println(rs.getString("username"));
            }
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // 连接自动返回到连接池,不会真正关闭
    }
    
    public void updateUser(int userId) {
        String sql = "UPDATE users SET last_login = NOW() WHERE id = ?";
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            
            stmt.setInt(1, userId);
            stmt.executeUpdate();
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

总结表格

优化类别 关键实践 预期收益
查询优化 EXPLAIN、LIMIT 1、避免 SELECT * 20-50% 查询性能提升
索引优化 为搜索字段建索引、JOIN字段类型一致 10-100倍查询加速
表结构设计 合适的数据类型、垂直分割、NOT NULL 30-80% 存储空间节省
操作优化 分批处理大操作、使用预处理语句 避免服务中断,提升并发
架构选择 正确存储引擎、谨慎使用ORM和持久连接 系统稳定性和扩展性

这些优化实践需要根据具体业务场景和数据特征进行选择和调整。建议先通过 EXPLAIN 和慢查询日志识别瓶颈,再有针对性地实施优化。

相关推荐
北i2 小时前
TiDB 关联子查询去关联优化实战案例与原理深度解析
java·数据库·sql·tidb
清风6666662 小时前
基于单片机的智慧校园自动打铃系统设计
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
我有一棵树4 小时前
深入理解html 加载、解析、渲染和 DOMContentLoaded、onload事件
前端·性能优化·html
JIngJaneIL4 小时前
就业|高校就业|基于ssm+vue的高校就业信息系统的设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·高校就业
CXH7284 小时前
nginx-file-server
运维·数据库·nginx
一 乐4 小时前
社区互助|社区交易|基于springboot+vue的社区互助交易系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小区互助系统
q***57744 小时前
Spring Boot 实战:轻松实现文件上传与下载功能
java·数据库·spring boot
武子康4 小时前
Java-174 FastFDS 从单机到分布式文件存储:实战与架构取舍
java·大数据·分布式·性能优化·系统架构·dfs·fastdfs
失散134 小时前
分布式专题——57 如何保证MySQL数据库到ES的数据一致性
java·数据库·分布式·mysql·elasticsearch·架构