阅读视图

发现新文章,点击刷新页面。
🔲 ☆

MySql入门:备份恢复与安全管理

MySQL备份恢复与安全管理

数据是企业的核心资产,确保数据安全性和可恢复性是DBA最重要的职责。今天,我们将深入探讨MySQL的备份恢复策略和安全管理制度,帮助你构建既安全又可靠的数据库环境。

1. 备份策略与实施

逻辑备份:mysqldump实用技巧

基础备份命令:

# 完整数据库备份mysqldump -u root -p --all-databases --single-transaction --master-data=2 --flush-logs > full_backup_$(date +%Y%m%d).sql# 单个数据库备份mysqldump -u root -p --databases company --single-transaction --routines --triggers --events > company_backup_$(date +%Y%m%d).sql# 单个表备份mysqldump -u root -p company employees departments --single-transaction --where="salary>5000" > high_salary_employees.sql# 压缩备份mysqldump -u root -p --all-databases --single-transaction | gzip > full_backup_$(date +%Y%m%d).sql.gz

高级备份选项:

# 生产环境完整备份脚本mysqldump -u backup_user -p'secure_password' \  --all-databases \  --single-transaction \  --master-data=2 \  --flush-logs \  --routines \  --triggers \  --events \  --hex-blob \  --complete-insert \  --extended-insert \  --max-allowed-packet=1G \  --set-gtid-purged=ON \  --result-file=/backup/full_backup_$(date +%Y%m%d_%H%M%S).sql# 分库备份脚本for DB in $(mysql -u root -p'password' -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|sys)")do    mysqldump -u root -p'password' --databases $DB --single-transaction --routines --triggers > /backup/${DB}_backup_$(date +%Y%m%d).sqldone

备份验证脚本:

#!/bin/bash# backup_verify.shBACKUP_FILE=$1LOG_FILE="/var/log/mysql/backup_verify.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}verify_backup() {    local file=$1        log "开始验证备份文件: $file"        # 检查文件是否存在    if [ ! -f "$file" ]; then        log "错误: 备份文件不存在 - $file"        return 1    fi        # 检查文件大小    local file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)    if [ "$file_size" -lt 1024 ]; then        log "错误: 备份文件过小 - $file"        return 1    fi        # 验证SQL文件完整性    if [[ "$file" == *.sql ]]; then        # 检查SQL文件头        if ! head -n 10 "$file" | grep -q "MySQL dump"; then            log "错误: 无效的SQL备份文件 - $file"            return 1        fi                # 检查SQL文件尾        if ! tail -n 5 "$file" | grep -q "Dump completed"; then            log "警告: 备份文件可能不完整 - $file"        fi    fi        # 验证压缩文件    if [[ "$file" == *.gz ]]; then        if ! gzip -t "$file" 2>/dev/null; then            log "错误: 压缩文件损坏 - $file"            return 1        fi    fi        log "备份文件验证通过: $file"    return 0}# 执行验证verify_backup "$BACKUP_FILE"exit $?

物理备份:XtraBackup实战

完整备份与恢复:

# 安装XtraBackup# Ubuntu/Debianwget https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.debsudo dpkg -i percona-release_latest.$(lsb_release -sc)_all.debsudo apt-get updatesudo apt-get install percona-xtrabackup-80# 完整备份xtrabackup --backup --user=backup_user --password='secure_password' --target-dir=/backup/full_$(date +%Y%m%d_%H%M%S)# 准备备份(应用日志)xtrabackup --prepare --target-dir=/backup/full_20231201_120000# 恢复备份systemctl stop mysqlmv /var/lib/mysql /var/lib/mysql_oldxtrabackup --copy-back --target-dir=/backup/full_20231201_120000chown -R mysql:mysql /var/lib/mysqlsystemctl start mysql

增量备份策略:

#!/bin/bash# incremental_backup.shBASE_DIR="/backup"FULL_BACKUP_DIR="$BASE_DIR/full_$(date +%Y%m%d)"INCREMENTAL_DIR="$BASE_DIR/inc_$(date +%Y%m%d_%H%M%S)"BACKUP_USER="backup_user"BACKUP_PASSWORD="secure_password"LOG_FILE="/var/log/mysql/xtrabackup.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}# 检查基础备份是否存在find_base_backup() {    find $BASE_DIR -name "full_*" -type d | sort -r | head -1}perform_full_backup() {    log "开始完整备份"    xtrabackup --backup --user=$BACKUP_USER --password=$BACKUP_PASSWORD --target-dir=$FULL_BACKUP_DIR    if [ $? -eq 0 ]; then        log "完整备份完成: $FULL_BACKUP_DIR"        echo $FULL_BACKUP_DIR > $BASE_DIR/latest_full_backup    else        log "完整备份失败"        exit 1    fi}perform_incremental_backup() {    local base_dir=$1    log "开始增量备份,基于: $base_dir"        xtrabackup --backup --user=$BACKUP_USER --password=$BACKUP_PASSWORD \        --target-dir=$INCREMENTAL_DIR \        --incremental-basedir=$base_dir        if [ $? -eq 0 ]; then        log "增量备份完成: $INCREMENTAL_DIR"    else        log "增量备份失败"        exit 1    fi}# 主逻辑BASE_BACKUP=$(find_base_backup)if [ -z "$BASE_BACKUP" ] || [ $(find $BASE_BACKUP -name "xtrabackup_checkpoints" -mtime +7 | wc -l) -gt 0 ]; then    # 没有基础备份或基础备份超过7天,执行完整备份    perform_full_backupelse    # 执行增量备份    perform_incremental_backup $BASE_BACKUPfi

备份恢复演练:

#!/bin/bash# disaster_recovery_drill.shRECOVERY_DIR="/recovery"BACKUP_SOURCE="/backup"MYSQL_DATA_DIR="/var/lib/mysql"LOG_FILE="/var/log/mysql/recovery_drill.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}prepare_recovery_environment() {    log "准备恢复环境"        # 停止MySQL服务    systemctl stop mysql        # 备份当前数据    mv $MYSQL_DATA_DIR ${MYSQL_DATA_DIR}_backup_$(date +%Y%m%d_%H%M%S)        # 创建恢复目录    mkdir -p $RECOVERY_DIR}restore_from_backup() {    local backup_dir=$1        log "从备份恢复: $backup_dir"        # 准备备份    xtrabackup --prepare --apply-log-only --target-dir=$backup_dir        # 恢复备份    xtrabackup --copy-back --target-dir=$backup_dir        # 设置权限    chown -R mysql:mysql $MYSQL_DATA_DIR}verify_recovery() {    log "验证恢复结果"        # 启动MySQL    systemctl start mysql        # 等待服务启动    sleep 30        # 基础验证    if mysql -u root -p'password' -e "SELECT 1;" > /dev/null 2>&1; then        log "MySQL服务启动成功"                # 验证关键表        local table_count=$(mysql -u root -p'password' -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys');")        log "发现 $table_count 个用户表"                # 验证数据完整性        mysql -u root -p'password' -e "CHECK TABLE company.employees EXTENDED;" >> $LOG_FILE                return 0    else        log "MySQL服务启动失败"        return 1    fi}# 执行恢复演练prepare_recovery_environment# 查找最新的完整备份LATEST_FULL_BACKUP=$(find $BACKUP_SOURCE -name "full_*" -type d | sort -r | head -1)if [ -n "$LATEST_FULL_BACKUP" ]; then    restore_from_backup $LATEST_FULL_BACKUP    verify_recoveryelse    log "错误: 未找到完整备份"    exit 1fi

增量备份与差异备份

二进制日志备份:

-- 启用二进制日志-- 在my.cnf中配置/*[mysqld]log_bin = /var/lib/mysql/mysql-binexpire_logs_days = 7max_binlog_size = 100M*/-- 查看二进制日志状态SHOW BINARY LOGS;/*+------------------+-----------+| Log_name         | File_size |+------------------+-----------+| mysql-bin.000001 |       194 || mysql-bin.000002 |       456 || mysql-bin.000003 |       123 |+------------------+-----------+*/-- 刷新日志(创建新的二进制日志文件)FLUSH BINARY LOGS;-- 查看当前正在使用的二进制日志SHOW MASTER STATUS;

自动化二进制日志备份:

#!/bin/bash# binlog_backup.shMYSQL_USER="backup_user"MYSQL_PASSWORD="secure_password"BACKUP_DIR="/backup/binlog"LOG_FILE="/var/log/mysql/binlog_backup.log"RETENTION_DAYS=7log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}backup_binlog() {    log "开始二进制日志备份"        # 获取当前二进制日志文件    CURRENT_BINLOG=$(mysql -u $MYSQL_USER -p$MYSQL_PASSWORD -N -e "SHOW MASTER STATUS" | awk '{print $1}')        # 备份所有未备份的二进制日志    for BINLOG in $(mysql -u $MYSQL_USER -p$MYSQL_PASSWORD -N -e "SHOW BINARY LOGS" | awk '{print $1}' | grep -v "$CURRENT_BINLOG"); do        if [ ! -f "$BACKUP_DIR/$BINLOG" ]; then            log "备份二进制日志: $BINLOG"            cp /var/lib/mysql/$BINLOG $BACKUP_DIR/                        # 验证备份            if cmp /var/lib/mysql/$BINLOG $BACKUP_DIR/$BINLOG; then                log "备份验证成功: $BINLOG"            else                log "备份验证失败: $BINLOG"            fi        fi    done}purge_old_backups() {    log "清理过期备份(保留 $RETENTION_DAYS 天)"    find $BACKUP_DIR -name "mysql-bin.*" -mtime +$RETENTION_DAYS -delete}# 创建备份目录mkdir -p $BACKUP_DIR# 执行备份backup_binlogpurge_old_backupslog "二进制日志备份完成"

备份压缩与加密

加密备份方案:

#!/bin/bash# encrypted_backup.shBACKUP_DIR="/backup/encrypted"MYSQL_USER="backup_user"MYSQL_PASSWORD="secure_password"ENCRYPTION_KEY="/etc/mysql/backup.key"LOG_FILE="/var/log/mysql/encrypted_backup.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}generate_encryption_key() {    if [ ! -f "$ENCRYPTION_KEY" ]; then        log "生成加密密钥"        openssl rand -base64 32 > $ENCRYPTION_KEY        chmod 600 $ENCRYPTION_KEY    fi}create_encrypted_backup() {    local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).xb.enc"        log "创建加密备份: $backup_file"        # 使用XtraBackup创建备份并立即加密    xtrabackup --backup --user=$MYSQL_USER --password=$MYSQL_PASSWORD --stream=xbstream | \    openssl enc -aes-256-cbc -salt -pass file:$ENCRYPTION_KEY -out $backup_file        if [ $? -eq 0 ]; then        log "加密备份创建成功: $backup_file"    else        log "加密备份创建失败"        exit 1    fi}verify_encrypted_backup() {    local backup_file=$1        log "验证加密备份: $backup_file"        # 尝试解密备份头信息    if openssl enc -aes-256-cbc -d -pass file:$ENCRYPTION_KEY -in $backup_file | head -c 100 | strings | grep -q "MySQL"; then        log "加密备份验证成功"        return 0    else        log "加密备份验证失败"        return 1    fi}# 主逻辑generate_encryption_keymkdir -p $BACKUP_DIRcreate_encrypted_backup# 验证最新的备份LATEST_BACKUP=$(ls -t $BACKUP_DIR/*.enc | head -1)if [ -n "$LATEST_BACKUP" ]; then    verify_encrypted_backup $LATEST_BACKUPfi

压缩备份优化:

#!/bin/bash# compressed_backup.shBACKUP_DIR="/backup/compressed"MYSQL_USER="backup_user"MYSQL_PASSWORD="secure_password"COMPRESSION_LEVEL=6  # 1-9,数字越大压缩率越高但速度越慢LOG_FILE="/var/log/mysql/compressed_backup.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}create_compressed_backup() {    local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql.gz"        log "创建压缩备份 (级别: $COMPRESSION_LEVEL)"        # 使用mysqldump和gzip创建压缩备份    mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --all-databases --single-transaction --routines --triggers --events | \    gzip -$COMPRESSION_LEVEL > $backup_file        local backup_size=$(du -h $backup_file | cut -f1)    log "压缩备份完成: $backup_file (大小: $backup_size)"}create_parallel_compressed_backup() {    local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql.gz"        log "创建并行压缩备份"        # 使用pigz进行并行压缩(如果可用)    if command -v pigz >/dev/null 2>&1; then        mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --all-databases --single-transaction | \        pigz -p 4 -$COMPRESSION_LEVEL > $backup_file    else        mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --all-databases --single-transaction | \        gzip -$COMPRESSION_LEVEL > $backup_file    fi        local backup_size=$(du -h $backup_file | cut -f1)    log "并行压缩备份完成: $backup_file (大小: $backup_size)"}# 创建备份目录mkdir -p $BACKUP_DIR# 根据系统资源选择备份方式if [ $(nproc) -gt 2 ]; then    create_parallel_compressed_backupelse    create_compressed_backupfi

云环境备份方案

AWS S3备份方案:

#!/bin/bash# s3_backup.shBACKUP_DIR="/backup/s3_upload"S3_BUCKET="my-company-mysql-backups"S3_PATH="mysql/$(hostname)"MYSQL_USER="backup_user"MYSQL_PASSWORD="secure_password"RETENTION_DAYS=30LOG_FILE="/var/log/mysql/s3_backup.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}create_backup() {    local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql.gz"        log "创建备份文件: $backup_file"        mysqldump -u $MYSQL_USER -p$MYSQL_PASSWORD --all-databases --single-transaction --routines --triggers --events | \    gzip > $backup_file        echo $backup_file}upload_to_s3() {    local backup_file=$1    local s3_key="$S3_PATH/$(basename $backup_file)"        log "上传到S3: s3://$S3_BUCKET/$s3_key"        if aws s3 cp $backup_file s3://$S3_BUCKET/$s3_key; then        log "S3上传成功"        return 0    else        log "S3上传失败"        return 1    fi}cleanup_old_backups() {    log "清理本地过期备份"    find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete        log "清理S3过期备份"    aws s3 ls s3://$S3_BUCKET/$S3_PATH/ | while read line; do        create_date=$(echo $line | awk '{print $1" "$2}')        create_date_epoch=$(date -d "$create_date" +%s)        retention_epoch=$(date -d "$RETENTION_DAYS days ago" +%s)                if [ $create_date_epoch -lt $retention_epoch ]; then            file_name=$(echo $line | awk '{print $4}')            aws s3 rm s3://$S3_BUCKET/$S3_PATH/$file_name            log "删除过期S3备份: $file_name"        fi    done}verify_s3_backup() {    local backup_file=$1    local s3_key="$S3_PATH/$(basename $backup_file)"        log "验证S3备份完整性"        # 下载备份文件    local temp_file="/tmp/verify_$(basename $backup_file)"    aws s3 cp s3://$S3_BUCKET/$s3_key $temp_file        # 比较本地和S3的文件    if cmp $backup_file $temp_file; then        log "S3备份验证成功"        rm $temp_file        return 0    else        log "S3备份验证失败"        rm $temp_file        return 1    fi}# 主逻辑mkdir -p $BACKUP_DIRBACKUP_FILE=$(create_backup)if [ -n "$BACKUP_FILE" ]; then    if upload_to_s3 $BACKUP_FILE; then        verify_s3_backup $BACKUP_FILE    fificleanup_old_backups

2. 数据恢复与灾难恢复

基于时间点的恢复(PITR)

PITR恢复流程:

#!/bin/bash# pitr_recovery.shRESTORE_TIME="2023-12-01 14:30:00"BACKUP_DIR="/backup"BINLOG_DIR="/var/lib/mysql"RECOVERY_DIR="/recovery"MYSQL_DATA_DIR="/var/lib/mysql"LOG_FILE="/var/log/mysql/pitr_recovery.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}find_relevant_backup() {    log "查找适用于时间点 $RESTORE_TIME 的备份"        # 查找在恢复时间之前的最新完整备份    for BACKUP in $(ls -t $BACKUP_DIR/full_* 2>/dev/null); do        local backup_time=$(stat -c %y $BACKUP/xtrabackup_info | cut -d' ' -f1,2 | cut -d'.' -f1)        local backup_epoch=$(date -d "$backup_time" +%s)        local restore_epoch=$(date -d "$RESTORE_TIME" +%s)                if [ $backup_epoch -le $restore_epoch ]; then            echo $BACKUP            return 0        fi    done        log "错误: 未找到合适的完整备份"    exit 1}extract_binlog_events() {    local start_time=$1    local stop_time=$2    local output_file=$3        log "提取二进制日志事件: $start_time 到 $stop_time"        # 查找包含时间范围的二进制日志文件    for BINLOG in $(ls -tr $BINLOG_DIR/mysql-bin.* 2>/dev/null | grep -v '.index'); do        local first_event_time=$(mysqlbinlog $BINLOG | grep -m1 "end_log_pos" | awk '{print $1, $2}' | tr -d '#')        local last_event_time=$(mysqlbinlog $BINLOG | tail -10 | grep "end_log_pos" | tail -1 | awk '{print $1, $2}' | tr -d '#')                if [ -n "$first_event_time" ] && [ -n "$last_event_time" ]; then            local first_epoch=$(date -d "$first_event_time" +%s 2>/dev/null || echo 0)            local last_epoch=$(date -d "$last_event_time" +%s 2>/dev/null || echo 0)            local start_epoch=$(date -d "$start_time" +%s)            local stop_epoch=$(date -d "$stop_time" +%s)                        if [ $last_epoch -ge $start_epoch ] && [ $first_epoch -le $stop_epoch ]; then                log "处理二进制日志: $BINLOG"                mysqlbinlog --start-datetime="$start_time" --stop-datetime="$stop_time" $BINLOG >> $output_file            fi        fi    done}perform_pitr_recovery() {    local base_backup=$1        log "执行时间点恢复"        # 准备恢复环境    systemctl stop mysql    mv $MYSQL_DATA_DIR ${MYSQL_DATA_DIR}_backup_$(date +%Y%m%d_%H%M%S)        # 恢复基础备份    xtrabackup --copy-back --target-dir=$base_backup    chown -R mysql:mysql $MYSQL_DATA_DIR        # 启动MySQL到恢复模式    systemctl start mysql        # 获取备份时间    local backup_time=$(stat -c %y $base_backup/xtrabackup_info | cut -d' ' -f1,2 | cut -d'.' -f1)        # 提取和应用二进制日志    local binlog_events="/tmp/binlog_events.sql"    echo "" > $binlog_events        extract_binlog_events "$backup_time" "$RESTORE_TIME" $binlog_events        # 应用二进制日志事件    if [ -s $binlog_events ]; then        log "应用二进制日志事件"        mysql -u root -p'password' < $binlog_events    else        log "没有需要应用的二进制日志事件"    fi        log "时间点恢复完成"}# 主逻辑BASE_BACKUP=$(find_relevant_backup)perform_pitr_recovery $BASE_BACKUP

误操作数据恢复方案

Flashback工具使用:

-- 安装mysqlbinlog_flashback工具-- 使用my2sql或binlog2sql进行闪回-- 示例:恢复误删除的数据# 使用binlog2sql解析二进制日志python binlog2sql/binlog2sql.py -h127.0.0.1 -P3306 -uroot -p'password' -dcompany -temployees --start-file='mysql-bin.000001' --start-pos=4 --stop-pos=1000 -B-- 输出闪回SQL/*INSERT INTO `company`.`employees`(`create_time`, `phone`, `name`, `id`, `email`) VALUES ('2023-01-01 10:00:00', '13800138000', '张三', 1, 'zhangsan@company.com'); INSERT INTO `company`.`employees`(`create_time`, `phone`, `name`, `id`, `email`) VALUES ('2023-01-02 11:00:00', '13900139000', '李四', 2, 'lisi@company.com');*/

基于备份的误操作恢复:

#!/bin/bash# point_in_time_restore.shDB_NAME="company"TABLE_NAME="employees"BACKUP_DIR="/backup"RESTORE_TIME="2023-12-01 10:00:00"  # 误操作之前的时间LOG_FILE="/var/log/mysql/point_restore.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}create_restore_database() {    local restore_db="${DB_NAME}_restore_$(date +%Y%m%d_%H%M%S)"        log "创建恢复数据库: $restore_db"        mysql -u root -p'password' -e "CREATE DATABASE $restore_db;"    echo $restore_db}restore_table_to_point() {    local restore_db=$1    local backup_file=$(find $BACKUP_DIR -name "*${DB_NAME}*" -type f | sort -r | head -1)        if [ -z "$backup_file" ]; then        log "错误: 未找到备份文件"        exit 1    fi        log "从备份恢复表结构"        # 提取表结构    if [[ $backup_file == *.sql.gz ]]; then        gunzip -c $backup_file | sed -n "/^-- Table structure for table \`$TABLE_NAME\`/,/^-- Table structure/p" | \        mysql -u root -p'password' $restore_db    else        sed -n "/^-- Table structure for table \`$TABLE_NAME\`/,/^-- Table structure/p" $backup_file | \        mysql -u root -p'password' $restore_db    fi        # 应用二进制日志到指定时间点    log "应用二进制日志到时间点: $RESTORE_TIME"        local binlog_events="/tmp/binlog_events_$restore_db.sql"    mysqlbinlog --database=$DB_NAME --stop-datetime="$RESTORE_TIME" /var/lib/mysql/mysql-bin.* | \    sed -n "/^### INSERT INTO \`$DB_NAME\`.\`$TABLE_NAME\`/,/^### INSERT INTO/p" | \    sed 's/^### //' > $binlog_events        mysql -u root -p'password' $restore_db < $binlog_events    rm $binlog_events        log "表恢复完成: $restore_db.$TABLE_NAME"}compare_and_restore() {    local restore_db=$1        log "比较并恢复数据"        # 生成恢复SQL    local restore_sql="/tmp/restore_data.sql"        cat > $restore_sql << EOF-- 插入缺失的记录INSERT INTO $DB_NAME.$TABLE_NAME SELECT * FROM $restore_db.$TABLE_NAME rWHERE NOT EXISTS (    SELECT 1 FROM $DB_NAME.$TABLE_NAME c     WHERE c.id = r.id);-- 更新被修改的记录UPDATE $DB_NAME.$TABLE_NAME cJOIN $restore_db.$TABLE_NAME r ON c.id = r.idSET     c.name = r.name,    c.email = r.email,    c.phone = r.phone,    c.updated_at = NOW()WHERE c.name != r.name    OR c.email != r.email    OR c.phone != r.phone;EOF    mysql -u root -p'password' < $restore_sql    rm $restore_sql        log "数据恢复完成"}# 主逻辑RESTORE_DB=$(create_restore_database)restore_table_to_point $RESTORE_DBcompare_and_restore $RESTORE_DB# 清理恢复数据库mysql -u root -p'password' -e "DROP DATABASE $RESTORE_DB;"

主从切换与数据重建

计划内主从切换:

#!/bin/bash# planned_failover.shCURRENT_MASTER="192.168.1.100"NEW_MASTER="192.168.1.101"MYSQL_USER="repl_user"MYSQL_PASSWORD="repl_password"LOG_FILE="/var/log/mysql/planned_failover.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}check_replication_health() {    log "检查复制健康状况"        # 检查主库    local master_status=$(mysql -h $CURRENT_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SHOW MASTER STATUS\G")    if [ $? -ne 0 ]; then        log "错误: 无法连接主库 $CURRENT_MASTER"        exit 1    fi        # 检查从库延迟    local slave_status=$(mysql -h $NEW_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SHOW SLAVE STATUS\G")    local seconds_behind=$(echo "$slave_status" | grep "Seconds_Behind_Master" | awk '{print $2}')        if [ "$seconds_behind" != "0" ]; then        log "警告: 从库有延迟 ($seconds_behind 秒)"        read -p "是否继续? (y/n): " -n 1 -r        echo        if [[ ! $REPLY =~ ^[Yy]$ ]]; then            exit 1        fi    fi        log "复制健康状况良好"}perform_failover() {    log "开始主从切换"        # 1. 设置原主库为只读    log "设置原主库为只读模式"    mysql -h $CURRENT_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SET GLOBAL read_only = ON;"        # 2. 等待从库应用所有日志    log "等待从库应用所有日志"    while true; do        local slave_status=$(mysql -h $NEW_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SHOW SLAVE STATUS\G")        local seconds_behind=$(echo "$slave_status" | grep "Seconds_Behind_Master" | awk '{print $2}')        local io_running=$(echo "$slave_status" | grep "Slave_IO_Running" | awk '{print $2}')        local sql_running=$(echo "$slave_status" | grep "Slave_SQL_Running" | awk '{print $2}')                if [ "$seconds_behind" = "0" ] && [ "$io_running" = "Yes" ] && [ "$sql_running" = "Yes" ]; then            break        fi        sleep 1    done        # 3. 停止从库复制    log "停止新主库的复制"    mysql -h $NEW_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "STOP SLAVE;"        # 4. 记录新主库的二进制日志位置    local new_master_status=$(mysql -h $NEW_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SHOW MASTER STATUS\G")    local new_master_file=$(echo "$new_master_status" | grep "File" | awk '{print $2}')    local new_master_position=$(echo "$new_master_status" | grep "Position" | awk '{print $2}')        # 5. 设置新主库为可写    log "设置新主库为可写模式"    mysql -h $NEW_MASTER -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SET GLOBAL read_only = OFF;"        # 6. 配置其他从库指向新主库    log "重新配置其他从库"    # 这里可以添加其他从库的重新配置逻辑        log "主从切换完成"    log "新主库二进制日志位置: $new_master_file $new_master_position"}# 主逻辑check_replication_healthperform_failover

灾难恢复演练

完整灾难恢复演练:

#!/bin/bash# disaster_recovery_test.shDR_SITE_MYSQL="192.168.2.100"BACKUP_SERVER="192.168.3.100"MYSQL_USER="dr_user"MYSQL_PASSWORD="dr_password"LOG_FILE="/var/log/mysql/dr_test.log"log() {    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOG_FILE}verify_dr_environment() {    log "验证灾备环境"        # 检查网络连通性    if ! ping -c 3 $DR_SITE_MYSQL > /dev/null 2>&1; then        log "错误: 无法连接到灾备MySQL服务器"        return 1    fi        # 检查MySQL服务    if ! mysql -h $DR_SITE_MYSQL -u $MYSQL_USER -p$MYSQL_PASSWORD -e "SELECT 1;" > /dev/null 2>&1; then        log "错误: 灾备MySQL服务不可用"        return 1    fi        log "灾备环境验证通过"    return 0}restore_to_dr_site() {    log "开始恢复到灾备站点"        # 1. 停止灾备站点MySQL服务    log "停止灾备站点MySQL服务"    ssh root@$DR_SITE_MYSQL "systemctl stop mysql"        # 2. 备份当前数据    log "备份灾备站点当前数据"    ssh root@$DR_SITE_MYSQL "mv /var/lib/mysql /var/lib/mysql_backup_$(date +%Y%m%d_%H%M%S)"        # 3. 从备份服务器获取最新备份    log "获取最新备份"    local latest_backup=$(ssh root@$BACKUP_SERVER "ls -t /backup/full_* | head -1")        if [ -z "$latest_backup" ]; then        log "错误: 未找到备份文件"        return 1    fi        # 4. 传输备份到灾备站点    log "传输备份文件"    scp -r root@$BACKUP_SERVER:$latest_backup /tmp/dr_restore/        # 5. 准备备份    log "准备备份"    ssh root@$DR_SITE_MYSQL "xtrabackup --prepare --target-dir=/tmp/dr_restore/"        # 6. 恢复备份    log "恢复备份"    ssh root@$DR_SITE_MYSQL "xtrabackup --copy-back --target-dir=/tmp/dr_restore/"        # 7. 设置权限并启动服务    log "启动MySQL服务"    ssh root@$DR_SITE_MYSQL "chown -R mysql:mysql /var/lib/mysql && systemctl start mysql"        log "灾备恢复完成"}verify_dr_data() {    log "验证灾备数据"        # 检查数据库列表    local db_count=$(mysql -h $DR_SITE_MYSQL -u $MYSQL_USER -p$MYSQL_PASSWORD -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys');")        if [ "$db_count" -gt 0 ]; then        log "数据验证成功: 发现 $db_count 个用户表"        return 0    else        log "数据验证失败: 未发现用户表"        return 1    fi}perform_failover_test() {    log "执行故障切换测试"        # 模拟应用连接灾备数据库    local test_result=$(mysql -h $DR_SITE_MYSQL -u $MYSQL_USER -p$MYSQL_PASSWORD -e "CREATE DATABASE dr_test; USE dr_test; CREATE TABLE test_table (id INT); INSERT INTO test_table VALUES (1); SELECT * FROM test_table;" 2>&1)        if echo "$test_result" | grep -q "1"; then        log "故障切换测试成功"                # 清理测试数据        mysql -h $DR_SITE_MYSQL -u $MYSQL_USER -p$MYSQL_PASSWORD -e "DROP DATABASE dr_test;"                return 0    else        log "故障切换测试失败"        return 1    fi}# 主逻辑if verify_dr_environment; then    restore_to_dr_site    if verify_dr_data; then        perform_failover_test    fifi

备份恢复监控告警

备份状态监控:

-- 创建备份监控表CREATE TABLE backup_monitor (    id BIGINT AUTO_INCREMENT PRIMARY KEY,    backup_type ENUM('FULL', 'INCREMENTAL', 'BINLOG') NOT NULL,    backup_file VARCHAR(500) NOT NULL,    backup_size BIGINT,    start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    end_time TIMESTAMP NULL,    status ENUM('RUNNING', 'COMPLETED', 'FAILED') DEFAULT 'RUNNING',    error_message TEXT,    checksum VARCHAR(64));-- 创建备份告警表CREATE TABLE backup_alerts (    id BIGINT AUTO_INCREMENT PRIMARY KEY,    alert_type VARCHAR(50) NOT NULL,    alert_message TEXT NOT NULL,    severity ENUM('LOW', 'MEDIUM', 'HIGH', 'CRITICAL') NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    resolved_at TIMESTAMP NULL,    resolved_by VARCHAR(100));-- 备份状态检查存储过程DELIMITER //CREATE PROCEDURE CheckBackupStatus()BEGIN    DECLARE last_full_backup TIMESTAMP;    DECLARE backup_age_hours INT;    DECLARE failed_backups INT;        -- 检查最近完整备份的时间    SELECT MAX(start_time) INTO last_full_backup    FROM backup_monitor    WHERE backup_type = 'FULL' AND status = 'COMPLETED';        SET backup_age_hours = TIMESTAMPDIFF(HOUR, last_full_backup, NOW());        -- 如果超过24小时没有完整备份,发出告警    IF backup_age_hours > 24 THEN        INSERT INTO backup_alerts (alert_type, alert_message, severity)        VALUES ('BACKUP_MISSING',                 CONCAT('超过', backup_age_hours, '小时没有完整备份'),                 'HIGH');    END IF;        -- 检查失败的备份    SELECT COUNT(*) INTO failed_backups    FROM backup_monitor    WHERE status = 'FAILED' AND start_time > NOW() - INTERVAL 24 HOUR;        IF failed_backups > 0 THEN        INSERT INTO backup_alerts (alert_type, alert_message, severity)        VALUES ('BACKUP_FAILED',                 CONCAT('过去24小时有', failed_backups, '个备份失败'),                 'HIGH');    END IF;    END //DELIMITER ;

3. 安全与权限管理

用户权限体系设计

最小权限原则实施:

-- 创建应用用户(遵循最小权限原则)CREATE USER 'app_readonly'@'192.168.1.%' IDENTIFIED BY 'secure_password_123';GRANT SELECT ON company.* TO 'app_readonly'@'192.168.1.%';CREATE USER 'app_readwrite'@'192.168.1.%' IDENTIFIED BY 'secure_password_456';GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO 'app_readwrite'@'192.168.1.%';CREATE USER 'app_report'@'192.168.1.%' IDENTIFIED BY 'secure_password_789';GRANT SELECT ON company.employees TO 'app_report'@'192.168.1.%';GRANT SELECT ON company.departments TO 'app_report'@'192.168.1.%';-- 创建管理用户CREATE USER 'db_admin'@'localhost' IDENTIFIED BY 'admin_secure_password';GRANT ALL PRIVILEGES ON *.* TO 'db_admin'@'localhost' WITH GRANT OPTION;-- 创建备份用户CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'backup_secure_password';GRANT SELECT, RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'backup_user'@'localhost';-- 查看用户权限SHOW GRANTS FOR 'app_readonly'@'192.168.1.%';

数据库权限审计:

-- 创建权限审计表CREATE TABLE privilege_audit (    id BIGINT AUTO_INCREMENT PRIMARY KEY,    username VARCHAR(100) NOT NULL,    host_pattern VARCHAR(100) NOT NULL,    database_name VARCHAR(100),    table_name VARCHAR(100),    privilege_type VARCHAR(50) NOT NULL,    granted_by VARCHAR(100),    granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    is_revoked BOOLEAN DEFAULT FALSE,    revoked_at TIMESTAMP NULL,    revoked_by VARCHAR(100));-- 权限审计存储过程DELIMITER //CREATE PROCEDURE AuditUserPrivileges()BEGIN    DECLARE done INT DEFAULT 0;    DECLARE v_user, v_host, v_db, v_table, v_privilege VARCHAR(100);    DECLARE cur CURSOR FOR         SELECT User, Host, Db, Table_name, Privilege         FROM information_schema.table_privileges;    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;        OPEN cur;        read_loop: LOOP        FETCH cur INTO v_user, v_host, v_db, v_table, v_privilege;        IF done THEN            LEAVE read_loop;        END IF;                -- 检查权限是否已经记录        IF NOT EXISTS (            SELECT 1 FROM privilege_audit             WHERE username = v_user               AND host_pattern = v_host               AND database_name = v_db               AND table_name = v_table               AND privilege_type = v_privilege               AND is_revoked = FALSE        ) THEN            -- 记录新权限            INSERT INTO privilege_audit (username, host_pattern, database_name, table_name, privilege_type)            VALUES (v_user, v_host, v_db, v_table, v_privilege);        END IF;    END LOOP;        CLOSE cur;        -- 标记已撤销的权限    UPDATE privilege_audit pa    LEFT JOIN information_schema.table_privileges tp         ON pa.username = tp.User         AND pa.host_pattern = tp.Host         AND pa.database_name = tp.Db         AND pa.table_name = tp.Table_name         AND pa.privilege_type = tp.Privilege    SET pa.is_revoked = TRUE,        pa.revoked_at = CURRENT_TIMESTAMP    WHERE pa.is_revoked = FALSE      AND tp.User IS NULL;    END //DELIMITER ;

角色管理与权限继承

MySQL 8.0角色管理:

-- 创建角色CREATE ROLE read_only_role;CREATE ROLE read_write_role;CREATE ROLE dba_role;-- 为角色分配权限GRANT SELECT ON company.* TO read_only_role;GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO read_write_role;GRANT ALL PRIVILEGES ON *.* TO dba_role;-- 创建用户并分配角色CREATE USER 'report_user'@'%' IDENTIFIED BY 'report_password';CREATE USER 'app_user'@'%' IDENTIFIED BY 'app_password';CREATE USER 'admin_user'@'localhost' IDENTIFIED BY 'admin_password';-- 分配角色给用户GRANT read_only_role TO 'report_user'@'%';GRANT read_write_role TO 'app_user'@'%';GRANT dba_role TO 'admin_user'@'localhost';-- 设置默认角色SET DEFAULT ROLE read_only_role TO 'report_user'@'%';SET DEFAULT ROLE read_write_role TO 'app_user'@'%';SET DEFAULT ROLE dba_role TO 'admin_user'@'localhost';-- 激活角色SET ROLE ALL;-- 查看角色权限SHOW GRANTS FOR 'report_user'@'%' USING read_only_role;-- 创建层次化角色CREATE ROLE junior_dba;CREATE ROLE senior_dba;GRANT junior_dba TO senior_dba;GRANT SELECT, INSERT, UPDATE, DELETE ON mysql.* TO junior_dba;GRANT ALL PRIVILEGES ON *.* TO senior_dba;

动态权限管理:

-- 创建存储过程管理用户权限DELIMITER //CREATE PROCEDURE ManageUserAccess(    IN p_username VARCHAR(100),    IN p_host_pattern VARCHAR(100),    IN p_database_name VARCHAR(100),    IN p_action ENUM('GRANT_READ', 'GRANT_WRITE', 'REVOKE_ACCESS'))BEGIN    DECLARE user_exists INT;        -- 检查用户是否存在    SELECT COUNT(*) INTO user_exists    FROM mysql.user     WHERE User = p_username AND Host = p_host_pattern;        IF user_exists = 0 THEN        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '用户不存在';    END IF;        CASE p_action        WHEN 'GRANT_READ' THEN            SET @grant_sql = CONCAT('GRANT SELECT ON ', p_database_name, '.* TO ''', p_username, '''@''', p_host_pattern, '''');            PREPARE stmt FROM @grant_sql;            EXECUTE stmt;            DEALLOCATE PREPARE stmt;                        -- 记录权限变更            INSERT INTO privilege_audit (username, host_pattern, database_name, privilege_type)            VALUES (p_username, p_host_pattern, p_database_name, 'SELECT');                    WHEN 'GRANT_WRITE' THEN            SET @grant_sql = CONCAT('GRANT SELECT, INSERT, UPDATE, DELETE ON ', p_database_name, '.* TO ''', p_username, '''@''', p_host_pattern, '''');            PREPARE stmt FROM @grant_sql;            EXECUTE stmt;            DEALLOCATE PREPARE stmt;                        INSERT INTO privilege_audit (username, host_pattern, database_name, privilege_type)            VALUES (p_username, p_host_pattern, p_database_name, 'READ_WRITE');                    WHEN 'REVOKE_ACCESS' THEN            SET @revoke_sql = CONCAT('REVOKE ALL PRIVILEGES ON ', p_database_name, '.* FROM ''', p_username, '''@''', p_host_pattern, '''');            PREPARE stmt FROM @revoke_sql;            EXECUTE stmt;            DEALLOCATE PREPARE stmt;                        UPDATE privilege_audit             SET is_revoked = TRUE, revoked_at = NOW()            WHERE username = p_username               AND host_pattern = p_host_pattern               AND database_name = p_database_name              AND is_revoked = FALSE;    END CASE;    END //DELIMITER ;

数据加密:透明加密与列加密

InnoDB表空间加密:

-- 安装密钥环组件(MySQL 8.0)INSTALL COMPONENT "file://component_keyring_file";SET GLOBAL keyring_file_data = '/var/lib/mysql-keyring/keyring';-- 创建加密表空间CREATE TABLESPACE encrypted_ts ADD DATAFILE 'encrypted_ts.ibd' ENGINE=InnoDBENCRYPTION='Y';-- 在加密表空间中创建表CREATE TABLE sensitive_data (    id INT PRIMARY KEY,    secret_data VARCHAR(500)) TABLESPACE encrypted_ts;-- 加密现有表ALTER TABLE existing_sensitive_table ENCRYPTION='Y';-- 查看加密状态SELECT     TABLE_SCHEMA,    TABLE_NAME,    CREATE_OPTIONSFROM information_schema.TABLES WHERE CREATE_OPTIONS LIKE '%ENCRYPTION%';

列级加密:

-- 创建加密函数DELIMITER //CREATE FUNCTION aes_encrypt(data TEXT, key_str VARCHAR(255))RETURNS VARBINARY(500)DETERMINISTICBEGIN    RETURN AES_ENCRYPT(data, key_str);END //CREATE FUNCTION aes_decrypt(encrypted_data VARBINARY(500), key_str VARCHAR(255))RETURNS TEXTDETERMINISTICBEGIN    RETURN AES_DECRYPT(encrypted_data, key_str);END //DELIMITER ;-- 创建存储加密数据的表CREATE TABLE user_secrets (    user_id INT PRIMARY KEY,    -- 加密存储的敏感数据    ssn VARBINARY(500),    credit_card VARBINARY(500),    medical_info VARBINARY(500),    -- 加密密钥(在实际应用中应该安全存储)    encryption_key VARCHAR(255) DEFAULT 'default_encryption_key');-- 插入加密数据INSERT INTO user_secrets (user_id, ssn, credit_card)VALUES (    1,    aes_encrypt('123-45-6789', 'user1_key'),    aes_encrypt('4111111111111111', 'user1_key'));-- 查询解密数据SELECT     user_id,    aes_decrypt(ssn, 'user1_key') as decrypted_ssn,    aes_decrypt(credit_card, 'user1_key') as decrypted_credit_cardFROM user_secrets WHERE user_id = 1;

密钥管理策略:

-- 创建密钥管理表CREATE TABLE encryption_keys (    key_id VARCHAR(100) PRIMARY KEY,    key_value VARBINARY(500) NOT NULL,    key_type ENUM('COLUMN', 'TABLE', 'BACKUP') NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    created_by VARCHAR(100),    is_active BOOLEAN DEFAULT TRUE,    rotated_at TIMESTAMP NULL);-- 密钥轮换存储过程DELIMITER //CREATE PROCEDURE RotateEncryptionKey(    IN p_key_id VARCHAR(100),    IN p_new_key_value VARBINARY(500))BEGIN    DECLARE old_key_value VARBINARY(500);    DECLARE done INT DEFAULT 0;    DECLARE v_user_id INT;    DECLARE v_ssn, v_credit_card VARBINARY(500);        -- 获取旧密钥    SELECT key_value INTO old_key_value    FROM encryption_keys    WHERE key_id = p_key_id AND is_active = TRUE;        IF old_key_value IS NULL THEN        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '未找到活动的密钥';    END IF;        -- 使用游标处理所有需要重新加密的数据    DECLARE cur CURSOR FOR         SELECT user_id, ssn, credit_card         FROM user_secrets;    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;        OPEN cur;        read_loop: LOOP        FETCH cur INTO v_user_id, v_ssn, v_credit_card;        IF done THEN            LEAVE read_loop;        END IF;                -- 解密并使用新密钥重新加密        UPDATE user_secrets         SET ssn = aes_encrypt(aes_decrypt(v_ssn, old_key_value), p_new_key_value),            credit_card = aes_encrypt(aes_decrypt(v_credit_card, old_key_value), p_new_key_value)        WHERE user_id = v_user_id;    END LOOP;        CLOSE cur;        -- 停用旧密钥,激活新密钥    UPDATE encryption_keys SET is_active = FALSE, rotated_at = NOW() WHERE key_id = p_key_id;    INSERT INTO encryption_keys (key_id, key_value, key_type) VALUES (p_key_id, p_new_key_value, 'COLUMN');    END //DELIMITER ;

审计日志与安全监控

MySQL企业版审计:

-- 安装审计插件(企业版)INSTALL PLUGIN audit_log SONAME 'audit_log.so';-- 配置审计日志(在my.cnf中)/*[mysqld]audit_log_format=JSONaudit_log_file=/var/log/mysql/audit.logaudit_log_policy=ALLaudit_log_rotate_on_size=100000000audit_log_rotations=5*/-- 查看审计日志状态SHOW VARIABLES LIKE 'audit_log%';-- 查询审计日志SELECT     JSON_EXTRACT(audit_record, '$.timestamp') as timestamp,    JSON_EXTRACT(audit_record, '$.class') as event_class,    JSON_EXTRACT(audit_record, '$.event') as event_type,    JSON_EXTRACT(audit_record, '$.connection_id') as connection_id,    JSON_EXTRACT(audit_record, '$.user') as user,    JSON_EXTRACT(audit_record, '$.query') as queryFROM mysql.audit_log WHERE JSON_EXTRACT(audit_record, '$.query') IS NOT NULLORDER BY timestamp DESC LIMIT 10;

社区版审计方案:

-- 使用通用日志实现基础审计SET GLOBAL general_log = 1;SET GLOBAL log_output = 'TABLE';-- 创建自定义审计表CREATE TABLE custom_audit_log (    id BIGINT AUTO_INCREMENT PRIMARY KEY,    event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    user_host VARCHAR(200) NOT NULL,    thread_id BIGINT NOT NULL,    server_id INT NOT NULL,    command_type VARCHAR(64) NOT NULL,    argument TEXT NOT NULL,    client_ip VARCHAR(45),    database_name VARCHAR(100),    execution_time DECIMAL(10,6),    rows_affected INT);-- 审计触发器示例DELIMITER //CREATE TRIGGER audit_user_changesAFTER INSERT ON mysql.userFOR EACH ROWBEGIN    INSERT INTO custom_audit_log (user_host, thread_id, server_id, command_type, argument, client_ip)    VALUES (USER(), CONNECTION_ID(), @@server_id, 'CREATE_USER',             CONCAT('Created user: ', NEW.User, '@', NEW.Host),             SUBSTRING_INDEX(USER(), '@', -1));END //CREATE TRIGGER audit_privilege_changesAFTER INSERT ON mysql.dbFOR EACH ROWBEGIN    INSERT INTO custom_audit_log (user_host, thread_id, server_id, command_type, argument, database_name)    VALUES (USER(), CONNECTION_ID(), @@server_id, 'GRANT_PRIVILEGE',            CONCAT('Granted privileges on ', NEW.Db, ' to ', NEW.User),            NEW.Db);END //DELIMITER ;

安全监控仪表板:

-- 创建安全监控视图CREATE VIEW security_dashboard ASSELECT     'Failed Logins' as metric_name,    COUNT(*) as metric_value,    MAX(event_time) as last_occurrenceFROM custom_audit_logWHERE argument LIKE '%Access denied%'  AND event_time > NOW() - INTERVAL 1 HOURUNION ALLSELECT     'New Users Created' as metric_name,    COUNT(*) as metric_value,    MAX(event_time) as last_occurrenceFROM custom_audit_logWHERE command_type = 'CREATE_USER'  AND event_time > NOW() - INTERVAL 24 HOURUNION ALLSELECT     'Privilege Changes' as metric_name,    COUNT(*) as metric_value,    MAX(event_time) as last_occurrenceFROM custom_audit_logWHERE command_type IN ('GRANT_PRIVILEGE', 'REVOKE_PRIVILEGE')  AND event_time > NOW() - INTERVAL 24 HOURUNION ALLSELECT     'Sensitive Data Access' as metric_name,    COUNT(*) as metric_value,    MAX(event_time) as last_occurrenceFROM custom_audit_logWHERE argument LIKE '%user_secrets%'  AND event_time > NOW() - INTERVAL 1 HOUR;

SQL注入防护与安全开发

预处理语句使用:

-- 不安全的查询(容易SQL注入)SET @user_input = "1'; DROP TABLE users; --";SET @sql = CONCAT("SELECT * FROM users WHERE id = '", @user_input, "'");PREPARE stmt FROM @sql;EXECUTE stmt;-- 安全的预处理语句PREPARE safe_stmt FROM "SELECT * FROM users WHERE id = ?";SET @user_id = "1";EXECUTE safe_stmt USING @user_id;-- 存储过程参数化查询DELIMITER //CREATE PROCEDURE GetUserByEmail(IN p_email VARCHAR(255))BEGIN    -- 直接使用参数,避免拼接    SELECT * FROM users WHERE email = p_email;END //DELIMITER ;

输入验证函数:

DELIMITER //CREATE FUNCTION ValidateEmail(email VARCHAR(255))RETURNS BOOLEANDETERMINISTICBEGIN    -- 简单的邮箱格式验证    IF email REGEXP '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$' THEN        RETURN TRUE;    ELSE        RETURN FALSE;    END IF;END //CREATE FUNCTION SanitizeInput(input_text TEXT)RETURNS TEXTDETERMINISTICBEGIN    -- 移除潜在的SQL注入字符    SET input_text = REPLACE(input_text, "'", "''");    SET input_text = REPLACE(input_text, ";", "");    SET input_text = REPLACE(input_text, "--", "");    SET input_text = REPLACE(input_text, "/*", "");    SET input_text = REPLACE(input_text, "*/", "");        RETURN input_text;END //DELIMITER ;

安全开发规范检查:

-- 检查存储过程的安全问题SELECT     ROUTINE_NAME,    ROUTINE_DEFINITIONFROM information_schema.ROUTINESWHERE ROUTINE_DEFINITION LIKE '%CONCAT(%'   OR ROUTINE_DEFINITION LIKE '%EXECUTE%IMMEDIATE%'   OR ROUTINE_DEFINITION LIKE '%PREPARE%'   OR ROUTINE_DEFINITION LIKE '%sp_executesql%';-- 查找可能包含动态SQL的代码SELECT     TABLE_NAME,    COLUMN_NAMEFROM information_schema.COLUMNSWHERE TABLE_SCHEMA = 'your_database'  AND (COLUMN_NAME LIKE '%sql%' OR COLUMN_NAME LIKE '%query%')  AND TABLE_NAME NOT LIKE '%audit%';

总结

通过本篇的深入学习,我们掌握了MySQL备份恢复和安全管理的完整体系:

  1. 备份策略:逻辑备份、物理备份、增量备份的实战应用
  2. 恢复技术:时间点恢复、误操作恢复、灾难恢复的完整流程
  3. 安全管理:权限体系、数据加密、审计监控的全面方案
  4. 安全开发:SQL注入防护、输入验证的安全编码实践

关键安全原则:

  • 最小权限:用户只拥有完成工作所需的最小权限
  • 纵深防御:多层安全措施,避免单点失效
  • 定期审计:持续监控和审查安全状态
  • 应急准备:完善的备份和恢复预案

备份恢复最佳实践:

  • 3-2-1规则:3个副本,2种介质,1个离线存储
  • 定期恢复演练:确保备份可用性
  • 监控备份状态:及时发现问题
  • 加密敏感数据:保护数据隐私

在下一篇中,我们将探讨MySQL在云原生环境中的应用,包括容器化部署、微服务架构集成等现代技术。

动手练习:

  1. 设计并实施完整的备份策略,包括完整备份和增量备份
  2. 执行时间点恢复演练,验证备份的可用性
  3. 建立权限管理体系,实施最小权限原则
  4. 配置数据加密和审计日志,增强安全性
  5. 进行安全代码审查,修复潜在的SQL注入漏洞

欢迎在评论区分享你的备份恢复实践和安全加固经验!

🔲 ☆

MySql入门:高可用与架构设计

MySQL高可用与架构设计

在现代互联网应用中,数据库的高可用性和可扩展性至关重要。单点故障可能导致整个系统瘫痪,性能瓶颈可能影响用户体验。今天,我们将深入探讨MySQL的高可用架构设计,从主从复制到分布式集群,帮助你构建稳定可靠的数据库系统。

1. 主从复制架构

复制原理与三种复制模式

复制基本原理:

-- 复制过程涉及的关键线程-- 主库:Binlog Dump Thread-- 从库:I/O Thread, SQL Thread-- 查看主库状态SHOW MASTER STATUS;/*+------------------+----------+--------------+------------------+-------------------+| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |+------------------+----------+--------------+------------------+-------------------+| mysql-bin.000003 |      194 |              |                  |                   |+------------------+----------+--------------+------------------+-------------------+*/-- 查看从库状态SHOW SLAVE STATUS\G/*             Slave_IO_State: Waiting for master to send event                Master_Host: 192.168.1.100                Master_User: repl                Master_Port: 3306              Connect_Retry: 60            Master_Log_File: mysql-bin.000003        Read_Master_Log_Pos: 194             Relay_Log_File: relay-bin.000002              Relay_Log_Pos: 320      Relay_Master_Log_File: mysql-bin.000003           Slave_IO_Running: Yes          Slave_SQL_Running: Yes            Replicate_Do_DB:         Replicate_Ignore_DB:          Replicate_Do_Table:      Replicate_Ignore_Table:     Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table:                  Last_Errno: 0                 Last_Error:                Skip_Counter: 0        Exec_Master_Log_Pos: 194            Relay_Log_Space: 526            Until_Condition: None             Until_Log_File:               Until_Log_Pos: 0         Master_SSL_Allowed: No         Master_SSL_CA_File:          Master_SSL_CA_Path:             Master_SSL_Cert:           Master_SSL_Cipher:              Master_SSL_Key:       Seconds_Behind_Master: 0Master_SSL_Verify_Server_Cert: No             Last_IO_Errno: 0             Last_IO_Error:             Last_SQL_Errno: 0            Last_SQL_Error:   Replicate_Ignore_Server_Ids:              Master_Server_Id: 1                  Master_UUID: 6b0f1c1a-5d5e-11eb-ae93-000c29a3a3a3             Master_Info_File: mysql.slave_master_info                    SQL_Delay: 0          SQL_Remaining_Delay: NULL      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates           Master_Retry_Count: 86400                  Master_Bind:       Last_IO_Error_Timestamp:      Last_SQL_Error_Timestamp:                Master_SSL_Crl:            Master_SSL_Crlpath:            Retrieved_Gtid_Set:             Executed_Gtid_Set:                 Auto_Position: 0         Replicate_Rewrite_DB:                  Channel_Name:            Master_TLS_Version: */

三种复制模式对比:

-- 1. 基于语句的复制(Statement-Based Replication)-- 配置SET GLOBAL binlog_format = 'STATEMENT';-- 优点:二进制日志较小,网络传输量少-- 缺点:非确定性函数可能导致数据不一致-- 2. 基于行的复制(Row-Based Replication)SET GLOBAL binlog_format = 'ROW';-- 优点:数据一致性更好-- 缺点:二进制日志较大,网络传输量大-- 3. 混合模式复制(Mixed)SET GLOBAL binlog_format = 'MIXED';-- 优点:结合两者优势,自动选择最优方式-- 缺点:配置相对复杂-- 生产环境推荐使用ROW或MIXED模式

基于二进制日志的复制机制

二进制日志配置:

-- 查看二进制日志配置SHOW VARIABLES LIKE 'log_bin%';SHOW VARIABLES LIKE 'binlog_format%';SHOW VARIABLES LIKE 'sync_binlog%';SHOW VARIABLES LIKE 'expire_logs_days%';-- 二进制日志配置示例(my.cnf)/*[mysqld]# 启用二进制日志log_bin = /var/lib/mysql/mysql-bin# 日志格式binlog_format = ROW# 每次事务提交都同步到磁盘sync_binlog = 1# 日志保留7天expire_logs_days = 7# 每个日志文件大小max_binlog_size = 100M# 自动清理日志binlog_expire_logs_seconds = 604800*/

复制过滤规则:

-- 主库过滤规则-- 在my.cnf中配置/*# 忽略系统库的复制binlog_ignore_db = mysqlbinlog_ignore_db = information_schemabinlog_ignore_db = performance_schemabinlog_ignore_db = sys*/-- 从库过滤规则CHANGE MASTER TO     REPLICATE_DO_DB = (app_db),    REPLICATE_IGNORE_DB = (test,temp_db),    REPLICATE_DO_TABLE = (app_db.important_table),    REPLICATE_IGNORE_TABLE = (app_db.log_table);-- 通配符过滤CHANGE MASTER TO    REPLICATE_WILD_DO_TABLE = ('app_db.shard_%'),    REPLICATE_WILD_IGNORE_TABLE = ('app_db.temp_%');

半同步复制配置实战

半同步复制原理:

-- 安装半同步插件(主从库都需要)INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';-- 查看插件状态SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE '%semi%';-- 配置主库半同步SET GLOBAL rpl_semi_sync_master_enabled = 1;SET GLOBAL rpl_semi_sync_master_timeout = 1000;  -- 1秒超时-- 配置从库半同步SET GLOBAL rpl_semi_sync_slave_enabled = 1;-- 查看半同步状态SHOW STATUS LIKE 'Rpl_semi_sync%';/*Rpl_semi_sync_master_status          | ONRpl_semi_sync_master_clients         | 2      -- 连接的半同步从库数量Rpl_semi_sync_master_yes_tx          | 1000   -- 成功通过半同步的事务数Rpl_semi_sync_master_no_tx           | 5      -- 超时后转为异步的事务数*/

半同步复制配置优化:

-- 持久化配置(在my.cnf中)/*[mysqld]# 主库配置plugin_load = "rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"rpl_semi_sync_master_enabled = 1rpl_semi_sync_slave_enabled = 1rpl_semi_sync_master_timeout = 1000rpl_semi_sync_master_wait_point = AFTER_SYNC  -- MySQL 5.7+ 推荐*/-- 监控半同步复制SELECT     VARIABLE_NAME,    VARIABLE_VALUEFROM performance_schema.global_statusWHERE VARIABLE_NAME LIKE 'RPL_SEMI_SYNC%';-- 半同步复制降级监控-- 当从库响应超时或故障时,主库会自动降级为异步复制-- 需要监控降级事件并及时处理

多源复制与链式复制

多源复制配置:

-- MySQL 5.7+ 支持多源复制-- 从多个主库复制数据到单个从库-- 配置多源复制通道-- 主库1配置CHANGE MASTER TO     MASTER_HOST = 'master1_host',    MASTER_USER = 'repl',    MASTER_PASSWORD = 'password',    MASTER_PORT = 3306,    MASTER_AUTO_POSITION = 1FOR CHANNEL 'master1';-- 主库2配置  CHANGE MASTER TO    MASTER_HOST = 'master2_host',    MASTER_USER = 'repl',    MASTER_PASSWORD = 'password',    MASTER_PORT = 3306,    MASTER_AUTO_POSITION = 1FOR CHANNEL 'master2';-- 启动多源复制START SLAVE FOR CHANNEL 'master1';START SLAVE FOR CHANNEL 'master2';-- 查看多源复制状态SHOW SLAVE STATUS FOR CHANNEL 'master1'\GSHOW SLAVE STATUS FOR CHANNEL 'master2'\G-- 按通道过滤操作STOP SLAVE SQL_THREAD FOR CHANNEL 'master1';START SLAVE SQL_THREAD FOR CHANNEL 'master1';

链式复制架构:

-- 三级复制链:Master -> Relay Slave -> Leaf Slave-- 配置中继从库/*Master配置:log_bin = onlog_slave_updates = off  -- 默认,中继库不需要记录从库更新Relay Slave配置:log_bin = onlog_slave_updates = on   -- 关键:记录从主库接收的更新Leaf Slave配置:log_bin = off  -- 或者 on,根据需求log_slave_updates = off*/-- 中继从库的特殊配置/*[mysqld]# 中继从库配置server_id = 2log_bin = mysql-binlog_slave_updates = 1relay_log = relay-binread_only = 1# 过滤规则(可选)replicate_do_db = app_dbreplicate_ignore_db = mysql*/

复制故障排查与修复

常见复制错误处理:

-- 1. 主键冲突错误-- 错误信息:Duplicate entry 'X' for key 'PRIMARY'-- 解决方案:STOP SLAVE;SET GLOBAL sql_slave_skip_counter = 1;START SLAVE;-- 或者手动处理冲突数据STOP SLAVE;-- 查看冲突数据SELECT * FROM table_name WHERE primary_key = 'X';-- 删除冲突数据或更新主键DELETE FROM table_name WHERE primary_key = 'X';START SLAVE;-- 2. 数据不存在错误-- 错误信息:Can't find record in 'table_name'-- 解决方案:STOP SLAVE;-- 在从库插入缺失的数据INSERT IGNORE INTO table_name VALUES (...);START SLAVE;-- 3. 网络中断导致的复制延迟-- 监控复制延迟SHOW SLAVE STATUS\G-- 查看Seconds_Behind_Master-- 自动重连配置CHANGE MASTER TO     MASTER_CONNECT_RETRY = 60,    MASTER_RETRY_COUNT = 86400;

GTID复制故障处理:

-- 启用GTID复制-- 在my.cnf中配置/*[mysqld]gtid_mode = ONenforce_gtid_consistency = ON*/-- GTID复制错误处理-- 查看错误的GTIDSHOW SLAVE STATUS\G-- Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s)...-- Retrieved_Gtid_Set: 6b0f1c1a-5d5e-11eb-ae93-000c29a3a3a3:1-100-- Executed_Gtid_Set: 6b0f1c1a-5d5e-11eb-ae93-000c29a3a3a3:1-95-- 跳过特定GTID事务STOP SLAVE;SET GTID_NEXT = '6b0f1c1a-5d5e-11eb-ae93-000c29a3a3a3:96';BEGIN; COMMIT;SET GTID_NEXT = 'AUTOMATIC';START SLAVE;-- 重置GTID复制-- 注意:这会清除所有复制信息,需要重新配置STOP SLAVE;RESET SLAVE ALL;CHANGE MASTER TO ...;START SLAVE;

2. 高可用集群方案

MySQL Router读写分离

MySQL Router部署配置:

# MySQL Router配置文件 (mysqlrouter.conf)[DEFAULT]logging_folder = /var/log/mysqlrouterruntime_folder = /var/run/mysqlrouterconfig_folder = /etc/mysqlrouter[routing:read_write]bind_address = 0.0.0.0bind_port = 6446destinations = metadata-cache://mycluster/?role=PRIMARYrouting_strategy = first-available[routing:read_only]bind_address = 0.0.0.0bind_port = 6447destinations = metadata-cache://mycluster/?role=SECONDARYrouting_strategy = round-robin# 启动MySQL Router# mysqlrouter --config=/etc/mysqlrouter/mysqlrouter.conf &

应用程序连接配置:

# Python应用程序连接示例import mysql.connector# 写操作连接(主库)write_config = {    'host': 'router_host',    'port': 6446,  # 读写端口    'user': 'app_user',    'password': 'password',    'database': 'app_db'}# 读操作连接(从库)read_config = {    'host': 'router_host',     'port': 6447,  # 只读端口    'user': 'app_user',    'password': 'password',    'database': 'app_db'}# 写操作def update_user_profile(user_id, data):    conn = mysql.connector.connect(**write_config)    # 执行更新操作    conn.close()# 读操作  def get_user_profile(user_id):    conn = mysql.connector.connect(**read_config)    # 执行查询操作    conn.close()

MHA自动故障转移

MHA架构组成:

# MHA组件# 1. MHA Manager - 管理节点# 2. MHA Node - 数据节点代理# MHA Manager配置 (app1.cnf)[server default]manager_log=/var/log/masterha/app1.logmanager_workdir=/var/log/masterha/app1master_binlog_dir=/var/lib/mysqluser=mha_userpassword=mha_passwordping_interval=3remote_workdir=/tmprepl_user=repl_userrepl_password=repl_passwordssh_user=root[server1]hostname=master_hostport=3306[server2] hostname=slave1_hostport=3306candidate_master=1[server3]hostname=slave2_hostport=3306no_master=1# 启动MHA监控masterha_manager --conf=/etc/masterha/app1.cnf

MHA故障转移过程:

# 1. 检测主库故障# 2. 选择新主库(优先candidate_master=1的从库)# 3. 应用差异的二进制日志# 4. 提升新主库# 5. 其他从库指向新主库# 6. 虚拟IP切换(可选)# 手动触发故障转移masterha_master_switch --conf=/etc/masterha/app1.cnf --master_state=dead# 检查MHA状态masterha_check_status --conf=/etc/masterha/app1.cnf# MHA监控脚本示例#!/bin/bash# mha_monitor.shCONFIG_FILE="/etc/masterha/app1.cnf"LOG_FILE="/var/log/masterha/monitor.log"while true; do    status=$(masterha_check_status --conf=$CONFIG_FILE 2>&1)    if [[ $status != *"alive"* ]]; then        echo "$(date): MHA manager is not running, restarting..." >> $LOG_FILE        nohup masterha_manager --conf=$CONFIG_FILE >> $LOG_FILE 2>&1 &    fi    sleep 30done

Orchestrator管理工具

Orchestrator部署配置:

// orchestrator.conf.json{  "Debug": false,  "EnableSyslog": false,    "MySQLTopologyUser": "orchestrator",  "MySQLTopologyPassword": "orchestrator_password",  "MySQLTopologyCredentialsConfigFile": "",  "MySQLTopologySSLPrivateKeyFile": "",  "MySQLTopologySSLCertFile": "",  "MySQLTopologySSLCAFile": "",  "MySQLTopologySSLSkipVerify": true,  "MySQLTopologyUseMutualTLS": false,    "MySQLOrchestratorHost": "127.0.0.1",  "MySQLOrchestratorPort": 3306,  "MySQLOrchestratorDatabase": "orchestrator",  "MySQLOrchestratorUser": "orchestrator",  "MySQLOrchestratorPassword": "orchestrator_password",    "RaftEnabled": true,  "RaftDataDir": "/var/lib/orchestrator",  "RaftBind": "192.168.1.100",  "DefaultRaftPort": 10008,    "AutoPseudoGTID": false,  "DetectClusterAliasQuery": "SELECT SUBSTRING_INDEX(@@hostname, '.', 1)",  "DetectInstanceAliasQuery": "SELECT @@hostname",    "RecoveryPeriodBlockSeconds": 3600,  "RecoveryIgnoreHostnameFilters": [],    "PromotionIgnoreHostnameFilters": [],    "ApplyMySQLPromotionAfterMasterFailover": true,  "PreFailoverProcesses": [    "echo 'Will recover from {failureType} on {failureCluster}' >> /tmp/recovery.log"  ],  "PostFailoverProcesses": [    "echo 'Recovered from {failureType} on {failureCluster}. Failed: {failedHost}:{failedPort}; Successor: {successorHost}:{successorPort}' >> /tmp/recovery.log"  ]}

Orchestrator API使用:

# 通过REST API管理集群# 发现并注册实例curl "http://orchestrator:3000/api/discover/192.168.1.101/3306"# 查看集群拓扑curl "http://orchestrator:3000/api/cluster/myapp"# 手动故障转移curl "http://orchestrator:3000/api/force-master-failover/myapp"# 查看恢复信息curl "http://orchestrator:3000/api/audit-recovery"# 维护模式curl "http://orchestrator:3000/api/maintenance/myapp/begin"curl "http://orchestrator:3000/api/maintenance/myapp/end"

基于Keepalived的VIP方案

Keepalived配置:

# keepalived.confglobal_defs {    router_id MYSQL_HA}vrrp_script chk_mysql {    script "/usr/bin/mysqlchk"    interval 2    weight 2    fall 2    rise 2}vrrp_instance VI_1 {    state BACKUP    interface eth0    virtual_router_id 51    priority 100    advert_int 1        authentication {        auth_type PASS        auth_pass 1111    }        virtual_ipaddress {        192.168.1.200    }        track_script {        chk_mysql    }        notify_master "/etc/keepalived/notify.sh master"    notify_backup "/etc/keepalived/notify.sh backup"    notify_fault "/etc/keepalived/notify.sh fault"}

MySQL健康检查脚本:

#!/bin/bash# mysqlchk - MySQL健康检查脚本MYSQL_HOST="localhost"MYSQL_PORT="3306"MYSQL_USER="health_check"MYSQL_PASS="health_check_password"MYSQL_CMD="/usr/bin/mysql"# 检查MySQL是否可连接$MYSQL_CMD -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS -e "SELECT 1;" > /dev/null 2>&1if [ $? -eq 0 ]; then    # 检查复制状态(如果是从库)    SLAVE_STATUS=$($MYSQL_CMD -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS -e "SHOW SLAVE STATUS\G" 2>/dev/null)        if [ -n "$SLAVE_STATUS" ]; then        # 是从库,检查复制状态        IO_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_IO_Running:" | awk '{print $2}')        SQL_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}')        SECONDS_BEHIND=$(echo "$SLAVE_STATUS" | grep "Seconds_Behind_Master:" | awk '{print $2}')                if [ "$IO_RUNNING" = "Yes" ] && [ "$SQL_RUNNING" = "Yes" ] && [ "$SECONDS_BEHIND" -lt 60 ]; then            exit 0  # 健康        else            exit 1  # 不健康        fi    else        # 是主库,直接健康        exit 0    fielse    exit 1  # MySQL不可连接fi

状态切换通知脚本:

#!/bin/bash# notify.sh - 状态切换通知TYPE=$1VIP="192.168.1.200"LOG_FILE="/var/log/keepalived.log"log() {    echo "$(date): $1" >> $LOG_FILE}case $TYPE in    master)        log "切换为MASTER状态,绑定VIP: $VIP"        # 这里可以添加提升为主库的逻辑        # 比如设置read_only=OFF,通知应用等        mysql -e "SET GLOBAL read_only=OFF;"        ;;    backup)        log "切换为BACKUP状态,释放VIP"        # 设置只读模式        mysql -e "SET GLOBAL read_only=ON;"        ;;    fault)        log "进入FAULT状态"        ;;    *)        log "未知状态: $TYPE"        ;;esac

高可用架构选型指南

架构选型矩阵:

方案适用场景优点缺点复杂度
主从+VIP中小型应用,预算有限简单可靠,成本低手动切换,监控复杂
MHA中型应用,需要自动故障转移自动故障转移,成熟稳定需要额外管理节点
Orchestrator复杂拓扑,需要灵活管理拓扑感知,API丰富配置复杂,学习成本高
MySQL InnoDB ClusterMySQL 8.0,原生高可用官方方案,集成度高版本要求高,资源消耗大
云数据库快速部署,免运维全托管,自动备份成本较高,厂商锁定

选型考虑因素:

-- 业务需求评估-- 1. RTO(恢复时间目标)SELECT     CASE         WHEN rto_requirement <= 30 THEN '需要自动故障转移'        WHEN rto_requirement <= 300 THEN '半自动故障转移'        ELSE '手动故障转移可接受'    END as ha_levelFROM business_requirements;-- 2. RPO(数据恢复点目标)SELECT     CASE        WHEN rpo_requirement = 0 THEN '需要同步复制'        WHEN rpo_requirement <= 1 THEN '需要半同步复制'         WHEN rpo_requirement <= 60 THEN '异步复制可接受'        ELSE '数据丢失可接受'    END as data_protection_levelFROM business_requirements;-- 3. 读写分离需求SELECT     CASE        WHEN read_ratio > 0.8 THEN '需要强大的读写分离'        WHEN read_ratio > 0.5 THEN '需要基础读写分离'        ELSE '读写分离非必需'    END as read_write_separationFROM workload_analysis;

3. 数据库架构设计

读写分离架构设计

应用层读写分离:

// Java应用层读写分离示例@Componentpublic class DataSourceRouter {        @Value("${datasource.master.url}")    private String masterUrl;        @Value("${datasource.slave.url}")     private String slaveUrl;        private ThreadLocal<Boolean> readOnly = new ThreadLocal<>();        public void setReadOnly(boolean readOnly) {        this.readOnly.set(readOnly);    }        public DataSource getDataSource() {        if (Boolean.TRUE.equals(readOnly.get())) {            return createDataSource(slaveUrl);        } else {            return createDataSource(masterUrl);        }    }        // AOP切面自动设置读写分离    @Aspect    @Component    public class ReadWriteSeparationAspect {                @Around("@annotation(org.springframework.transaction.annotation.Transactional)")        public Object handleTransaction(ProceedingJoinPoint joinPoint) throws Throwable {            Transactional transactional = ((MethodSignature) joinPoint.getSignature())                .getMethod().getAnnotation(Transactional.class);                        if (transactional.readOnly()) {                DataSourceContextHolder.setReadOnly(true);            }                        try {                return joinPoint.proceed();            } finally {                DataSourceContextHolder.clear();            }        }    }}

中间件读写分离:

# ShardingSphere配置示例# config-sharding.yamldataSources:  master_ds:    url: jdbc:mysql://master:3306/db?serverTimezone=UTC&useSSL=false    username: root    password: password    connectionTimeoutMilliseconds: 30000    idleTimeoutMilliseconds: 60000    maxLifetimeMilliseconds: 1800000    maxPoolSize: 50  slave_ds_0:    url: jdbc:mysql://slave0:3306/db?serverTimezone=UTC&useSSL=false    username: root    password: password    connectionTimeoutMilliseconds: 30000    idleTimeoutMilliseconds: 60000    maxLifetimeMilliseconds: 1800000    maxPoolSize: 50  slave_ds_1:    url: jdbc:mysql://slave1:3306/db?serverTimezone=UTC&useSSL=false    username: root    password: password    connectionTimeoutMilliseconds: 30000    idleTimeoutMilliseconds: 60000    maxLifetimeMilliseconds: 1800000    maxPoolSize: 50rules:- !LOAD_BALANCE  loadBalancers:    round_robin:      type: ROUND_ROBIN  dataSources:    read_ds:      dataSourceNames:        - slave_ds_0        - slave_ds_1      loadBalancerName: round_robin      - !SINGLE  defaultDataSource: master_ds  loadBalancers:    round_robin:      type: ROUND_ROBIN

分库分表策略与实现

水平分表策略:

-- 用户表按ID范围分表-- 创建分表CREATE TABLE users_0000 LIKE users_template;CREATE TABLE users_0001 LIKE users_template;CREATE TABLE users_0002 LIKE users_template;-- ... 创建更多分表-- 分表路由函数DELIMITER //CREATE FUNCTION get_user_table_name(user_id BIGINT)RETURNS VARCHAR(64)DETERMINISTICBEGIN    DECLARE table_suffix VARCHAR(4);    SET table_suffix = LPAD(MOD(user_id, 16), 4, '0');    RETURN CONCAT('users_', table_suffix);END //DELIMITER ;-- 分表查询示例SET @user_id = 123456;SET @table_name = get_user_table_name(@user_id);SET @sql = CONCAT('SELECT * FROM ', @table_name, ' WHERE user_id = ?');PREPARE stmt FROM @sql;EXECUTE stmt USING @user_id;DEALLOCATE PREPARE stmt;

垂直分库设计:

-- 业务垂直拆分-- 用户库CREATE DATABASE user_center;USE user_center;CREATE TABLE users (    user_id BIGINT PRIMARY KEY,    username VARCHAR(50),    email VARCHAR(100),    password_hash VARCHAR(255),    created_at TIMESTAMP);CREATE TABLE user_profiles (    user_id BIGINT PRIMARY KEY,    real_name VARCHAR(100),    avatar_url VARCHAR(500),    bio TEXT);-- 订单库CREATE DATABASE order_center;USE order_center;CREATE TABLE orders (    order_id BIGINT PRIMARY KEY,    user_id BIGINT,  -- 跨库关联    total_amount DECIMAL(12,2),    status VARCHAR(20),    created_at TIMESTAMP);CREATE TABLE order_items (    item_id BIGINT PRIMARY KEY,    order_id BIGINT,    product_id BIGINT,    quantity INT,    price DECIMAL(10,2));-- 商品库CREATE DATABASE product_center;USE product_center;CREATE TABLE products (    product_id BIGINT PRIMARY KEY,    product_name VARCHAR(200),    category_id INT,    price DECIMAL(10,2),    stock_quantity INT);

数据拆分:垂直拆分与水平拆分

垂直拆分实施:

-- 原始大表CREATE TABLE user_comprehensive (    user_id BIGINT PRIMARY KEY,    -- 基础信息    username VARCHAR(50),    email VARCHAR(100),    password_hash VARCHAR(255),    -- 个人信息    real_name VARCHAR(100),    id_card VARCHAR(20),    phone VARCHAR(20),    -- 扩展信息    education VARCHAR(50),    occupation VARCHAR(50),    income_level INT,    -- 行为信息    last_login_time TIMESTAMP,    login_count INT,    -- 其他字段...    created_at TIMESTAMP,    updated_at TIMESTAMP);-- 垂直拆分后-- 用户基础表(高频访问)CREATE TABLE users_basic (    user_id BIGINT PRIMARY KEY,    username VARCHAR(50),    email VARCHAR(100),    password_hash VARCHAR(255),    last_login_time TIMESTAMP,    login_count INT,    created_at TIMESTAMP);-- 用户详情表(低频访问)CREATE TABLE users_detail (    user_id BIGINT PRIMARY KEY,    real_name VARCHAR(100),    id_card VARCHAR(20),    phone VARCHAR(20),    education VARCHAR(50),    occupation VARCHAR(50),    income_level INT,    updated_at TIMESTAMP);-- 创建索引优化查询ALTER TABLE users_basic ADD INDEX idx_username (username);ALTER TABLE users_basic ADD INDEX idx_email (email);ALTER TABLE users_detail ADD INDEX idx_phone (phone);

水平拆分策略:

-- 时间范围分表(适用于时间序列数据)-- 按月分表CREATE TABLE logs_2023_01 LIKE logs_template;CREATE TABLE logs_2023_02 LIKE logs_template;CREATE TABLE logs_2023_03 LIKE logs_template;-- 时间分表管理存储过程DELIMITER //CREATE PROCEDURE create_next_month_table()BEGIN    DECLARE next_month VARCHAR(7);    DECLARE table_name VARCHAR(64);    DECLARE create_sql TEXT;        SET next_month = DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 MONTH), '%Y_%m');    SET table_name = CONCAT('logs_', next_month);    SET create_sql = CONCAT('CREATE TABLE IF NOT EXISTS ', table_name, ' LIKE logs_template');        PREPARE stmt FROM create_sql;    EXECUTE stmt;    DEALLOCATE PREPARE stmt;        -- 记录创建日志    INSERT INTO table_creation_log (table_name, created_at)     VALUES (table_name, NOW());END //DELIMITER ;-- 地理分表(适用于地域性数据)CREATE TABLE users_north LIKE users_template;  -- 北方用户CREATE TABLE users_south LIKE users_template;  -- 南方用户CREATE TABLE users_east LIKE users_template;   -- 东方用户  CREATE TABLE users_west LIKE users_template;   -- 西方用户-- 基于业务特征分表CREATE TABLE users_vip LIKE users_template;    -- VIP用户CREATE TABLE users_normal LIKE users_template; -- 普通用户CREATE TABLE users_trial LIKE users_template;  -- 试用用户

分布式ID生成方案

数据库序列方案:

-- 基于数据库的ID生成器CREATE TABLE sequence_generator (    sequence_name VARCHAR(50) PRIMARY KEY,    current_value BIGINT NOT NULL DEFAULT 0,    step INT NOT NULL DEFAULT 1,    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP);-- 获取下一个ID的存储过程DELIMITER //CREATE FUNCTION next_id(seq_name VARCHAR(50))RETURNS BIGINTBEGIN    DECLARE current_val BIGINT;    DECLARE retry_count INT DEFAULT 0;    DECLARE max_retries INT DEFAULT 3;        retry_loop: WHILE retry_count < max_retries DO        -- 获取当前值        SELECT current_value INTO current_val        FROM sequence_generator         WHERE sequence_name = seq_name;                IF current_val IS NULL THEN            -- 初始化序列            INSERT INTO sequence_generator (sequence_name, current_value)             VALUES (seq_name, 1)            ON DUPLICATE KEY UPDATE current_value = 1;            SET current_val = 1;        END IF;                -- 尝试更新        UPDATE sequence_generator         SET current_value = current_value + step,            updated_at = CURRENT_TIMESTAMP        WHERE sequence_name = seq_name           AND current_value = current_val;                IF ROW_COUNT() = 1 THEN            RETURN current_val + 1;        END IF;                SET retry_count = retry_count + 1;        DO SLEEP(0.01);  -- 短暂等待后重试    END WHILE;        -- 重试失败,抛出异常    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Failed to generate sequence ID';END //DELIMITER ;

Snowflake算法实现:

-- Snowflake ID生成器表CREATE TABLE snowflake_worker (    worker_id INT PRIMARY KEY,    datacenter_id INT NOT NULL,    worker_name VARCHAR(100),    last_timestamp BIGINT,    sequence BIGINT DEFAULT 0,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);-- Snowflake ID生成函数DELIMITER //CREATE FUNCTION snowflake_next_id(worker_id INT)RETURNS BIGINTBEGIN    DECLARE epoch BIGINT DEFAULT 1609459200000; -- 2021-01-01    DECLARE current_ms BIGINT;    DECLARE last_ms BIGINT;    DECLARE sequence_val BIGINT;    DECLARE datacenter_id_val INT;        -- 获取worker信息    SELECT last_timestamp, sequence, datacenter_id     INTO last_ms, sequence_val, datacenter_id_val    FROM snowflake_worker     WHERE worker_id = worker_id    FOR UPDATE;  -- 加锁防止并发        -- 计算当前时间戳    SET current_ms = (UNIX_TIMESTAMP(NOW(3)) * 1000);        IF current_ms < last_ms THEN        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Clock moved backwards';    END IF;        IF current_ms = last_ms THEN        SET sequence_val = (sequence_val + 1) & 4095;  -- 12位序列号,最大4095        IF sequence_val = 0 THEN            -- 序列号耗尽,等待下一毫秒            SET current_ms = wait_next_ms(last_ms);        END IF;    ELSE        SET sequence_val = 0;    END IF;        -- 更新worker状态    UPDATE snowflake_worker     SET last_timestamp = current_ms,        sequence = sequence_val    WHERE worker_id = worker_id;        -- 生成ID: 时间戳(41位) + 数据中心ID(5位) + 工作节点ID(5位) + 序列号(12位)    RETURN ((current_ms - epoch) << 22)          | (datacenter_id_val << 17)          | (worker_id << 12)          | sequence_val;END //CREATE FUNCTION wait_next_ms(last_ms BIGINT)RETURNS BIGINTBEGIN    DECLARE current_ms BIGINT;    SET current_ms = (UNIX_TIMESTAMP(NOW(3)) * 1000);    WHILE current_ms <= last_ms DO        SET current_ms = (UNIX_TIMESTAMP(NOW(3)) * 1000);    END WHILE;    RETURN current_ms;END //DELIMITER ;

数据迁移与同步方案

在线数据迁移:

-- 双写迁移方案-- 1. 准备阶段:创建新表,建立双写机制CREATE TABLE users_new LIKE users_old;-- 2. 数据同步阶段:存量数据迁移INSERT INTO users_new SELECT * FROM users_old WHERE id > ? AND id <= ?;  -- 分批迁移-- 3. 增量数据双写-- 应用程序同时写入users_old和users_new-- 4. 数据验证SELECT     COUNT(*) as old_count,    (SELECT COUNT(*) FROM users_new) as new_count,    COUNT(*) - (SELECT COUNT(*) FROM users_new) as diffFROM users_old;-- 5. 切换阶段:停止写入旧表,全面使用新表-- 6. 清理阶段:删除旧表-- 使用pt-online-schema-change工具-- pt-online-schema-change --alter="ADD COLUMN new_column INT" D=database,t=table --execute

数据同步监控:

-- 创建数据同步监控表CREATE TABLE data_sync_monitor (    id BIGINT AUTO_INCREMENT PRIMARY KEY,    sync_job VARCHAR(100) NOT NULL,    source_count BIGINT,    target_count BIGINT,    diff_count BIGINT,    sync_status ENUM('running', 'completed', 'failed'),    started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    completed_at TIMESTAMP NULL,    error_message TEXT);-- 数据一致性检查存储过程DELIMITER //CREATE PROCEDURE check_data_consistency(    IN source_table VARCHAR(64),    IN target_table VARCHAR(64),    IN primary_key VARCHAR(64))BEGIN    DECLARE source_total BIGINT;    DECLARE target_total BIGINT;    DECLARE diff_count BIGINT;        -- 检查记录总数    SET @source_sql = CONCAT('SELECT COUNT(*) INTO @source_count FROM ', source_table);    PREPARE stmt1 FROM @source_sql;    EXECUTE stmt1;    DEALLOCATE PREPARE stmt1;        SET @target_sql = CONCAT('SELECT COUNT(*) INTO @target_count FROM ', target_table);    PREPARE stmt2 FROM @target_sql;    EXECUTE stmt2;    DEALLOCATE PREPARE stmt2;        SET source_total = @source_count;    SET target_total = @target_count;    SET diff_count = ABS(source_total - target_total);        -- 记录检查结果    INSERT INTO data_sync_monitor (sync_job, source_count, target_count, diff_count, sync_status)    VALUES (CONCAT(source_table, '_to_', target_table), source_total, target_total, diff_count,            CASE WHEN diff_count = 0 THEN 'completed' ELSE 'failed' END);        -- 如果有差异,记录具体差异数据    IF diff_count > 0 THEN        -- 这里可以添加更详细的差异分析        INSERT INTO data_diff_log (sync_job, diff_type, diff_details)        VALUES (CONCAT(source_table, '_to_', target_table), 'count_mismatch',                CONCAT('Source: ', source_total, ', Target: ', target_total));    END IF;    END //DELIMITER ;

总结

通过本篇的深入学习,我们掌握了MySQL高可用架构设计的核心知识:

  1. 主从复制:理解了复制原理、配置方法和故障处理
  2. 高可用方案:掌握了MHA、Orchestrator、Keepalived等工具的使用
  3. 架构设计:学会了读写分离、分库分表、分布式ID生成等高级技术
  4. 数据迁移:了解了在线数据迁移和同步的最佳实践

关键架构原则:

  • 冗余设计:确保没有单点故障
  • 自动故障转移:减少人工干预,提高可用性
  • 监控告警:及时发现问题并处理
  • 容量规划:提前规划系统扩展能力
  • 数据安全:保证数据的一致性和完整性

架构演进路径:

  1. 单机架构主从复制
  2. 主从复制读写分离
  3. 读写分离分库分表
  4. 分库分表分布式数据库

动手练习:

  1. 搭建MySQL主从复制环境,并测试故障转移
  2. 配置MHA或Orchestrator实现自动故障转移
  3. 设计并实施读写分离架构
  4. 实践分库分表方案,解决单表数据量过大的问题
  5. 实现分布式ID生成方案

欢迎在评论区分享你的高可用架构实践经验和遇到的问题!

🔲 ☆

MySql入门:SQL编程与高级特性

SQL编程与高级特性

SQL不仅仅是简单的数据查询语言,它拥有强大的编程能力和高级特性。掌握这些特性可以让你写出更高效、更优雅的数据库操作代码。今天,我们将深入探讨MySQL的SQL编程能力,从基础查询到高级特性,帮助你成为SQL编程的高手。

1. SQL基础与高级查询

DDL、DML、DCL、TCL全面掌握

数据定义语言(DDL) - 定义数据结构:

-- 数据库操作CREATE DATABASE company CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;ALTER DATABASE company CHARACTER SET utf8mb4;DROP DATABASE IF EXISTS old_company;-- 表操作CREATE TABLE employees (    emp_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    emp_name VARCHAR(100) NOT NULL,    email VARCHAR(255) UNIQUE,    salary DECIMAL(10,2) CHECK (salary > 0),    dept_id INT UNSIGNED,    hire_date DATE DEFAULT (CURRENT_DATE),    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB COMMENT='员工表';-- 表结构修改ALTER TABLE employees ADD COLUMN phone VARCHAR(20) AFTER email,MODIFY COLUMN emp_name VARCHAR(150) NOT NULL,ADD INDEX idx_dept_hire (dept_id, hire_date);-- 表维护ANALYZE TABLE employees;  -- 更新统计信息OPTIMIZE TABLE employees; -- 优化表存储RENAME TABLE employees TO staff;

数据操作语言(DML) - 操作数据:

-- 插入数据INSERT INTO employees (emp_name, email, salary, dept_id, hire_date)VALUES     ('张三', 'zhangsan@company.com', 8000.00, 1, '2023-01-15'),    ('李四', 'lisi@company.com', 7500.00, 1, '2023-02-20'),    ('王五', 'wangwu@company.com', 9000.00, 2, '2023-03-10');-- 插入并忽略重复键INSERT IGNORE INTO employees (emp_name, email, salary, dept_id)VALUES ('赵六', 'zhangsan@company.com', 8500.00, 2);  -- 邮箱重复,被忽略-- 批量插入(高效方式)INSERT INTO employees (emp_name, email, salary, dept_id)SELECT     CONCAT('员工', num) as emp_name,    CONCAT('emp', num, '@company.com') as email,    5000 + (RAND() * 5000) as salary,    FLOOR(1 + RAND() * 3) as dept_idFROM (    SELECT @row := @row + 1 as num    FROM information_schema.columns c1,         information_schema.columns c2,         (SELECT @row := 0) r    LIMIT 100) numbers;-- 更新数据UPDATE employees SET salary = salary * 1.1,    updated_at = CURRENT_TIMESTAMPWHERE dept_id = 1   AND hire_date < '2023-06-01';-- 使用JOIN更新UPDATE employees eJOIN departments d ON e.dept_id = d.dept_idSET e.salary = e.salary * 1.05WHERE d.dept_name = '技术部';-- 删除数据DELETE FROM employees WHERE emp_id = 100;-- 使用JOIN删除DELETE e FROM employees eLEFT JOIN departments d ON e.dept_id = d.dept_idWHERE d.dept_id IS NULL;  -- 删除部门不存在的员工

数据控制语言(DCL) - 权限管理:

-- 用户管理CREATE USER 'report_user'@'192.168.1.%' IDENTIFIED BY 'secure_password_123';CREATE USER 'app_user'@'%' IDENTIFIED WITH mysql_native_password BY 'app_password';-- 权限管理GRANT SELECT ON company.* TO 'report_user'@'192.168.1.%';GRANT SELECT, INSERT, UPDATE, DELETE ON company.employees TO 'app_user'@'%';GRANT EXECUTE ON PROCEDURE company.CalculateDepartmentStats TO 'report_user'@'192.168.1.%';-- 角色管理(MySQL 8.0+)CREATE ROLE data_reader;GRANT SELECT ON company.* TO data_reader;GRANT data_reader TO 'report_user'@'192.168.1.%';-- 权限回收REVOKE DELETE ON company.employees FROM 'app_user'@'%';-- 查看权限SHOW GRANTS FOR 'report_user'@'192.168.1.%';

事务控制语言(TCL) - 事务管理:

-- 基本事务START TRANSACTION;INSERT INTO accounts (account_id, balance) VALUES (1, 1000.00);INSERT INTO accounts (account_id, balance) VALUES (2, 2000.00);COMMIT;-- 复杂事务控制START TRANSACTION;SAVEPOINT before_transfer;UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;-- 检查约束SELECT balance INTO @bal FROM accounts WHERE account_id = 1 FOR UPDATE;IF @bal < 0 THEN    ROLLBACK TO SAVEPOINT before_transfer;    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';END IF;UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;COMMIT;

复杂查询:子查询、连接查询、联合查询

子查询深度应用:

-- 标量子查询(返回单个值)SELECT     emp_name,    salary,    (SELECT AVG(salary) FROM employees) as avg_salary,    salary - (SELECT AVG(salary) FROM employees) as diff_from_avgFROM employeesWHERE salary > (SELECT AVG(salary) FROM employees);-- 列子查询(返回一列)SELECT     dept_nameFROM departmentsWHERE dept_id IN (    SELECT DISTINCT dept_id     FROM employees     WHERE salary > 10000);-- 行子查询(返回一行)SELECT     emp_name,    salaryFROM employeesWHERE (salary, dept_id) = (    SELECT MAX(salary), dept_id    FROM employees    WHERE dept_id = 1);-- 表子查询(在FROM中)SELECT     dept_stats.dept_name,    dept_stats.avg_salary,    dept_stats.employee_countFROM (    SELECT         d.dept_name,        AVG(e.salary) as avg_salary,        COUNT(e.emp_id) as employee_count    FROM departments d    LEFT JOIN employees e ON d.dept_id = e.dept_id    GROUP BY d.dept_id, d.dept_name) dept_statsWHERE dept_stats.avg_salary > 8000;-- 关联子查询SELECT     e1.emp_name,    e1.salary,    e1.dept_idFROM employees e1WHERE e1.salary > (    SELECT AVG(e2.salary)    FROM employees e2    WHERE e2.dept_id = e1.dept_id  -- 关联外部查询);-- EXISTS子查询SELECT     d.dept_nameFROM departments dWHERE EXISTS (    SELECT 1    FROM employees e    WHERE e.dept_id = d.dept_id      AND e.salary > 15000);

高级连接查询:

-- 内连接(INNER JOIN)SELECT     e.emp_name,    d.dept_name,    p.project_nameFROM employees eINNER JOIN departments d ON e.dept_id = d.dept_idINNER JOIN projects p ON e.dept_id = p.dept_idWHERE p.status = 'active';-- 左外连接(LEFT JOIN)SELECT     d.dept_name,    COUNT(e.emp_id) as employee_countFROM departments dLEFT JOIN employees e ON d.dept_id = e.dept_idGROUP BY d.dept_id, d.dept_name;-- 右外连接(RIGHT JOIN)SELECT     e.emp_name,    p.project_nameFROM employees eRIGHT JOIN project_assignments pa ON e.emp_id = pa.emp_idRIGHT JOIN projects p ON pa.project_id = p.project_id;-- 全外连接模拟(UNION + LEFT/RIGHT JOIN)SELECT     e.emp_name,    d.dept_nameFROM employees eLEFT JOIN departments d ON e.dept_id = d.dept_idUNIONSELECT     e.emp_name,    d.dept_nameFROM employees eRIGHT JOIN departments d ON e.dept_id = d.dept_id;-- 自连接(查询员工和经理)SELECT     emp.emp_name as employee_name,    mgr.emp_name as manager_nameFROM employees empLEFT JOIN employees mgr ON emp.manager_id = mgr.emp_id;-- 交叉连接(CROSS JOIN)SELECT     e.emp_name,    p.project_nameFROM employees eCROSS JOIN projects pWHERE e.dept_id = p.dept_id;-- 自然连接(NATURAL JOIN)- 不推荐在生产环境使用SELECT     emp_name,    dept_nameFROM employeesNATURAL JOIN departments;

联合查询与集合操作:

-- UNION(去重)SELECT     emp_name as name,    'employee' as type,    salaryFROM employeesWHERE salary > 8000UNIONSELECT     dept_name as name,    'department' as type,    NULL as salaryFROM departmentsWHERE budget > 100000;-- UNION ALL(不去重)SELECT     emp_name,    dept_idFROM employeesWHERE hire_date >= '2023-01-01'UNION ALLSELECT     emp_name,    dept_idFROM former_employeesWHERE leave_date >= '2023-01-01';-- INTERSECT模拟(MySQL 8.0.31+ 直接支持)SELECT emp_nameFROM employeesWHERE dept_id = 1INTERSECTSELECT emp_nameFROM employeesWHERE salary > 8000;-- 在旧版本中模拟INTERSECTSELECT DISTINCT e1.emp_nameFROM employees e1INNER JOIN employees e2 ON e1.emp_name = e2.emp_nameWHERE e1.dept_id = 1 AND e2.salary > 8000;-- EXCEPT/MINUS模拟SELECT emp_nameFROM employeesWHERE dept_id = 1EXCEPTSELECT emp_nameFROM employeesWHERE salary <= 8000;-- 在旧版本中模拟EXCEPTSELECT e1.emp_nameFROM employees e1LEFT JOIN employees e2 ON e1.emp_name = e2.emp_name AND e2.salary <= 8000WHERE e1.dept_id = 1 AND e2.emp_name IS NULL;

窗口函数:排名、分组、累计计算

排名窗口函数:

-- 基本排名SELECT     emp_name,    salary,    dept_id,    ROW_NUMBER() OVER (ORDER BY salary DESC) as rank_all,    RANK() OVER (ORDER BY salary DESC) as rank_with_ties,    DENSE_RANK() OVER (ORDER BY salary DESC) as dense_rank_no_gapsFROM employees;-- 分区排名SELECT     emp_name,    salary,    dept_id,    ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) as dept_rank,    RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) as dept_rank_with_tiesFROM employees;-- 前N名查询WITH ranked_employees AS (    SELECT         emp_name,        salary,        dept_id,        ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) as rn    FROM employees)SELECT *FROM ranked_employeesWHERE rn <= 3;  -- 每个部门前3名

聚合窗口函数:

-- 累计计算SELECT     emp_name,    hire_date,    salary,    SUM(salary) OVER (        ORDER BY hire_date         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW    ) as running_total,        AVG(salary) OVER (        PARTITION BY dept_id        ORDER BY hire_date        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW    ) as moving_avg_3,        SUM(salary) OVER (        PARTITION BY dept_id    ) as dept_total_salaryFROM employeesORDER BY hire_date;-- 前后值访问SELECT     emp_name,    hire_date,    salary,    LAG(salary, 1) OVER (ORDER BY hire_date) as prev_salary,    LEAD(salary, 1) OVER (ORDER BY hire_date) as next_salary,    salary - LAG(salary, 1) OVER (ORDER BY hire_date) as salary_changeFROM employees;-- 首尾值访问SELECT     emp_name,    dept_id,    salary,    FIRST_VALUE(salary) OVER (        PARTITION BY dept_id         ORDER BY salary DESC    ) as highest_in_dept,        LAST_VALUE(salary) OVER (        PARTITION BY dept_id         ORDER BY salary DESC        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING    ) as lowest_in_deptFROM employees;

窗口帧详解:

-- 不同的窗口帧定义SELECT     emp_name,    hire_date,    salary,    -- 从开始到当前行    SUM(salary) OVER (        ORDER BY hire_date        ROWS UNBOUNDED PRECEDING    ) as running_total,        -- 最近3行(包括当前行)    AVG(salary) OVER (        ORDER BY hire_date        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW    ) as moving_avg_3,        -- 前后各1行    AVG(salary) OVER (        ORDER BY hire_date        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING    ) as centered_avg,        -- 分组内所有行    AVG(salary) OVER (        PARTITION BY dept_id    ) as dept_avgFROM employeesORDER BY hire_date;

公用表表达式(CTE)与递归查询

普通CTE:

-- 简单CTEWITH department_stats AS (    SELECT         dept_id,        COUNT(*) as employee_count,        AVG(salary) as avg_salary,        MAX(salary) as max_salary    FROM employees    GROUP BY dept_id),high_paid_employees AS (    SELECT         e.emp_name,        e.salary,        d.dept_name    FROM employees e    JOIN departments d ON e.dept_id = d.dept_id    JOIN department_stats ds ON e.dept_id = ds.dept_id    WHERE e.salary > ds.avg_salary * 1.2)SELECT     dept_name,    COUNT(*) as high_paid_count,    AVG(salary) as avg_high_salaryFROM high_paid_employeesGROUP BY dept_nameORDER BY high_paid_count DESC;-- 多CTE链式使用WITH employee_data AS (    SELECT         emp_id,        emp_name,        salary,        dept_id    FROM employees    WHERE status = 'active'),department_data AS (    SELECT         dept_id,        dept_name,        budget    FROM departments),combined_data AS (    SELECT         e.emp_name,        e.salary,        d.dept_name,        d.budget    FROM employee_data e    JOIN department_data d ON e.dept_id = d.dept_id)SELECT     dept_name,    AVG(salary) as avg_salary,    SUM(salary) / budget as salary_budget_ratioFROM combined_dataGROUP BY dept_name, budgetHAVING salary_budget_ratio < 0.8;

递归CTE:

-- 组织结构递归查询CREATE TABLE organization (    emp_id INT PRIMARY KEY,    emp_name VARCHAR(100),    manager_id INT,    title VARCHAR(100));-- 递归CTE查询完整汇报链WITH RECURSIVE employee_hierarchy AS (    -- 锚点:顶级管理者(没有经理)    SELECT         emp_id,        emp_name,        manager_id,        title,        0 as level,        CAST(emp_name AS CHAR(1000)) as hierarchy_path    FROM organization    WHERE manager_id IS NULL        UNION ALL        -- 递归部分:下属员工    SELECT         o.emp_id,        o.emp_name,        o.manager_id,        o.title,        eh.level + 1,        CONCAT(eh.hierarchy_path, ' -> ', o.emp_name)    FROM organization o    JOIN employee_hierarchy eh ON o.manager_id = eh.emp_id)SELECT     emp_id,    emp_name,    title,    level,    hierarchy_pathFROM employee_hierarchyORDER BY hierarchy_path;-- 数字序列生成WITH RECURSIVE number_sequence AS (    SELECT 1 as num    UNION ALL    SELECT num + 1    FROM number_sequence    WHERE num < 100)SELECT num FROM number_sequence;-- 日期序列生成WITH RECURSIVE date_sequence AS (    SELECT '2023-01-01' as date_val    UNION ALL    SELECT date_val + INTERVAL 1 DAY    FROM date_sequence    WHERE date_val < '2023-01-31')SELECT     date_val,    DAYNAME(date_val) as day_nameFROM date_sequence;

JSON函数与空间数据查询

JSON函数深度应用:

-- JSON创建函数SELECT     emp_name,    salary,    JSON_OBJECT(        'name', emp_name,        'salary', salary,        'department', dept_id,        'hire_year', YEAR(hire_date)    ) as emp_jsonFROM employeesLIMIT 5;-- JSON数组操作SELECT     dept_id,    JSON_ARRAYAGG(        JSON_OBJECT(            'name', emp_name,            'salary', salary        )    ) as employees_jsonFROM employeesGROUP BY dept_id;-- JSON查询函数SELECT     emp_name,    JSON_EXTRACT(profile, '$.contact.email') as email,    profile->>'$.contact.phone' as phone,  -- 简写形式    JSON_UNQUOTE(JSON_EXTRACT(profile, '$.address.city')) as cityFROM employeesWHERE JSON_CONTAINS_PATH(profile, 'one', '$.skills')   AND JSON_LENGTH(profile->'$.skills') >= 3;-- JSON修改函数UPDATE employees SET profile = JSON_SET(    profile,    '$.last_updated', CURRENT_TIMESTAMP,    '$.contact.phone', '+86-13800138000')WHERE emp_id = 1001;-- JSON搜索和索引SELECT     emp_name,    profile->>'$.title' as job_titleFROM employeesWHERE JSON_SEARCH(profile, 'one', '%经理%') IS NOT NULL;-- 创建JSON索引(MySQL 8.0.17+)CREATE TABLE products (    id INT PRIMARY KEY AUTO_INCREMENT,    product_data JSON,        -- 函数索引    INDEX idx_product_name ((CAST(product_data->>'$.name' AS CHAR(100)))),    INDEX idx_product_price ((CAST(product_data->>'$.price' AS DECIMAL(10,2)))));

空间数据查询:

-- 空间数据创建和查询CREATE TABLE locations (    location_id INT PRIMARY KEY AUTO_INCREMENT,    location_name VARCHAR(100),    coordinates POINT NOT NULL,    area_boundary POLYGON,    SPATIAL INDEX idx_coordinates (coordinates),    SPATIAL INDEX idx_area (area_boundary));-- 插入空间数据INSERT INTO locations (location_name, coordinates, area_boundary)VALUES (    '公司总部',    ST_GeomFromText('POINT(116.3974 39.9093)'),    ST_GeomFromText('POLYGON((116.396 39.908, 116.398 39.908, 116.398 39.910, 116.396 39.910, 116.396 39.908))'));-- 空间查询SELECT     location_name,    ST_AsText(coordinates) as coordinates,    ST_X(coordinates) as longitude,    ST_Y(coordinates) as latitudeFROM locations;-- 距离计算SELECT     l1.location_name as place1,    l2.location_name as place2,    ST_Distance_Sphere(l1.coordinates, l2.coordinates) as distance_metersFROM locations l1CROSS JOIN locations l2WHERE l1.location_id != l2.location_id;-- 包含查询SELECT     location_nameFROM locationsWHERE ST_Contains(    area_boundary,     ST_GeomFromText('POINT(116.3974 39.9093)'));-- 缓冲区查询SELECT     location_name,    ST_AsText(ST_Buffer(coordinates, 1000)) as buffer_zone  -- 1000米缓冲区FROM locations;

2. 存储过程与函数

存储过程编写与调试

基础存储过程:

-- 创建存储过程DELIMITER //CREATE PROCEDURE GetEmployeeStatistics(    IN p_dept_id INT,    OUT p_employee_count INT,    OUT p_avg_salary DECIMAL(10,2),    OUT p_max_salary DECIMAL(10,2))BEGIN    -- 声明局部变量    DECLARE v_total_budget DECIMAL(12,2);        -- 业务逻辑    SELECT         COUNT(*),        AVG(salary),        MAX(salary)    INTO         p_employee_count,        p_avg_salary,        p_max_salary    FROM employees    WHERE dept_id = p_dept_id      AND status = 'active';        -- 调试信息(在生产环境可注释)    SELECT CONCAT('部门 ', p_dept_id, ' 统计完成') as debug_info;    END //DELIMITER ;-- 调用存储过程CALL GetEmployeeStatistics(1, @emp_count, @avg_sal, @max_sal);SELECT @emp_count, @avg_sal, @max_sal;

带条件逻辑的存储过程:

DELIMITER //CREATE PROCEDURE UpdateEmployeeSalary(    IN p_emp_id INT,    IN p_increase_percent DECIMAL(5,2),    OUT p_result VARCHAR(500))BEGIN    DECLARE v_current_salary DECIMAL(10,2);    DECLARE v_new_salary DECIMAL(10,2);    DECLARE v_emp_name VARCHAR(100);    DECLARE EXIT HANDLER FOR SQLEXCEPTION    BEGIN        GET DIAGNOSTICS CONDITION 1            @sqlstate = RETURNED_SQLSTATE,            @errno = MYSQL_ERRNO,            @text = MESSAGE_TEXT;        SET p_result = CONCAT('错误: ', @errno, ' - ', @text);        ROLLBACK;    END;        START TRANSACTION;        -- 获取当前薪资    SELECT emp_name, salary     INTO v_emp_name, v_current_salary    FROM employees     WHERE emp_id = p_emp_id    FOR UPDATE;  -- 加锁防止并发更新        IF v_current_salary IS NULL THEN        SET p_result = CONCAT('员工ID ', p_emp_id, ' 不存在');        ROLLBACK;    ELSE        -- 计算新薪资        SET v_new_salary = v_current_salary * (1 + p_increase_percent / 100);                -- 更新薪资        UPDATE employees         SET salary = v_new_salary,            updated_at = CURRENT_TIMESTAMP        WHERE emp_id = p_emp_id;                -- 记录薪资变更历史        INSERT INTO salary_history (emp_id, old_salary, new_salary, change_date, change_reason)        VALUES (p_emp_id, v_current_salary, v_new_salary, NOW(), '年度调薪');                SET p_result = CONCAT(            '员工 ', v_emp_name,             ' 薪资从 ', v_current_salary,             ' 调整为 ', v_new_salary,            ' (涨幅 ', p_increase_percent, '%)'        );                COMMIT;    END IF;    END //DELIMITER ;

游标使用:

DELIMITER //CREATE PROCEDURE ProcessDepartmentSalaries(IN p_dept_id INT)BEGIN    DECLARE v_done INT DEFAULT 0;    DECLARE v_emp_id INT;    DECLARE v_emp_name VARCHAR(100);    DECLARE v_current_salary DECIMAL(10,2);    DECLARE v_new_salary DECIMAL(10,2);        -- 声明游标    DECLARE emp_cursor CURSOR FOR        SELECT emp_id, emp_name, salary        FROM employees        WHERE dept_id = p_dept_id          AND status = 'active';        -- 声明结束处理程序    DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;        -- 创建临时表存储结果    CREATE TEMPORARY TABLE IF NOT EXISTS salary_adjustments (        emp_id INT,        emp_name VARCHAR(100),        old_salary DECIMAL(10,2),        new_salary DECIMAL(10,2),        increase_amount DECIMAL(10,2)    );        OPEN emp_cursor;        emp_loop: LOOP        FETCH emp_cursor INTO v_emp_id, v_emp_name, v_current_salary;        IF v_done THEN            LEAVE emp_loop;        END IF;                -- 业务逻辑:根据规则调整薪资        IF v_current_salary < 5000 THEN            SET v_new_salary = v_current_salary * 1.15;  -- 低薪员工涨15%        ELSEIF v_current_salary BETWEEN 5000 AND 10000 THEN            SET v_new_salary = v_current_salary * 1.10;  -- 中等薪资涨10%        ELSE            SET v_new_salary = v_current_salary * 1.05;  -- 高薪员工涨5%        END IF;                -- 更新薪资        UPDATE employees         SET salary = v_new_salary        WHERE emp_id = v_emp_id;                -- 记录调整结果        INSERT INTO salary_adjustments         VALUES (v_emp_id, v_emp_name, v_current_salary, v_new_salary, v_new_salary - v_current_salary);            END LOOP;        CLOSE emp_cursor;        -- 返回处理结果    SELECT * FROM salary_adjustments;        DROP TEMPORARY TABLE salary_adjustments;    END //DELIMITER ;

自定义函数开发

标量函数:

DELIMITER //CREATE FUNCTION CalculateTax(    p_salary DECIMAL(10,2),    p_tax_rate DECIMAL(5,3)) RETURNS DECIMAL(10,2)DETERMINISTICREADS SQL DATABEGIN    DECLARE v_tax_amount DECIMAL(10,2);        -- 计算税费(简单的线性计算)    SET v_tax_amount = p_salary * p_tax_rate;        -- 确保不为负数    IF v_tax_amount < 0 THEN        SET v_tax_amount = 0;    END IF;        RETURN v_tax_amount;END //DELIMITER ;-- 使用自定义函数SELECT     emp_name,    salary,    CalculateTax(salary, 0.1) as tax_amount,    salary - CalculateTax(salary, 0.1) as net_salaryFROM employees;

字符串处理函数:

DELIMITER //CREATE FUNCTION FormatPhoneNumber(    p_phone VARCHAR(20))RETURNS VARCHAR(20)DETERMINISTICBEGIN    DECLARE v_clean_phone VARCHAR(20);        -- 移除所有非数字字符    SET v_clean_phone = REGEXP_REPLACE(p_phone, '[^0-9]', '');        -- 格式化手机号    IF LENGTH(v_clean_phone) = 11 THEN        RETURN CONCAT(            SUBSTR(v_clean_phone, 1, 3), '-',            SUBSTR(v_clean_phone, 4, 4), '-',            SUBSTR(v_clean_phone, 8, 4)        );    ELSE        RETURN p_phone;  -- 无法格式化,返回原值    END IF;END //DELIMITER ;

复杂业务逻辑函数:

DELIMITER //CREATE FUNCTION GetEmployeeLevel(    p_salary DECIMAL(10,2),    p_hire_date DATE,    p_performance_rating INT)RETURNS VARCHAR(20)DETERMINISTICBEGIN    DECLARE v_years_worked INT;    DECLARE v_level_score DECIMAL(5,2);        -- 计算工作年限    SET v_years_worked = TIMESTAMPDIFF(YEAR, p_hire_date, CURDATE());        -- 计算级别分数(薪资权重40%,年限权重30%,绩效权重30%)    SET v_level_score =         (p_salary / 10000) * 0.4 +          -- 每万元0.4分        (v_years_worked * 0.3) +            -- 每年0.3分        (p_performance_rating * 0.3);       -- 绩效评分权重        -- 根据分数确定级别    RETURN CASE         WHEN v_level_score >= 8 THEN '专家'        WHEN v_level_score >= 6 THEN '高级'        WHEN v_level_score >= 4 THEN '中级'        ELSE '初级'    END;    END //DELIMITER ;

触发器设计与应用场景

审计触发器:

-- 创建审计表CREATE TABLE employee_audit (    audit_id INT AUTO_INCREMENT PRIMARY KEY,    action_type ENUM('INSERT', 'UPDATE', 'DELETE'),    emp_id INT,    old_data JSON,    new_data JSON,    changed_by VARCHAR(100),    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);-- 员工表更新触发器DELIMITER //CREATE TRIGGER before_employee_updateBEFORE UPDATE ON employeesFOR EACH ROWBEGIN    DECLARE v_changes JSON DEFAULT JSON_OBJECT();        -- 检查哪些字段被修改了    IF OLD.emp_name != NEW.emp_name THEN        SET v_changes = JSON_SET(v_changes, '$.emp_name', JSON_OBJECT(            'old', OLD.emp_name,            'new', NEW.emp_name        ));    END IF;        IF OLD.salary != NEW.salary THEN        SET v_changes = JSON_SET(v_changes, '$.salary', JSON_OBJECT(            'old', OLD.salary,            'new', NEW.salary        ));    END IF;        IF OLD.dept_id != NEW.dept_id THEN        SET v_changes = JSON_SET(v_changes, '$.dept_id', JSON_OBJECT(            'old', OLD.dept_id,            'new', NEW.dept_id        ));    END IF;        -- 如果有变化,记录审计日志    IF JSON_LENGTH(v_changes) > 0 THEN        INSERT INTO employee_audit (action_type, emp_id, old_data, new_data, changed_by)        VALUES (            'UPDATE',            OLD.emp_id,            JSON_OBJECT(                'emp_name', OLD.emp_name,                'salary', OLD.salary,                'dept_id', OLD.dept_id            ),            v_changes,            USER()        );    END IF;    END //DELIMITER ;

数据一致性触发器:

-- 部门预算检查触发器DELIMITER //CREATE TRIGGER before_department_updateBEFORE UPDATE ON departmentsFOR EACH ROWBEGIN    DECLARE v_total_salary DECIMAL(12,2);        -- 计算部门总薪资    SELECT COALESCE(SUM(salary), 0)    INTO v_total_salary    FROM employees    WHERE dept_id = NEW.dept_id      AND status = 'active';        -- 检查预算是否足够    IF NEW.budget < v_total_salary THEN        SIGNAL SQLSTATE '45000'         SET MESSAGE_TEXT = '部门预算不能低于员工总薪资';    END IF;    END //DELIMITER ;

派生数据触发器:

-- 维护部门统计信息的触发器DELIMITER //CREATE TRIGGER after_employee_changeAFTER INSERT OR UPDATE OR DELETE ON employeesFOR EACH ROWBEGIN    DECLARE affected_dept_id INT;        -- 确定受影响的部门    IF INSERTING THEN        SET affected_dept_id = NEW.dept_id;    ELSEIF UPDATING THEN        -- 如果部门变更,两个部门都受影响        IF OLD.dept_id != NEW.dept_id THEN            CALL UpdateDepartmentStats(OLD.dept_id);            SET affected_dept_id = NEW.dept_id;        ELSE            SET affected_dept_id = NEW.dept_id;        END IF;    ELSE  -- DELETING        SET affected_dept_id = OLD.dept_id;    END IF;        -- 更新部门统计    CALL UpdateDepartmentStats(affected_dept_id);    END //DELIMITER ;

事件调度器实现定时任务

基础事件调度:

-- 启用事件调度器SET GLOBAL event_scheduler = ON;-- 创建每日统计事件DELIMITER //CREATE EVENT daily_department_statsON SCHEDULE EVERY 1 DAYSTARTS '2023-01-01 02:00:00'COMMENT '每日部门统计'DOBEGIN    -- 防止事件重叠执行    DECLARE EXIT HANDLER FOR SQLEXCEPTION    BEGIN        INSERT INTO event_errors (event_name, error_message, occurred_at)        VALUES ('daily_department_stats', '执行失败', NOW());    END;        -- 更新部门统计表    REPLACE INTO department_daily_stats (stat_date, dept_id, employee_count, total_salary, avg_salary)    SELECT         CURDATE() as stat_date,        dept_id,        COUNT(*) as employee_count,        SUM(salary) as total_salary,        AVG(salary) as avg_salary    FROM employees    WHERE status = 'active'    GROUP BY dept_id;        -- 记录执行日志    INSERT INTO event_logs (event_name, executed_at, records_affected)    VALUES ('daily_department_stats', NOW(), ROW_COUNT());    END //DELIMITER ;

复杂定时任务:

DELIMITER //CREATE EVENT monthly_salary_reportON SCHEDULE     EVERY 1 MONTH    STARTS TIMESTAMP(DATE_FORMAT(NOW() + INTERVAL 1 MONTH, '%Y-%m-01 03:00:00'))COMMENT '月度薪资报告'DOBEGIN    DECLARE v_report_month DATE;    DECLARE v_previous_month DATE;        -- 设置报告月份(上个月)    SET v_report_month = DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-01');    SET v_previous_month = v_report_month - INTERVAL 1 MONTH;        -- 创建月度薪资报告    INSERT INTO monthly_salary_reports (report_month, dept_id, employee_count, total_salary, avg_salary, salary_growth)    SELECT         v_report_month as report_month,        dept_id,        COUNT(*) as employee_count,        SUM(salary) as total_salary,        AVG(salary) as avg_salary,        -- 计算薪资增长率        (AVG(salary) - COALESCE(            (SELECT avg_salary              FROM monthly_salary_reports              WHERE report_month = v_previous_month                AND dept_id = e.dept_id),            AVG(salary)        )) / COALESCE(            (SELECT avg_salary              FROM monthly_salary_reports              WHERE report_month = v_previous_month                AND dept_id = e.dept_id),            AVG(salary)        ) * 100 as salary_growth_percent    FROM employees e    WHERE status = 'active'    GROUP BY dept_id;        -- 生成高管报告    INSERT INTO executive_reports (report_month, report_type, report_data)    SELECT         v_report_month,        'salary_analysis',        JSON_OBJECT(            'total_employees', (SELECT COUNT(*) FROM employees WHERE status = 'active'),            'total_payroll', (SELECT SUM(salary) FROM employees WHERE status = 'active'),            'department_breakdown', (                SELECT JSON_ARRAYAGG(                    JSON_OBJECT(                        'dept_id', dept_id,                        'dept_name', dept_name,                        'employee_count', employee_count,                        'avg_salary', avg_salary                    )                )                FROM monthly_salary_reports                WHERE report_month = v_report_month            )        )    FROM dual;    END //DELIMITER ;

事件管理:

-- 查看事件状态SHOW EVENTS;-- 查看事件定义SHOW CREATE EVENT monthly_salary_report;-- 修改事件ALTER EVENT monthly_salary_reportON SCHEDULE EVERY 1 MONTHSTARTS CURRENT_TIMESTAMP + INTERVAL 1 DAYENABLE;-- 暂停事件ALTER EVENT monthly_salary_report DISABLE;-- 删除事件DROP EVENT IF EXISTS monthly_salary_report;-- 事件监控SELECT     event_schema as database_name,    event_name,    definer,    time_zone,    event_definition as sql_code,    execute_at,    interval_value,    interval_field,    created,    last_altered,    statusFROM information_schema.eventsWHERE event_schema = 'company';

3. 事务与并发控制

ACID特性深度理解

原子性(Atomicity)实现:

-- 银行转账事务 - 原子性示例START TRANSACTION;-- 检查账户余额SELECT balance INTO @current_balance FROM accounts WHERE account_id = 123 FOR UPDATE;IF @current_balance < 1000 THEN    -- 余额不足,回滚事务    ROLLBACK;    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';END IF;-- 扣款UPDATE accounts SET balance = balance - 1000 WHERE account_id = 123;-- 存款UPDATE accounts SET balance = balance + 1000 WHERE account_id = 456;-- 记录交易INSERT INTO transactions (from_account, to_account, amount, transaction_time)VALUES (123, 456, 1000, NOW());COMMIT;

一致性(Consistency)保证:

-- 使用约束保证一致性CREATE TABLE orders (    order_id INT AUTO_INCREMENT PRIMARY KEY,    customer_id INT NOT NULL,    order_amount DECIMAL(10,2) NOT NULL CHECK (order_amount > 0),    order_date DATE NOT NULL,    status ENUM('pending', 'confirmed', 'shipped', 'delivered') DEFAULT 'pending',        -- 外键约束    FOREIGN KEY (customer_id) REFERENCES customers(customer_id),        -- 检查约束(MySQL 8.0.16+)    CONSTRAINT chk_order_date CHECK (order_date >= '2020-01-01'));-- 事务中的一致性检查START TRANSACTION;-- 业务逻辑检查SELECT COUNT(*) INTO @active_productsFROM products WHERE product_id IN (SELECT product_id FROM order_items WHERE order_id = 1001)  AND status = 'active';IF @active_products = 0 THEN    ROLLBACK;    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单中没有有效商品';END IF;-- 继续其他操作...COMMIT;

事务隔离级别与实现原理

隔离级别对比:

-- 查看当前隔离级别SELECT @@transaction_isolation;-- 设置会话隔离级别SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 不同隔离级别的现象演示-- 1. 读未提交(READ UNCOMMITTED) - 脏读-- 事务ASET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;START TRANSACTION;UPDATE accounts SET balance = 2000 WHERE account_id = 1;  -- 未提交-- 事务B(在另一个连接中)SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;START TRANSACTION;SELECT balance FROM accounts WHERE account_id = 1;  -- 可能读到2000(脏读)-- 2. 读已提交(READ COMMITTED) - 避免脏读,但不可重复读SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 3. 可重复读(REPEATABLE READ) - MySQL默认级别SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;-- 4. 串行化(SERIALIZABLE) - 最高隔离级别SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

隔离级别实战:

-- 可重复读级别下的幻读问题-- 事务ASTART TRANSACTION;SELECT COUNT(*) FROM employees WHERE salary > 8000;  -- 假设返回10-- 事务B(在另一个连接中插入新员工)START TRANSACTION;INSERT INTO employees (emp_name, salary, dept_id) VALUES ('新员工', 9000, 1);COMMIT;-- 事务A再次查询SELECT COUNT(*) FROM employees WHERE salary > 8000;  -- 在REPEATABLE READ中仍然返回10-- 串行化级别解决幻读SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;START TRANSACTION;SELECT COUNT(*) FROM employees WHERE salary > 8000;  -- 加锁,阻止其他事务插入-- 事务B的插入会被阻塞,直到事务A提交

MVCC多版本并发控制机制

MVCC原理演示:

-- 查看InnoDB事务信息SELECT * FROM information_schema.INNODB_TRX;-- MVCC示例-- 事务ASTART TRANSACTION;SELECT * FROM employees WHERE emp_id = 1;  -- 读取当前版本-- 事务B修改同一条记录START TRANSACTION;UPDATE employees SET salary = salary + 1000 WHERE emp_id = 1;COMMIT;-- 事务A再次读取(REPEATABLE READ级别下看到的是旧版本)SELECT * FROM employees WHERE emp_id = 1;  -- 薪资未变化-- 提交事务A后看到新版本COMMIT;SELECT * FROM employees WHERE emp_id = 1;  -- 看到更新后的薪资

MVCC与Undo Log:

-- Undo Log维护多版本数据-- 当执行UPDATE时:-- 1. 将旧数据复制到Undo Log-- 2. 修改当前数据-- 3. 更新DB_ROLL_PTR指向Undo Log中的旧版本-- 长事务对Undo Log的影响SELECT     t.trx_id,    t.trx_started,    TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) as duration_seconds,    t.trx_state,    t.trx_operation_stateFROM information_schema.INNODB_TRX tORDER BY t.trx_started;-- 监控Undo Log使用SHOW ENGINE INNODB STATUS;

死锁检测与避免策略

死锁场景分析:

-- 死锁示例-- 事务1START TRANSACTION;UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;  -- 锁住账户1-- 事务2START TRANSACTION;UPDATE accounts SET balance = balance - 200 WHERE account_id = 2;  -- 锁住账户2-- 事务1尝试锁住账户2UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;  -- 等待事务2释放锁-- 事务2尝试锁住账户1UPDATE accounts SET balance = balance + 200 WHERE account_id = 1;  -- 等待事务1释放锁,死锁!-- InnoDB会自动检测到死锁并回滚其中一个事务

死锁避免策略:

-- 1. 按固定顺序访问资源-- 好的做法:总是先访问ID小的账户START TRANSACTION;UPDATE accounts SET balance = balance - 100 WHERE account_id = LEAST(1, 2);UPDATE accounts SET balance = balance + 100 WHERE account_id = GREATEST(1, 2);COMMIT;-- 2. 使用锁超时SET SESSION innodb_lock_wait_timeout = 10;  -- 设置锁等待超时为10秒-- 3. 使用NOWAIT和SKIP LOCKED(MySQL 8.0+)START TRANSACTION;SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE NOWAIT;  -- 如果锁被占用立即报错SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE SKIP LOCKED;  -- 跳过被锁定的行-- 4. 减少事务大小和时间START TRANSACTION;-- 尽快完成事务,减少锁持有时间UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;COMMIT;  -- 立即提交-- 死锁信息分析SHOW ENGINE INNODB STATUS;  -- 查看最近的死锁信息

分布式事务(XA事务)实战

XA事务基础:

-- XA事务示例(跨多个数据库)-- 第一阶段:准备阶段XA START 'xid1';  -- 开始XA事务UPDATE accounts SET balance = balance - 1000 WHERE account_id = 123;UPDATE accounts SET balance = balance + 1000 WHERE account_id = 456;XA END 'xid1';XA PREPARE 'xid1';  -- 准备提交-- 第二阶段:提交阶段XA COMMIT 'xid1';   -- 提交事务-- 或者回滚-- XA ROLLBACK 'xid1';-- 恢复中断的XA事务XA RECOVER;  -- 查看PREPARED状态的XA事务-- 对于PREPARED状态的事务,可以决定提交或回滚XA COMMIT 'xid_recovered';-- 或者 XA ROLLBACK 'xid_recovered';

分布式事务管理:

-- 监控XA事务SELECT * FROM performance_schema.events_transactions_currentWHERE STATE = 'PREPARED';-- XA事务错误处理DELIMITER //CREATE PROCEDURE DistributedTransfer(    IN p_from_account INT,    IN p_to_account INT,     IN p_amount DECIMAL(10,2))BEGIN    DECLARE EXIT HANDLER FOR SQLEXCEPTION    BEGIN        -- 回滚XA事务        XA END 'transfer_xid';        XA ROLLBACK 'transfer_xid';        RESIGNAL;    END;        -- 开始XA事务    XA START 'transfer_xid';        -- 业务操作    UPDATE accounts SET balance = balance - p_amount     WHERE account_id = p_from_account;        UPDATE accounts SET balance = balance + p_amount     WHERE account_id = p_to_account;        -- 结束并准备    XA END 'transfer_xid';    XA PREPARE 'transfer_xid';        -- 检查所有参与者是否准备成功    -- 这里可以添加其他数据库的检查逻辑        -- 提交事务    XA COMMIT 'transfer_xid';    END //DELIMITER ;

总结

通过本篇的深入学习,我们掌握了MySQL SQL编程的核心高级特性:

  1. 复杂查询能力:子查询、连接查询、窗口函数、CTE递归查询
  2. 存储程序开发:存储过程、函数、触发器、事件调度器
  3. 事务管理:ACID特性、隔离级别、MVCC机制、死锁处理
  4. 高级数据类型:JSON处理、空间数据查询
  5. 分布式事务:XA事务管理和恢复

关键收获:

  • 窗口函数可以优雅地解决复杂的分析需求
  • 存储过程和函数封装了业务逻辑,提高代码复用性
  • 合理使用事务隔离级别可以平衡性能和数据一致性
  • MVCC机制是MySQL高并发的基础
  • 分布式事务保证了跨数据库操作的一致性

这些高级特性使得MySQL能够处理复杂的企业级应用场景,为构建高性能、高可用的系统提供了坚实基础。

动手练习:

  1. 使用窗口函数分析你业务数据的排名和趋势
  2. 编写存储过程实现复杂的业务逻辑
  3. 设计触发器实现数据变更的审计跟踪
  4. 配置事件调度器实现定时数据维护任务
  5. 测试不同事务隔离级别对并发性能的影响

欢迎在评论区分享你的SQL编程经验和遇到的问题!

🔲 ☆

MySql入门:性能优化与调优

MySQL性能优化与调优

数据库性能是系统整体性能的基石。一个优化良好的MySQL数据库可以轻松应对高并发场景,而配置不当的数据库则可能成为整个系统的瓶颈。今天,我们将深入探讨MySQL性能优化的各个方面,从查询优化到服务器配置,帮助你构建高性能的数据库系统。

1. 查询性能优化

EXPLAIN执行计划深度解读

EXPLAIN输出详解:

-- 基本EXPLAIN使用EXPLAIN SELECT e.emp_name, d.dept_name, p.project_nameFROM employees eJOIN departments d ON e.dept_id = d.dept_idJOIN projects p ON e.emp_id = p.manager_idWHERE e.salary > 5000  AND d.location = '北京'ORDER BY e.hire_date DESCLIMIT 100;-- EXPLAIN输出字段解析CREATE TABLE explain_analysis (    id INT PRIMARY KEY,    select_type VARCHAR(50) COMMENT '查询类型',    table_name VARCHAR(64) COMMENT '表名',    partitions VARCHAR(255) COMMENT '匹配的分区',    type VARCHAR(30) COMMENT '连接类型',    possible_keys VARCHAR(255) COMMENT '可能使用的索引',    key VARCHAR(255) COMMENT '实际使用的索引',    key_len INT COMMENT '使用的索引长度',    ref VARCHAR(255) COMMENT '与索引比较的列',    rows INT COMMENT '估计要检查的行数',    filtered DECIMAL(5,2) COMMENT '按条件过滤的行百分比',    extra VARCHAR(255) COMMENT '额外信息');

执行计划类型深度分析:

-- 不同类型的执行计划示例-- 1. system & const(最优)EXPLAIN SELECT * FROM departments WHERE dept_id = 1;-- type: const,通过主键或唯一索引查找-- 2. eq_ref(优秀)EXPLAIN SELECT * FROM employees e JOIN departments d ON e.dept_id = d.dept_id;-- type: eq_ref,对于前表的每一行,后表只有一行匹配-- 3. ref(良好)EXPLAIN SELECT * FROM employees WHERE dept_id = 1;-- type: ref,使用非唯一索引查找-- 4. range(良好)EXPLAIN SELECT * FROM employees WHERE hire_date BETWEEN '2020-01-01' AND '2023-01-01';-- type: range,索引范围扫描-- 5. index(中等)EXPLAIN SELECT dept_id FROM employees;-- type: index,全索引扫描-- 6. ALL(最差)EXPLAIN SELECT * FROM employees WHERE salary > 5000;-- type: ALL,全表扫描(如果没有合适的索引)

EXPLAIN FORMAT=JSON深度分析:

-- 使用JSON格式获取更详细的执行计划EXPLAIN FORMAT=JSONSELECT     e.emp_name,    d.dept_name,    COUNT(p.project_id) as project_countFROM employees eJOIN departments d ON e.dept_id = d.dept_idLEFT JOIN projects p ON e.emp_id = p.manager_idWHERE e.hire_date >= '2020-01-01'  AND d.budget > 100000GROUP BY e.emp_id, e.emp_name, d.dept_nameHAVING project_count >= 2ORDER BY e.salary DESC;-- 解析JSON执行计划的关键信息SELECT     JSON_EXTRACT(        (EXPLAIN FORMAT=JSON          SELECT * FROM employees WHERE dept_id = 1),        '$.query_block.cost_info.query_cost'    ) as query_cost;

慢查询日志分析与优化

慢查询配置:

-- 查看慢查询配置SHOW VARIABLES LIKE 'slow_query_log%';SHOW VARIABLES LIKE 'long_query_time';SHOW VARIABLES LIKE 'min_examined_row_limit';SHOW VARIABLES LIKE 'log_queries_not_using_indexes';-- 动态配置慢查询(无需重启)SET GLOBAL slow_query_log = 1;SET GLOBAL long_query_time = 1.0;  -- 1秒SET GLOBAL min_examined_row_limit = 100;SET GLOBAL log_queries_not_using_indexes = 1;-- 永久配置(在my.cnf中)/*[mysqld]slow_query_log = 1slow_query_log_file = /var/log/mysql/slow.loglong_query_time = 1.0log_queries_not_using_indexes = 1min_examined_row_limit = 100log_slow_admin_statements = 1*/

慢查询日志分析:

-- 使用pt-query-digest分析慢查询日志(外部工具)-- pt-query-digest /var/log/mysql/slow.log > slow_report.txt-- 使用MySQL自身分析慢查询CREATE TABLE slow_log_analysis (    query_time DECIMAL(10,6),    lock_time DECIMAL(10,6),    rows_sent INT,    rows_examined INT,    db VARCHAR(512),    query TEXT,    timestamp TIMESTAMP);-- 将慢查询日志导入表中分析LOAD DATA INFILE '/var/log/mysql/slow.log'INTO TABLE slow_log_analysisFIELDS TERMINATED BY '\t'LINES TERMINATED BY '\n';-- 分析慢查询模式SELECT     LEFT(query, 100) as query_sample,    COUNT(*) as query_count,    AVG(query_time) as avg_time,    AVG(rows_examined) as avg_rows_examined,    AVG(rows_sent) as avg_rows_sentFROM slow_log_analysisGROUP BY LEFT(query, 100)ORDER BY avg_time DESCLIMIT 10;

常见慢查询模式及优化:

-- 1. 全表扫描优化-- 慢查询SELECT * FROM employees WHERE YEAR(hire_date) = 2023;-- 优化后SELECT * FROM employees WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31';-- 添加索引:ALTER TABLE employees ADD INDEX idx_hire_date (hire_date);-- 2. OR条件优化-- 慢查询SELECT * FROM employees WHERE dept_id = 1 OR dept_id = 2 OR salary > 10000;-- 优化后SELECT * FROM employees WHERE dept_id IN (1, 2)UNIONSELECT * FROM employees WHERE salary > 10000;-- 3. 分页优化-- 慢查询(偏移量大时)SELECT * FROM employees ORDER BY emp_id LIMIT 10000, 20;-- 优化后SELECT * FROM employees WHERE emp_id > 10000 ORDER BY emp_id LIMIT 20;-- 4. LIKE优化-- 慢查询SELECT * FROM employees WHERE emp_name LIKE '%张%';-- 优化方案-- 使用全文索引或添加反转索引ALTER TABLE employees ADD FULLTEXT idx_name_ft (emp_name);SELECT * FROM employees WHERE MATCH(emp_name) AGAINST('张' IN BOOLEAN MODE);

查询重写技巧与模式

查询重写模式:

-- 1. 使用EXISTS替代IN(当子查询数据量大时)-- 原始查询SELECT * FROM employees WHERE dept_id IN (SELECT dept_id FROM departments WHERE budget > 100000);-- 优化后SELECT e.* FROM employees eWHERE EXISTS (    SELECT 1 FROM departments d     WHERE d.dept_id = e.dept_id AND d.budget > 100000);-- 2. 使用JOIN替代子查询-- 原始查询SELECT emp_name,        (SELECT dept_name FROM departments WHERE dept_id = employees.dept_id) as dept_nameFROM employees;-- 优化后SELECT e.emp_name, d.dept_nameFROM employees eLEFT JOIN departments d ON e.dept_id = d.dept_id;-- 3. 避免SELECT *-- 原始查询SELECT * FROM employees WHERE dept_id = 1;-- 优化后SELECT emp_id, emp_name, email, salary FROM employees WHERE dept_id = 1;-- 4. 使用批处理替代循环-- 不好的做法(在应用程序中循环)-- FOR each id IN ids: --   UPDATE employees SET status = 1 WHERE emp_id = id-- 好的做法UPDATE employees SET status = 1 WHERE emp_id IN (1, 2, 3, 4, 5);

高级查询重写:

-- 5. 使用派生表优化复杂查询-- 原始复杂查询SELECT     e.emp_name,    d.dept_name,    (SELECT COUNT(*) FROM projects p WHERE p.manager_id = e.emp_id) as project_countFROM employees eJOIN departments d ON e.dept_id = d.dept_idWHERE e.salary > (SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id);-- 优化后使用派生表WITH dept_stats AS (    SELECT         dept_id,        AVG(salary) as avg_salary    FROM employees    GROUP BY dept_id),emp_projects AS (    SELECT         manager_id,        COUNT(*) as project_count    FROM projects    GROUP BY manager_id)SELECT     e.emp_name,    d.dept_name,    COALESCE(ep.project_count, 0) as project_countFROM employees eJOIN departments d ON e.dept_id = d.dept_idJOIN dept_stats ds ON e.dept_id = ds.dept_idLEFT JOIN emp_projects ep ON e.emp_id = ep.manager_idWHERE e.salary > ds.avg_salary;-- 6. 条件顺序优化-- 原始查询(选择性差的条件在前)SELECT * FROM employees WHERE status = 1  -- 可能80%的数据status=1  AND hire_date > '2023-01-01'  -- 只有5%的数据满足  AND dept_id = 2;  -- 只有10%的数据满足-- 优化后(选择性好的条件在前)SELECT * FROM employees WHERE dept_id = 2  -- 先过滤掉90%的数据  AND hire_date > '2023-01-01'  -- 在剩下的10%中再过滤  AND status = 1;  -- 最后过滤

分页查询优化方案

传统分页的问题:

-- 问题:偏移量大时性能差SELECT * FROM employees ORDER BY emp_id LIMIT 100000, 20;  -- 需要扫描100000+20行-- 使用索引覆盖优化SELECT emp_id, emp_name, email  -- 只选择需要的列FROM employees ORDER BY emp_id LIMIT 100000, 20;-- 添加覆盖索引ALTER TABLE employees ADD INDEX idx_cover (emp_id, emp_name, email);

高效分页方案:

-- 方案1:游标分页(推荐)-- 第一页SELECT * FROM employees ORDER BY emp_id LIMIT 20;-- 获取最后一条记录的emp_id: 比如是20-- 第二页SELECT * FROM employees WHERE emp_id > 20  -- 使用游标ORDER BY emp_id LIMIT 20;-- 方案2:使用子查询(MySQL 8.0+)SELECT * FROM employees WHERE emp_id IN (    SELECT emp_id FROM employees     ORDER BY emp_id     LIMIT 100000, 20)ORDER BY emp_id;-- 方案3:延迟关联SELECT e.* FROM employees eJOIN (    SELECT emp_id     FROM employees     ORDER BY emp_id     LIMIT 100000, 20) tmp ON e.emp_id = tmp.emp_id;-- 方案4:业务分页(按时间范围)SELECT * FROM employees WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31'ORDER BY emp_id LIMIT 20;

复杂分页场景:

-- 多条件排序分页CREATE INDEX idx_dept_hire_salary ON employees (dept_id, hire_date, salary);-- 游标分页实现-- 第一页SELECT * FROM employees WHERE dept_id = 1ORDER BY hire_date DESC, salary DESC, emp_idLIMIT 20;-- 假设最后一条记录:hire_date='2023-05-20', salary=8000, emp_id=150-- 第二页SELECT * FROM employees WHERE dept_id = 1  AND (      hire_date < '2023-05-20'       OR (hire_date = '2023-05-20' AND salary < 8000)      OR (hire_date = '2023-05-20' AND salary = 8000 AND emp_id > 150)  )ORDER BY hire_date DESC, salary DESC, emp_idLIMIT 20;

大数据量查询处理策略

分批处理策略:

-- 大量数据更新分批处理DELIMITER //CREATE PROCEDURE BatchUpdateEmployees()BEGIN    DECLARE done INT DEFAULT 0;    DECLARE batch_size INT DEFAULT 1000;    DECLARE current_id INT DEFAULT 0;    DECLARE max_id INT;        -- 获取最大ID    SELECT MAX(emp_id) INTO max_id FROM employees;        WHILE current_id < max_id DO        -- 分批更新        UPDATE employees         SET last_updated = NOW()        WHERE emp_id > current_id           AND emp_id <= current_id + batch_size;                -- 记录处理进度        INSERT INTO batch_process_log (process_name, processed_id, processed_at)        VALUES ('employee_update', current_id + batch_size, NOW());                SET current_id = current_id + batch_size;                -- 短暂休眠,减少对系统的影响        DO SLEEP(0.1);    END WHILE;    END //DELIMITER ;

数据归档策略:

-- 历史数据归档CREATE TABLE employees_archive LIKE employees;-- 归档过程DELIMITER //CREATE PROCEDURE ArchiveOldEmployees(IN cutoff_date DATE)BEGIN    DECLARE EXIT HANDLER FOR SQLEXCEPTION    BEGIN        ROLLBACK;        RESIGNAL;    END;        START TRANSACTION;        -- 归档数据    INSERT INTO employees_archive     SELECT * FROM employees     WHERE hire_date < cutoff_date;        -- 删除已归档数据    DELETE FROM employees     WHERE hire_date < cutoff_date;        COMMIT;    END //DELIMITER ;

2. 索引优化实战

索引创建策略与原则

索引选择策略:

-- 索引创建决策流程SELECT     TABLE_NAME,    COLUMN_NAME,    DATA_TYPE,    IS_NULLABLE,    COLUMN_DEFAULT,    CHARACTER_MAXIMUM_LENGTH,    NUMERIC_PRECISION,    NUMERIC_SCALEFROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'your_database'  AND TABLE_NAME = 'your_table';-- 计算索引选择性SELECT     COUNT(DISTINCT dept_id) as distinct_values,    COUNT(*) as total_rows,    ROUND(COUNT(DISTINCT dept_id) / COUNT(*) * 100, 2) as selectivity_percentFROM employees;-- 选择性建议:-- > 20%:适合创建索引-- 5%-20%:根据查询频率决定-- < 5%:通常不适合单独创建索引

复合索引设计:

-- 好的复合索引设计CREATE TABLE orders (    order_id BIGINT PRIMARY KEY,    customer_id BIGINT NOT NULL,    order_date DATETIME NOT NULL,    status ENUM('pending', 'paid', 'shipped', 'delivered') NOT NULL,    total_amount DECIMAL(12,2) NOT NULL,    -- 其他字段...        -- 复合索引设计    INDEX idx_customer_date (customer_id, order_date),           -- 客户订单查询    INDEX idx_date_status (order_date, status),                  -- 按状态查询订单    INDEX idx_status_date (status, order_date),                  -- 状态+日期查询    INDEX idx_customer_status_date (customer_id, status, order_date) -- 覆盖多种查询);-- 索引使用分析EXPLAIN SELECT * FROM orders WHERE customer_id = 123   AND order_date >= '2023-01-01'  AND status = 'shipped';-- 可能使用 idx_customer_date 或 idx_customer_status_date

索引失效场景分析

常见索引失效场景:

-- 1. 对索引列使用函数-- 索引失效SELECT * FROM employees WHERE YEAR(hire_date) = 2023;SELECT * FROM employees WHERE UPPER(emp_name) = 'ZHANGSAN';-- 优化后SELECT * FROM employees WHERE hire_date BETWEEN '2023-01-01' AND '2023-12-31';SELECT * FROM employees WHERE emp_name = 'zhangsan';  -- 应用层处理大小写-- 2. 隐式类型转换-- 索引失效(如果phone是varchar类型)SELECT * FROM users WHERE phone = 13800138000;-- 优化后SELECT * FROM users WHERE phone = '13800138000';-- 3. OR条件使用不当-- 索引可能失效SELECT * FROM employees WHERE dept_id = 1 OR emp_name LIKE '张%';-- 优化后SELECT * FROM employees WHERE dept_id = 1UNIONSELECT * FROM employees WHERE emp_name LIKE '张%';-- 4. 使用NOT、!=、<> -- 索引通常失效SELECT * FROM employees WHERE dept_id != 1;-- 优化方案:考虑是否真的需要排除,或者使用范围查询SELECT * FROM employees WHERE dept_id > 1 OR dept_id < 1;-- 5. LIKE以通配符开头-- 索引失效SELECT * FROM employees WHERE emp_name LIKE '%张%';-- 优化方案:使用全文索引或反转存储

复合索引失效场景:

-- 创建测试索引CREATE INDEX idx_dept_hire_salary ON employees (dept_id, hire_date, salary);-- 1. 不满足最左前缀原则-- 索引失效SELECT * FROM employees WHERE hire_date > '2023-01-01';-- 只能使用hire_date的索引,不能使用复合索引-- 2. 跳过中间列-- 部分使用索引(只用到dept_id)SELECT * FROM employees WHERE dept_id = 1 AND salary > 5000;-- 3. 范围查询后的列无法使用索引-- 只使用dept_id和hire_date进行索引查找,salary使用索引过滤SELECT * FROM employees WHERE dept_id = 1   AND hire_date > '2023-01-01'  AND salary > 5000;-- 4. 索引列顺序影响-- 好的顺序:等值查询列在前,范围查询列在后CREATE INDEX idx_good_order ON employees (dept_id, salary, hire_date);

前缀索引与函数索引

前缀索引:

-- 文本列的前缀索引-- 计算合适的前缀长度SELECT     COUNT(DISTINCT LEFT(emp_name, 5)) as distinct_5,    COUNT(DISTINCT LEFT(emp_name, 10)) as distinct_10,    COUNT(DISTINCT emp_name) as distinct_full,    COUNT(*) as total_rowsFROM employees;-- 创建前缀索引CREATE INDEX idx_emp_name_prefix ON employees (emp_name(10));-- 前缀索引的使用限制-- 不能用于ORDER BY和GROUP BY完整列-- 不能用于覆盖索引-- 查看索引统计信息ANALYZE TABLE employees;SHOW INDEX FROM employees;

函数索引(MySQL 8.0+):

-- 创建函数索引CREATE TABLE products (    product_id INT PRIMARY KEY,    product_name VARCHAR(200),    product_data JSON,    created_at DATESTAMP);-- 函数索引示例CREATE INDEX idx_product_name_upper ON products ((UPPER(product_name)));CREATE INDEX idx_product_price ON products ((CAST(JSON_EXTRACT(product_data, '$.price') AS DECIMAL(10,2))));CREATE INDEX idx_created_date ON products ((DATE(created_at)));-- 使用函数索引查询SELECT * FROM products WHERE UPPER(product_name) = UPPER('iPhone 14');SELECT * FROM products WHERE CAST(JSON_EXTRACT(product_data, '$.price') AS DECIMAL(10,2)) > 1000;SELECT * FROM products WHERE DATE(created_at) = '2023-01-01';

索引维护与重建策略

索引监控:

-- 监控索引使用情况SELECT     OBJECT_SCHEMA,    OBJECT_NAME,    INDEX_NAME,    COUNT_FETCH,    COUNT_INSERT,    COUNT_UPDATE,    COUNT_DELETEFROM performance_schema.table_io_waits_summary_by_index_usageWHERE OBJECT_SCHEMA = 'your_database'ORDER BY COUNT_FETCH DESC;-- 查找未使用的索引SELECT     OBJECT_SCHEMA,    OBJECT_NAME,    INDEX_NAMEFROM performance_schema.table_io_waits_summary_by_index_usageWHERE INDEX_NAME IS NOT NULL  AND COUNT_FETCH = 0  AND COUNT_INSERT = 0  AND COUNT_UPDATE = 0  AND COUNT_DELETE = 0;

索引维护操作:

-- 索引重建ALTER TABLE employees ENGINE=InnoDB;  -- 重建表,包括所有索引ALTER TABLE employees DROP INDEX idx_old, ADD INDEX idx_new (columns);OPTIMIZE TABLE employees;  -- 重建表,整理碎片-- 在线索引操作(MySQL 5.6+)ALTER TABLE employees ADD INDEX idx_new_column (new_column),ALGORITHM=INPLACE, LOCK=NONE;-- 索引统计信息更新ANALYZE TABLE employees;  -- 更新统计信息-- 监控索引大小SELECT     TABLE_NAME,    INDEX_NAME,    ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) as index_size_mbFROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'your_database'GROUP BY TABLE_NAME, INDEX_NAMEORDER BY index_size_mb DESC;

全文索引与空间索引应用

全文索引:

-- 创建全文索引ALTER TABLE articles ADD FULLTEXT idx_content_ft (title, content);-- 全文索引查询SELECT     article_id,    title,    MATCH(title, content) AGAINST('数据库 优化') as relevance_scoreFROM articlesWHERE MATCH(title, content) AGAINST('+数据库 +优化' IN BOOLEAN MODE)ORDER BY relevance_score DESC;-- 全文索引配置-- 查看最小词长SHOW VARIABLES LIKE 'innodb_ft_min_token_size';SHOW VARIABLES LIKE 'innodb_ft_max_token_size';-- 停用词配置SHOW VARIABLES LIKE 'innodb_ft_enable_stopword';SHOW VARIABLES LIKE 'innodb_ft_server_stopword_table';-- 重建全文索引ALTER TABLE articles DROP INDEX idx_content_ft;ALTER TABLE articles ADD FULLTEXT idx_content_ft (title, content);

空间索引:

-- 创建空间数据表CREATE TABLE locations (    location_id INT PRIMARY KEY AUTO_INCREMENT,    location_name VARCHAR(100),    coordinates POINT NOT NULL,    area POLYGON,    SPATIAL INDEX idx_coordinates (coordinates),    SPATIAL INDEX idx_area (area));-- 空间索引查询SELECT     location_name,    ST_AsText(coordinates) as coordsFROM locationsWHERE ST_Contains(    ST_GeomFromText('POLYGON((116.3 39.8, 116.5 39.8, 116.5 40.0, 116.3 40.0, 116.3 39.8))'),    coordinates);-- 距离查询优化SELECT     l1.location_name as place1,    l2.location_name as place2,    ST_Distance_Sphere(l1.coordinates, l2.coordinates) as distanceFROM locations l1JOIN locations l2 ON l1.location_id < l2.location_idWHERE ST_Distance_Sphere(l1.coordinates, l2.coordinates) < 5000;  -- 5公里内

3. 服务器参数调优

内存参数优化

InnoDB缓冲池优化:

-- 查看当前缓冲池配置SHOW VARIABLES LIKE 'innodb_buffer_pool_size';SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';SHOW VARIABLES LIKE 'innodb_buffer_pool_chunk_size';-- 计算合适的缓冲池大小SELECT     @@innodb_buffer_pool_size / 1024 / 1024 / 1024 as current_buffer_pool_gb,    @@innodb_buffer_pool_instances as buffer_pool_instances;-- 缓冲池使用监控SHOW ENGINE INNODB STATUS\G-- 查看 BUFFER POOL AND MEMORY 部分-- 在线调整缓冲池(MySQL 5.7+)SET GLOBAL innodb_buffer_pool_size = 8589934592;  -- 8GB-- 监控缓冲池命中率SELECT     (1 - (variable_value / (        SELECT variable_value         FROM information_schema.global_status         WHERE variable_name = 'innodb_buffer_pool_read_requests'    ))) * 100 as buffer_pool_hit_rateFROM information_schema.global_status WHERE variable_name = 'innodb_buffer_pool_reads';

内存参数配置建议:

# my.cnf 内存配置示例[mysqld]# 缓冲池配置(建议为系统内存的70-80%)innodb_buffer_pool_size = 16Ginnodb_buffer_pool_instances = 8innodb_buffer_pool_chunk_size = 128M# 日志缓冲区innodb_log_buffer_size = 256M# 排序缓冲区sort_buffer_size = 2Mread_buffer_size = 2Mread_rnd_buffer_size = 2Mjoin_buffer_size = 2M# 临时表tmp_table_size = 64Mmax_heap_table_size = 64M# 连接内存thread_cache_size = 100table_open_cache = 4000table_definition_cache = 2000

I/O参数调优

InnoDB I/O优化:

-- 查看I/O相关配置SHOW VARIABLES LIKE 'innodb_io_capacity%';SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';SHOW VARIABLES LIKE 'innodb_flush_method';SHOW VARIABLES LIKE 'innodb_read_io_threads';SHOW VARIABLES LIKE 'innodb_write_io_threads';-- I/O性能监控SHOW STATUS LIKE 'innodb%io%';

I/O参数配置建议:

# my.cnf I/O配置示例[mysqld]# I/O容量(根据存储设备性能调整)# SSD: 2000-10000, HDD: 200-500innodb_io_capacity = 2000innodb_io_capacity_max = 4000# 日志刷盘策略# 1: 最高安全性(每次提交刷盘)# 2: 折中(每秒刷盘)# 0: 最高性能(每秒刷盘,崩溃可能丢失1秒数据)innodb_flush_log_at_trx_commit = 1# 刷盘方法(Linux推荐O_DIRECT)innodb_flush_method = O_DIRECT# I/O线程数innodb_read_io_threads = 8innodb_write_io_threads = 8# 预读设置innodb_random_read_ahead = ON# 双写缓冲(SSD可考虑关闭)innodb_doublewrite = ON

连接数与会话管理

连接配置优化:

-- 查看连接相关配置SHOW VARIABLES LIKE 'max_connections';SHOW VARIABLES LIKE 'max_user_connections';SHOW VARIABLES LIKE 'thread_cache_size';SHOW VARIABLES LIKE 'wait_timeout';SHOW VARIABLES LIKE 'interactive_timeout';-- 监控连接状态SHOW STATUS LIKE 'Threads_%';SHOW PROCESSLIST;-- 连接使用分析SELECT     USER,    HOST,    DB,    COMMAND,    TIME,    STATE,    LEFT(INFO, 100) as query_sampleFROM INFORMATION_SCHEMA.PROCESSLIST WHERE COMMAND != 'Sleep'ORDER BY TIME DESC;

连接优化配置:

# my.cnf 连接配置示例[mysqld]# 最大连接数max_connections = 1000# 用户最大连接数max_user_connections = 500# 线程缓存thread_cache_size = 100# 超时设置wait_timeout = 600interactive_timeout = 600# 连接限制max_connect_errors = 100000# 反向解析(建议关闭提升性能)skip_name_resolve = 1

复制参数配置优化

主从复制优化:

-- 查看复制配置SHOW VARIABLES LIKE 'binlog_format';SHOW VARIABLES LIKE 'sync_binlog';SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';-- 监控复制状态SHOW SLAVE STATUS\G-- 复制性能参数SHOW VARIABLES LIKE 'slave_parallel_workers';SHOW VARIABLES LIKE 'slave_parallel_type';

复制优化配置:

# my.cnf 复制配置示例[mysqld]# 二进制日志格式binlog_format = ROW# 二进制日志同步sync_binlog = 1# 从库并行复制slave_parallel_workers = 8slave_parallel_type = LOGICAL_CLOCK# 复制延迟控制slave_preserve_commit_order = 1# 二进制日志保留expire_logs_days = 7binlog_expire_logs_seconds = 604800

监控指标与调优依据

关键性能指标监控:

-- 创建性能监控表CREATE TABLE performance_metrics (    metric_id INT AUTO_INCREMENT PRIMARY KEY,    metric_name VARCHAR(100) NOT NULL,    metric_value DECIMAL(20,4),    collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    notes TEXT);-- 收集关键指标INSERT INTO performance_metrics (metric_name, metric_value)SELECT 'qps', VARIABLE_VALUEFROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Queries';INSERT INTO performance_metrics (metric_name, metric_value)SELECT 'tps', VARIABLE_VALUEFROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Com_commit';-- 缓冲池命中率INSERT INTO performance_metrics (metric_name, metric_value)SELECT 'buffer_pool_hit_rate',     (1 - (         SELECT VARIABLE_VALUE         FROM information_schema.GLOBAL_STATUS         WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads'    ) / (        SELECT VARIABLE_VALUE         FROM information_schema.GLOBAL_STATUS         WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'    )) * 100;-- 连接使用率INSERT INTO performance_metrics (metric_name, metric_value)SELECT 'connection_usage_rate',    (         SELECT VARIABLE_VALUE         FROM information_schema.GLOBAL_STATUS         WHERE VARIABLE_NAME = 'Threads_connected'    ) / (        SELECT VARIABLE_VALUE         FROM information_schema.GLOBAL_VARIABLES         WHERE VARIABLE_NAME = 'max_connections'    ) * 100;

性能调优检查清单:

-- 系统健康检查查询SELECT     '连接数' as metric,    CONCAT(        (SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Threads_connected'),        '/',        (SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_VARIABLES WHERE VARIABLE_NAME = 'max_connections')    ) as valueUNION ALLSELECT     '缓冲池命中率',    CONCAT(        ROUND((1 - (             SELECT VARIABLE_VALUE             FROM information_schema.GLOBAL_STATUS             WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads'        ) / (            SELECT VARIABLE_VALUE             FROM information_schema.GLOBAL_STATUS             WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'        )) * 100, 2),        '%'    )UNION ALLSELECT     '临时表磁盘使用率',    CONCAT(        ROUND((            SELECT VARIABLE_VALUE             FROM information_schema.GLOBAL_STATUS             WHERE VARIABLE_NAME = 'Created_tmp_disk_tables'        ) / NULLIF(            SELECT VARIABLE_VALUE             FROM information_schema.GLOBAL_STATUS             WHERE VARIABLE_NAME = 'Created_tmp_tables'        , 0) * 100, 2),        '%'    )UNION ALLSELECT     '慢查询比例',    CONCAT(        ROUND((            SELECT VARIABLE_VALUE             FROM information_schema.GLOBAL_STATUS             WHERE VARIABLE_NAME = 'Slow_queries'        ) / NULLIF(            SELECT VARIABLE_VALUE             FROM information_schema.GLOBAL_STATUS             WHERE VARIABLE_NAME = 'Questions'        , 0) * 100, 4),        '%'    );

总结

通过本篇的深入学习,我们掌握了MySQL性能优化的核心技能:

  1. 查询优化:深入理解执行计划,掌握慢查询分析和查询重写技巧
  2. 索引优化:合理设计索引,避免索引失效,掌握索引维护策略
  3. 服务器调优:优化内存、I/O、连接等关键参数配置
  4. 监控体系:建立完善的性能监控和报警机制

关键优化原则:

  • 测量优先:基于实际监控数据进行优化
  • 渐进优化:每次只调整一个参数,观察效果
  • 平衡考虑:在性能、安全、可靠性之间找到平衡点
  • 预防为主:建立常规维护和监控机制

性能优化层次:

  1. SQL和索引优化:效果最明显,成本最低
  2. 数据库配置优化:需要深入了解MySQL内部机制
  3. 架构优化:读写分离、分库分表等
  4. 硬件优化:SSD、更多内存、更好CPU

动手练习:

  1. 分析你当前系统的慢查询,并实施优化
  2. 检查索引使用情况,删除无用索引,添加必要索引
  3. 根据服务器配置调整MySQL参数
  4. 建立性能监控体系,定期收集关键指标
  5. 实施定期的数据库维护操作

欢迎在评论区分享你的性能优化经验和遇到的问题!

🔲 ☆

MySql入门:MySQL数据类型与表设计

MySQL数据类型与表设计

在数据库设计中,选择合适的数据类型和设计良好的表结构是构建高性能应用的基石。今天,我们将深入探讨MySQL的数据类型选择策略、表设计原则以及索引优化技巧,帮助你构建既高效又可维护的数据库结构。

1. 数据类型深度解析

数值类型:整型、浮点型、定点数的选择策略

整型数据类型对比:

类型存储空间有符号范围无符号范围适用场景
TINYINT1字节-128 到 1270 到 255状态标志、年龄、小范围计数
SMALLINT2字节-32,768 到 32,7670 到 65,535端口号、中等范围计数
MEDIUMINT3字节-8,388,608 到 8,388,6070 到 16,777,215用户ID、文章ID
INT4字节-2,147,483,648 到 2,147,483,6470 到 4,294,967,295订单ID、大范围计数
BIGINT8字节-2^63 到 2^63-10 到 2^64-1分布式ID、极大范围计数

数值类型选择实战:

-- 用户表 - 合理的数值类型选择CREATE TABLE users (    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',    age TINYINT UNSIGNED COMMENT '年龄',    status TINYINT DEFAULT 1 COMMENT '状态:1正常,0禁用',    login_count INT UNSIGNED DEFAULT 0 COMMENT '登录次数',    balance DECIMAL(10,2) UNSIGNED DEFAULT 0.00 COMMENT '账户余额') COMMENT='用户表';-- 订单表 - 货币相关使用DECIMALCREATE TABLE orders (    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    user_id BIGINT UNSIGNED NOT NULL,    total_amount DECIMAL(12,2) NOT NULL COMMENT '订单总金额',    tax_amount DECIMAL(10,2) NOT NULL COMMENT '税费',    discount_amount DECIMAL(8,2) DEFAULT 0.00 COMMENT '折扣金额',    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) COMMENT='订单表';

浮点数与定点数选择:

-- 科学计算 - 使用浮点数CREATE TABLE sensor_data (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    temperature FLOAT COMMENT '温度,允许精度损失',    pressure DOUBLE COMMENT '压力,更高精度',    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);-- 金融计算 - 必须使用DECIMALCREATE TABLE financial_records (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    transaction_amount DECIMAL(15,4) NOT NULL, -- 精确计算    exchange_rate DECIMAL(10,6) NOT NULL,      -- 汇率需要高精度    calculated_amount DECIMAL(15,4) AS (transaction_amount * exchange_rate));

字符串类型:CHAR、VARCHAR、TEXT的应用场景

字符串类型对比分析:

类型最大长度存储特点适用场景性能影响
CHAR(N)255字符定长,不足补空格固定长度数据(MD5、UUID)读取快,可能浪费空间
VARCHAR(N)65,535字节变长,额外1-2字节存储长度用户名、邮箱、地址空间效率高,读取稍慢
TINYTEXT255字节变长,不支持默认值短文本描述类似VARCHAR
TEXT65,535字节变长,存储较大文本文章内容、评论可能产生临时表
MEDIUMTEXT16MB变长,大文本存储大型文档、日志影响查询性能
LONGTEXT4GB变长,极大文本存储二进制数据、历史记录谨慎使用

字符串类型实战应用:

-- 用户表 - 合理的字符串类型选择CREATE TABLE user_profiles (    user_id BIGINT UNSIGNED PRIMARY KEY,    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',    email VARCHAR(255) NOT NULL UNIQUE COMMENT '邮箱',    phone CHAR(11) COMMENT '手机号,固定11位',    id_card CHAR(18) COMMENT '身份证号,固定18位',    avatar_url VARCHAR(500) COMMENT '头像URL',    bio TEXT COMMENT '个人简介,可变长文本',        -- 索引优化    INDEX idx_username (username),    INDEX idx_email (email),    INDEX idx_phone (phone)) COMMENT='用户档案表';-- 文章表 - 大文本处理CREATE TABLE articles (    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    title VARCHAR(200) NOT NULL COMMENT '文章标题',    summary VARCHAR(500) COMMENT '文章摘要',    content LONGTEXT NOT NULL COMMENT '文章内容',    tags VARCHAR(300) COMMENT '标签,逗号分隔',        -- 全文索引    FULLTEXT idx_content (title, summary, content),    INDEX idx_created (created_at)) COMMENT='文章表';

日期时间类型:DATETIME、TIMESTAMP、DATE的差异

日期时间类型深度对比:

类型存储空间范围时区处理自动更新适用场景
DATE3字节1000-01-01 到 9999-12-31不支持生日、日期
TIME3字节-838:59:59 到 838:59:59不支持持续时间
DATETIME8字节1000-01-01 00:00:00 到 9999-12-31 23:59:59不支持创建时间、日志时间
TIMESTAMP4字节1970-01-01 00:00:01 到 2038-01-19 03:14:07 UTC自动转换支持更新时间、系统时间
YEAR1字节1901 到 2155不支持年份

日期时间类型实战:

-- 时间字段设计最佳实践CREATE TABLE time_demo (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,        -- 创建时间 - 使用DATETIME,不涉及时区转换    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',        -- 更新时间 - 使用TIMESTAMP,自动更新    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP                  ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',        -- 业务时间 - 使用DATE    birth_date DATE COMMENT '出生日期',    event_date DATE COMMENT '事件日期',        -- 时间范围 - 使用TIME    start_time TIME COMMENT '开始时间',    end_time TIME COMMENT '结束时间',        -- 索引优化    INDEX idx_created (created_at),    INDEX idx_updated (updated_at),    INDEX idx_event_date (event_date));-- 时间查询优化示例SELECT * FROM time_demo WHERE created_at >= '2023-01-01 00:00:00'   AND created_at < '2023-02-01 00:00:00';  SELECT * FROM time_demo WHERE event_date BETWEEN '2023-01-01' AND '2023-01-31';-- 时间函数使用SELECT     id,    created_at,    DATE(created_at) as create_date,    HOUR(created_at) as create_hour,    DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') as formatted_timeFROM time_demo;

JSON数据类型:现代应用的数据存储方案

JSON类型优势与应用场景:

-- 动态schema数据存储CREATE TABLE product_catalog (    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    sku VARCHAR(50) NOT NULL UNIQUE,    basic_info JSON NOT NULL COMMENT '基础信息',    specifications JSON COMMENT '规格参数',    metadata JSON COMMENT '元数据',        -- 生成列 + 索引    product_name VARCHAR(200)         GENERATED ALWAYS AS (basic_info->>'$.name') VIRTUAL,    price DECIMAL(10,2)        GENERATED ALWAYS AS (JSON_UNQUOTE(basic_info->'$.price')) VIRTUAL,        -- 索引    INDEX idx_sku (sku),    INDEX idx_product_name (product_name),    INDEX idx_price (price)) COMMENT='商品目录表';-- JSON数据插入示例INSERT INTO product_catalog (sku, basic_info, specifications) VALUES (    'IPHONE14-128-BLACK',    '{        "name": "iPhone 14",        "brand": "Apple",        "price": 5999.00,        "color": "黑色",        "weight": 172    }',    '{        "screen": {"size": 6.1, "type": "OLED"},        "storage": 128,        "camera": {"main": "48MP", "front": "12MP"},        "battery": 3279    }');-- JSON查询操作SELECT     sku,    basic_info->>'$.name' as product_name,    JSON_UNQUOTE(basic_info->'$.brand') as brand,    specifications->'$.screen.size' as screen_size,        -- JSON路径查询    JSON_EXTRACT(basic_info, '$.price') as price,        -- JSON包含检查    JSON_CONTAINS_PATH(basic_info, 'one', '$.color') as has_color,        -- JSON数组操作    JSON_LENGTH(COALESCE(metadata->'$.tags', '[]')) as tag_count    FROM product_catalogWHERE basic_info->>'$.brand' = 'Apple'  AND specifications->'$.screen.size' > 6.0;-- JSON更新操作UPDATE product_catalog SET basic_info = JSON_SET(    basic_info,    '$.price', 5799.00,    '$.discount', true)WHERE sku = 'IPHONE14-128-BLACK';-- JSON索引优化(MySQL 8.0+)CREATE INDEX idx_brand ON product_catalog((basic_info->>'$.brand'));CREATE INDEX idx_screen_size ON product_catalog(    (CAST(specifications->'$.screen.size' AS UNSIGNED)));

空间数据类型:GIS应用支持

空间数据类型应用:

-- 地理位置数据存储CREATE TABLE locations (    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    name VARCHAR(100) NOT NULL,        -- 空间数据类型    point_coord POINT NOT NULL COMMENT '点坐标',    area_boundary POLYGON COMMENT '区域边界',    route_path LINESTRING COMMENT '路线路径',        -- 空间索引    SPATIAL INDEX idx_point (point_coord),    SPATIAL INDEX idx_area (area_boundary),        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) COMMENT='地理位置表';-- 空间数据插入INSERT INTO locations (name, point_coord, area_boundary) VALUES (    '公司总部',    ST_GeomFromText('POINT(116.3974 39.9093)'),    ST_GeomFromText('POLYGON((116.396 39.908, 116.398 39.908, 116.398 39.910, 116.396 39.910, 116.396 39.908))'));-- 空间查询SELECT     name,    ST_AsText(point_coord) as coordinates,    ST_Distance_Sphere(        point_coord,         ST_GeomFromText('POINT(116.4074 39.9042)')    ) as distance_metersFROM locationsWHERE ST_Contains(    area_boundary,     ST_GeomFromText('POINT(116.3974 39.9093)'));-- 附近查询优化SELECT     name,    ST_Distance_Sphere(point_coord, @user_point) as distanceFROM locationsWHERE ST_Distance_Sphere(point_coord, @user_point) < 5000  -- 5公里内ORDER BY distance ASCLIMIT 10;

2. 表设计与规范化

数据库设计三大范式实战

第一范式(1NF) - 原子性:

-- 违反1NF的设计CREATE TABLE bad_design (    user_id INT PRIMARY KEY,    user_name VARCHAR(100),    phone_numbers VARCHAR(500) -- 存储多个电话号码,用逗号分隔);-- 符合1NF的设计CREATE TABLE users (    user_id INT PRIMARY KEY,    user_name VARCHAR(100) NOT NULL);CREATE TABLE user_phones (    id INT AUTO_INCREMENT PRIMARY KEY,    user_id INT NOT NULL,    phone_type ENUM('mobile', 'home', 'work') NOT NULL,    phone_number VARCHAR(20) NOT NULL,    is_primary BOOLEAN DEFAULT FALSE,        FOREIGN KEY (user_id) REFERENCES users(user_id),    UNIQUE KEY unique_user_phone (user_id, phone_number));

第二范式(2NF) - 完全依赖:

-- 违反2NF的设计(订单明细包含产品信息)CREATE TABLE order_details_bad (    order_id INT,    product_id INT,    product_name VARCHAR(100),  -- 依赖于product_id,而不是完全依赖于主键    quantity INT,    unit_price DECIMAL(10,2),    PRIMARY KEY (order_id, product_id));-- 符合2NF的设计CREATE TABLE orders (    order_id INT PRIMARY KEY,    order_date DATETIME,    customer_id INT,    total_amount DECIMAL(12,2));CREATE TABLE products (    product_id INT PRIMARY KEY,    product_name VARCHAR(100) NOT NULL,    category_id INT,    unit_price DECIMAL(10,2));CREATE TABLE order_items (    order_id INT,    product_id INT,    quantity INT NOT NULL,    unit_price DECIMAL(10,2) NOT NULL, -- 下单时的价格    PRIMARY KEY (order_id, product_id),    FOREIGN KEY (order_id) REFERENCES orders(order_id),    FOREIGN KEY (product_id) REFERENCES products(product_id));

第三范式(3NF) - 无传递依赖:

-- 违反3NF的设计CREATE TABLE employees_bad (    emp_id INT PRIMARY KEY,    emp_name VARCHAR(100),    dept_id INT,    dept_name VARCHAR(100),  -- 传递依赖于emp_id,通过dept_id    manager_name VARCHAR(100));-- 符合3NF的设计CREATE TABLE departments (    dept_id INT PRIMARY KEY,    dept_name VARCHAR(100) NOT NULL,    manager_id INT);CREATE TABLE employees (    emp_id INT PRIMARY KEY,    emp_name VARCHAR(100) NOT NULL,    dept_id INT,    FOREIGN KEY (dept_id) REFERENCES departments(dept_id));

反范式设计的适用场景

读写分离场景的反范式优化:

-- 报表查询优化 - 反范式设计CREATE TABLE user_statistics (    user_id INT PRIMARY KEY,    user_name VARCHAR(100),    total_orders INT DEFAULT 0,    total_amount DECIMAL(12,2) DEFAULT 0,    last_order_date DATETIME,    favorite_category VARCHAR(50),        -- 定期更新的统计字段    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,        INDEX idx_total_amount (total_amount),    INDEX idx_last_order (last_order_date)) COMMENT='用户统计表(反范式设计)';-- 订单列表查询优化CREATE TABLE order_summary (    order_id INT PRIMARY KEY,    order_number VARCHAR(50),    customer_id INT,    customer_name VARCHAR(100),  -- 反范式:冗余存储    total_amount DECIMAL(12,2),    status ENUM('pending', 'paid', 'shipped', 'completed'),    created_at DATETIME,        -- 复合索引支持多种查询    INDEX idx_customer_status (customer_id, status),    INDEX idx_created_status (created_at, status),    INDEX idx_customer_created (customer_id, created_at)) COMMENT='订单汇总表(反范式设计)';

计数器场景的优化:

-- 高频更新计数器的优化设计CREATE TABLE post_counters (    post_id INT PRIMARY KEY,    view_count INT DEFAULT 0,    like_count INT DEFAULT 0,    comment_count INT DEFAULT 0,    share_count INT DEFAULT 0,        -- 定期同步到主表,减少主表更新压力    last_sync_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);-- 计数更新(高频操作)UPDATE post_counters SET view_count = view_count + 1 WHERE post_id = 1234;-- 定期同步到文章主表UPDATE posts pJOIN post_counters pc ON p.id = pc.post_idSET p.view_count = pc.view_count,    p.like_count = pc.like_countWHERE pc.last_sync_at < NOW() - INTERVAL 1 HOUR;

表关系设计:一对一、一对多、多对多

一对一关系设计:

-- 用户基础信息与详细信息的垂直分表CREATE TABLE users (    user_id INT PRIMARY KEY AUTO_INCREMENT,    username VARCHAR(50) UNIQUE NOT NULL,    email VARCHAR(100) UNIQUE NOT NULL,    password_hash VARCHAR(255) NOT NULL,    status TINYINT DEFAULT 1,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) COMMENT='用户基础表';CREATE TABLE user_profiles (    user_id INT PRIMARY KEY,    full_name VARCHAR(100),    birth_date DATE,    gender ENUM('M', 'F', 'O'),    avatar_url VARCHAR(500),    bio TEXT,    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,        FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE) COMMENT='用户详情表(一对一)';

一对多关系设计:

-- 用户与订单的一对多关系CREATE TABLE customers (    customer_id INT PRIMARY KEY AUTO_INCREMENT,    customer_name VARCHAR(100) NOT NULL,    email VARCHAR(100),    phone VARCHAR(20),    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) COMMENT='客户表';CREATE TABLE orders (    order_id INT PRIMARY KEY AUTO_INCREMENT,    order_number VARCHAR(50) UNIQUE NOT NULL,    customer_id INT NOT NULL,    order_date DATETIME NOT NULL,    total_amount DECIMAL(12,2) NOT NULL,    status ENUM('pending', 'confirmed', 'shipped', 'delivered', 'cancelled'),        -- 外键约束    FOREIGN KEY (customer_id) REFERENCES customers(customer_id),        -- 查询优化索引    INDEX idx_customer_date (customer_id, order_date),    INDEX idx_status_date (status, order_date)) COMMENT='订单表(一对多)';

多对多关系设计:

-- 文章与标签的多对多关系CREATE TABLE articles (    article_id INT PRIMARY KEY AUTO_INCREMENT,    title VARCHAR(200) NOT NULL,    content TEXT NOT NULL,    author_id INT,    status ENUM('draft', 'published', 'archived') DEFAULT 'draft',    published_at DATETIME,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,        INDEX idx_author_status (author_id, status),    INDEX idx_published (published_at)) COMMENT='文章表';CREATE TABLE tags (    tag_id INT PRIMARY KEY AUTO_INCREMENT,    tag_name VARCHAR(50) UNIQUE NOT NULL,    tag_slug VARCHAR(50) UNIQUE NOT NULL,    description VARCHAR(200),    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) COMMENT='标签表';CREATE TABLE article_tags (    article_id INT NOT NULL,    tag_id INT NOT NULL,    assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,        -- 复合主键    PRIMARY KEY (article_id, tag_id),        -- 外键约束    FOREIGN KEY (article_id) REFERENCES articles(article_id) ON DELETE CASCADE,    FOREIGN KEY (tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE,        -- 双向查询优化    INDEX idx_tag_article (tag_id, article_id)) COMMENT='文章标签关联表(多对多)';

字段选择与数据类型优化

枚举与集合类型的选择:

-- 使用ENUM替代字符串CREATE TABLE tasks (    task_id INT PRIMARY KEY AUTO_INCREMENT,    title VARCHAR(200) NOT NULL,    priority ENUM('low', 'medium', 'high', 'critical') NOT NULL DEFAULT 'medium',    status ENUM('pending', 'in_progress', 'completed', 'cancelled') NOT NULL DEFAULT 'pending',        -- ENUM存储为数字,查询效率高    INDEX idx_priority_status (priority, status));-- 使用SET存储多选项CREATE TABLE user_preferences (    user_id INT PRIMARY KEY,    notification_types SET('email', 'sms', 'push', 'in_app') DEFAULT 'email',    language SET('zh_CN', 'en_US', 'ja_JP') DEFAULT 'zh_CN',        -- SET查询示例    CHECK (JSON_LENGTH(language) > 0) -- 至少选择一种语言);-- SET查询技巧SELECT * FROM user_preferences WHERE FIND_IN_SET('sms', notification_types) > 0;SELECT * FROM user_preferences WHERE notification_types = 'email,sms'; -- 精确匹配

默认值与约束优化:

CREATE TABLE products (    product_id INT PRIMARY KEY AUTO_INCREMENT,    sku VARCHAR(50) UNIQUE NOT NULL,    name VARCHAR(200) NOT NULL,        -- 合理的默认值    status TINYINT DEFAULT 1,    stock_quantity INT DEFAULT 0,    min_stock_level INT DEFAULT 10,        -- 检查约束(MySQL 8.0.16+)    price DECIMAL(10,2) CHECK (price >= 0),    weight_kg DECIMAL(8,3) CHECK (weight_kg > 0),        -- 时间戳默认值    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,        -- 计算列(MySQL 5.7+)    need_reorder BOOLEAN GENERATED ALWAYS AS (stock_quantity <= min_stock_level),        -- 索引优化    INDEX idx_sku_status (sku, status),    INDEX idx_stock (stock_quantity),    INDEX idx_need_reorder (need_reorder));

3. 索引设计原理与优化

B+Tree索引原理深度解析

B+Tree结构特点:

B+Tree结构:├── 根节点 (Root Node)├── 内部节点 (Internal Nodes)└── 叶子节点 (Leaf Nodes)    ├── 数据页指针    ├── 键值对(有序)    └── 相邻叶子节点指针

B+Tree优势分析:

  • 平衡树结构:所有叶子节点在同一层,查询稳定
  • 顺序访问:叶子节点链表支持范围查询
  • 高扇出:减少树高度,提高查询效率
  • 数据集中:数据只存储在叶子节点

聚簇索引与非聚簇索引

聚簇索引(InnoDB):

-- InnoDB表的聚簇索引(通常是主键)CREATE TABLE employees (    emp_id INT PRIMARY KEY,          -- 聚簇索引    emp_name VARCHAR(100),    department_id INT,    salary DECIMAL(10,2),        -- 数据按emp_id物理排序存储    INDEX idx_department (department_id)  -- 非聚簇索引);-- 没有主键时,InnoDB的处理CREATE TABLE logs (    id BIGINT UNSIGNED AUTO_INCREMENT,    log_message TEXT,    created_at TIMESTAMP,        -- 如果没有主键,InnoDB会:    -- 1. 找第一个UNIQUE NOT NULL列    -- 2. 否则创建隐藏的_rowid列作为聚簇索引    UNIQUE KEY uk_id (id));

非聚簇索引(二级索引)结构:

-- 二级索引包含主键值CREATE TABLE orders (    order_id BIGINT PRIMARY KEY,           -- 聚簇索引键    customer_id BIGINT NOT NULL,    order_date DATE NOT NULL,    total_amount DECIMAL(12,2),        -- 二级索引存储(customer_id, order_id)    INDEX idx_customer_date (customer_id, order_date),        -- 覆盖索引示例    INDEX idx_customer_amount (customer_id, total_amount));-- 二级索引查询过程EXPLAIN SELECT * FROM orders WHERE customer_id = 1234 AND order_date = '2023-01-01';-- 1. 在idx_customer_date找到(order_id)-- 2. 用order_id回表查询完整数据

复合索引设计与最左前缀原则

复合索引设计原则:

-- 用户行为日志表 - 复合索引设计CREATE TABLE user_actions (    user_id BIGINT NOT NULL,    action_type VARCHAR(50) NOT NULL,    action_time DATETIME NOT NULL,    device_type ENUM('web', 'ios', 'android'),    page_url VARCHAR(500),        -- 复合索引设计    PRIMARY KEY (user_id, action_time, action_type),    INDEX idx_time_type (action_time, action_type),    INDEX idx_type_time (action_type, action_time),    INDEX idx_user_type_time (user_id, action_type, action_time));-- 最左前缀原则验证EXPLAIN SELECT * FROM user_actions WHERE user_id = 1234;  -- ✅ 使用索引EXPLAIN SELECT * FROM user_actions WHERE user_id = 1234 AND action_time > '2023-01-01';  -- ✅ 使用索引EXPLAIN SELECT * FROM user_actions WHERE action_time > '2023-01-01';  -- ❌ 无法使用主键索引EXPLAIN SELECT * FROM user_actions WHERE user_id = 1234 AND action_type = 'login';  -- ✅ 使用索引

索引选择性优化:

-- 计算索引选择性SELECT     COUNT(DISTINCT user_id) as distinct_users,    COUNT(*) as total_records,    ROUND(COUNT(DISTINCT user_id) / COUNT(*), 4) as selectivityFROM user_actions;-- 低选择性索引示例(不推荐)CREATE TABLE low_selectivity_demo (    gender ENUM('M', 'F'),           -- 选择性差    status TINYINT DEFAULT 1,        -- 选择性差    created_date DATE,               -- 选择性随时间变差        -- 不推荐的索引    INDEX idx_gender (gender),        -- 推荐的复合索引    INDEX idx_status_date (status, created_date),    INDEX idx_gender_date (gender, created_date));

覆盖索引与索引下推优化

覆盖索引优化:

-- 覆盖索引设计CREATE TABLE sales (    sale_id BIGINT PRIMARY KEY,    product_id BIGINT NOT NULL,    sale_date DATE NOT NULL,    quantity INT NOT NULL,    unit_price DECIMAL(10,2) NOT NULL,    customer_id BIGINT NOT NULL,        -- 覆盖索引:包含查询所需的所有列    INDEX idx_product_date (product_id, sale_date),    INDEX idx_customer_product (customer_id, product_id, quantity, unit_price),    INDEX idx_date_customer (sale_date, customer_id, product_id));-- 覆盖索引查询示例EXPLAIN SELECT product_id, SUM(quantity) as total_quantityFROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-01-31'GROUP BY product_id;-- ✅ 使用idx_date_customer,不需要回表EXPLAINSELECT customer_id, product_id, quantity, unit_priceFROM sales WHERE customer_id = 1234 AND product_id IN (1, 2, 3);-- ✅ 使用idx_customer_product,不需要回表

索引下推(ICP)优化:

-- 索引下推示例CREATE TABLE orders_icp (    order_id BIGINT PRIMARY KEY,    customer_id BIGINT NOT NULL,    status ENUM('pending', 'paid', 'shipped') NOT NULL,    total_amount DECIMAL(12,2),    created_at DATETIME,        INDEX idx_customer_status (customer_id, status));-- 没有ICP的查询(旧版本)SELECT * FROM orders_icp WHERE customer_id = 1234 AND status = 'paid';-- 1. 通过customer_id找到所有记录-- 2. 回表读取完整数据-- 3. 在Server层过滤status-- 有ICP的查询(MySQL 5.6+)SELECT * FROM orders_icp WHERE customer_id = 1234 AND status = 'paid';-- 1. 在存储引擎层直接过滤customer_id和status-- 2. 只回表符合条件的记录

索引维护与重建策略

索引监控与维护:

-- 索引使用情况监控SELECT     OBJECT_SCHEMA,    OBJECT_NAME,    INDEX_NAME,    COUNT_READ,    COUNT_FETCH,    COUNT_INSERT,    COUNT_UPDATE,    COUNT_DELETEFROM performance_schema.table_io_waits_summary_by_index_usageWHERE OBJECT_SCHEMA = 'your_database'ORDER BY COUNT_READ DESC;-- 索引统计信息ANALYZE TABLE sales;  -- 更新统计信息SHOW INDEX FROM sales;-- 关注Cardinality(基数),值越接近记录数越好-- 索引碎片整理OPTIMIZE TABLE sales;  -- 重建表,整理碎片-- 在线DDL(MySQL 5.6+)ALTER TABLE sales DROP INDEX idx_old_index,ADD INDEX idx_new_index (customer_id, sale_date),ALGORITHM=INPLACE, LOCK=NONE;

索引设计检查清单:

-- 1. 为WHERE条件中的列创建索引-- 2. 为JOIN条件的列创建索引  -- 3. 为ORDER BY、GROUP BY的列创建索引-- 4. 考虑覆盖索引,避免回表-- 5. 使用复合索引,注意最左前缀-- 6. 避免在索引列上使用函数-- 7. 定期监控索引使用情况-- 索引使用分析EXPLAIN FORMAT=JSONSELECT o.order_id, c.customer_name, SUM(oi.quantity) as total_itemsFROM orders oJOIN customers c ON o.customer_id = c.customer_idJOIN order_items oi ON o.order_id = oi.order_idWHERE o.created_at >= '2023-01-01'  AND o.status = 'completed'GROUP BY o.order_id, c.customer_nameHAVING total_items > 5ORDER BY o.created_at DESC;

总结

通过本篇的深入学习,我们掌握了MySQL数据类型选择和表设计的核心知识:

  1. 数据类型选择:根据业务需求选择最合适的类型,平衡存储空间和查询性能
  2. 规范化设计:理解三大范式,知道何时应该反范式化优化性能
  3. 关系设计:掌握一对一、一对多、多对多关系的实现方式
  4. 索引原理:深入理解B+Tree、聚簇索引、覆盖索引的工作原理
  5. 索引优化:掌握复合索引设计、最左前缀原则、索引下推等高级技巧

关键实践要点:

  • 字符串类型:固定长度用CHAR,变长用VARCHAR,大文本用TEXT
  • 数值类型:根据范围选择最小合适的类型,金融计算用DECIMAL
  • 时间类型:业务时间用DATETIME,系统时间用TIMESTAMP
  • 索引设计:遵循最左前缀,考虑覆盖索引,监控索引使用情况

动手练习:

  1. 为你当前的项目重新设计表结构,应用学到的数据类型和索引原则
  2. 分析现有表的索引使用情况,优化低效索引
  3. 尝试使用JSON类型存储半结构化数据
  4. 设计一个符合范式要求的数据库schema,并考虑性能优化

欢迎在评论区分享你的表设计经验和遇到的问题!

🔲 ☆

MySql入门:MySQL核心概念与架构解析

MySQL核心概念与架构解析

在现代应用开发中,数据库是系统的核心支柱。而MySQL作为世界上最流行的开源关系型数据库,其重要性不言而喻。今天,我们将深入探讨MySQL的核心概念和架构设计,为你揭开这个强大数据库系统的神秘面纱。

1. MySQL概述与版本演进

什么是MySQL?关系型数据库的核心价值

MySQL是一个开源的关系型数据库管理系统(RDBMS),由瑞典MySQL AB公司开发,目前属于Oracle公司。它采用客户端-服务器模型,使用结构化查询语言(SQL)进行数据管理。

关系型数据库的核心价值:

-- ACID特性的具体体现START TRANSACTION;-- 原子性(Atomicity):要么全部成功,要么全部失败UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;-- 一致性(Consistency):始终满足业务规则约束-- 隔离性(Isolation):事务间互不干扰-- 持久性(Durability):提交后数据永久保存COMMIT;

MySQL的关键特性:

  • 开源免费(社区版)
  • 跨平台支持
  • 支持多种存储引擎
  • 强大的复制功能
  • 丰富的生态系统

MySQL发展历程与重要版本特性

版本演进时间线:

版本发布时间重要特性
MySQL 3.232001年引入InnoDB存储引擎
MySQL 4.02003年联合查询、重写解析器
MySQL 5.02005年视图、存储过程、触发器
MySQL 5.12008年分区、事件调度器
MySQL 5.52010年InnoDB成为默认引擎
MySQL 5.62013年全文索引、NoSQL API
MySQL 5.72015年原生JSON支持、多源复制
MySQL 8.02018年窗口函数、CTE、角色管理

MySQL 5.7 vs 8.0 核心差异对比

-- MySQL 5.7 特性示例SELECT * FROM users WHERE JSON_EXTRACT(profile, '$.age') > 25;-- MySQL 8.0 新特性示例-- 窗口函数SELECT     name,     salary,    AVG(salary) OVER (PARTITION BY department_id) as avg_dept_salaryFROM employees;-- 公用表表达式(CTE)WITH department_stats AS (    SELECT         department_id,        AVG(salary) as avg_salary    FROM employees     GROUP BY department_id)SELECT * FROM department_stats WHERE avg_salary > 5000;-- 角色管理CREATE ROLE read_only;GRANT SELECT ON company.* TO read_only;GRANT read_only TO 'report_user'@'%';

性能对比:

  • MySQL 8.0 在读写并发性能上提升约30%
  • 更好的JSON处理性能
  • 改进的优化器,更准确的成本估算

MySQL在现代应用架构中的定位

在现代微服务架构中,MySQL扮演着重要角色:

TypeError: Cannot read properties of undefined (reading 'v')

2. MySQL体系架构深度解析

整体架构概览

MySQL采用经典的客户端-服务器架构,其核心组件包括:

MySQL Architecture:├── 连接层 (Connection Layer)├── SQL层 (SQL Layer)│   ├── 连接池│   ├── 查询解析器│   ├── 查询优化器│   ├── 查询执行器│   └── 缓存└── 存储引擎层 (Storage Engine Layer)    ├── InnoDB (默认)    ├── MyISAM    ├── Memory    └── 其他引擎

连接层:连接池、身份验证、线程管理

连接处理机制:

public class MySQLConnectionPool{    // MySQL使用线程池处理连接    private const int MAX_CONNECTIONS = 151; // 默认最大连接数        public void HandleConnection(ClientConnection client)    {        // 1. 连接验证        if (!Authenticate(client.Username, client.Password))            throw new AuthenticationException();                    // 2. 权限检查        if (!CheckPrivileges(client.Username, client.Database))            throw new AccessDeniedException();                    // 3. 创建会话        var session = CreateSession(client);                // 4. 线程分配(一对一或线程池)        AssignThreadToSession(session);    }}

连接状态监控:

-- 查看当前连接信息SHOW PROCESSLIST;-- 查看连接统计SHOW STATUS LIKE 'Threads_%';-- 输出示例:-- Threads_cached: 10      -- 缓存中的线程数-- Threads_connected: 25   -- 当前连接数-- Threads_created: 1000   -- 已创建线程总数-- Threads_running: 5      -- 活跃线程数

SQL层:查询解析、优化器、执行器工作原理

SQL查询处理流程:

-- 示例查询SELECT u.name, COUNT(o.id) as order_countFROM users uJOIN orders o ON u.id = o.user_idWHERE u.created_at > '2023-01-01'GROUP BY u.idHAVING order_count > 5ORDER BY order_count DESCLIMIT 10;

处理步骤详解:

  1. 查询解析(Parser)

    • 语法分析:检查SQL语法正确性
    • 词法分析:将SQL分解为标记(tokens)
    • 生成解析树
  2. 查询优化(Optimizer)

    -- 使用EXPLAIN查看优化器决策EXPLAIN FORMAT=JSON SELECT u.name, COUNT(o.id) as order_countFROM users uJOIN orders o ON u.id = o.user_idWHERE u.created_at > '2023-01-01'GROUP BY u.idHAVING order_count > 5;
  3. 查询执行(Executor)

    • 根据执行计划访问存储引擎
    • 应用WHERE条件过滤
    • 执行JOIN操作
    • 进行GROUP BY和聚合
    • 应用HAVING条件
    • 排序和限制结果

存储引擎层:插件式架构设计

MySQL的存储引擎采用插件式架构,允许为不同表选择不同存储引擎:

-- 创建表时指定存储引擎CREATE TABLE users (    id INT PRIMARY KEY AUTO_INCREMENT,    name VARCHAR(100),    email VARCHAR(255),    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB;-- 查看表的存储引擎SHOW TABLE STATUS LIKE 'users';

存储引擎对比:

特性InnoDBMyISAMMemory
事务支持
行级锁
外键支持
崩溃恢复⚠️
全文索引✅ (5.6+)
适用场景事务型应用读密集型临时数据

InnoDB存储引擎架构详解

InnoDB是MySQL的默认存储引擎,其架构设计非常精妙:

InnoDB Architecture:├── 内存结构 (In-Memory Structures)│   ├── Buffer Pool (缓冲池)│   ├── Change Buffer (变更缓冲)│   ├── Adaptive Hash Index (自适应哈希索引)│   ├── Log Buffer (日志缓冲)│   └── Additional Memory Pool└── 磁盘结构 (On-Disk Structures)    ├── 表空间 (Tablespaces)    │   ├── 系统表空间    │   ├── 独立表空间    │   ├── 通用表空间    │   └── 临时表空间    ├── 重做日志 (Redo Logs)    ├── 撤销日志 (Undo Logs)    └── 二进制日志 (Binary Logs)

Buffer Pool工作机制:

-- 查看Buffer Pool状态SHOW ENGINE INNODB STATUS\G-- Buffer Pool相关配置SELECT @@innodb_buffer_pool_size;      -- 缓冲池大小SELECT @@innodb_buffer_pool_instances; -- 缓冲池实例数-- 监控Buffer Pool命中率SELECT     (1 - (Variable_value / (SELECT Variable_value                            FROM information_schema.global_status                            WHERE Variable_name = 'Innodb_pages_read'))) * 100 as hit_rateFROM information_schema.global_status WHERE Variable_name = 'Innodb_buffer_pool_reads';

内存结构与磁盘存储机制

内存管理:

public class InnoDBMemoryManager{    // Buffer Pool - 数据页缓存    private Dictionary<PageId, DataPage> bufferPool;        // Change Buffer - 非唯一索引变更缓存    private Dictionary<IndexId, IndexChange> changeBuffer;        // Log Buffer - 重做日志缓冲    private CircularBuffer<RedoLogRecord> logBuffer;        public DataPage ReadPage(PageId pageId)    {        // 1. 检查Buffer Pool        if (bufferPool.ContainsKey(pageId))            return bufferPool[pageId];                    // 2. 从磁盘读取        var page = diskStorage.ReadPage(pageId);                // 3. 使用LRU算法管理缓存        if (bufferPool.Count >= maxSize)            EvictLeastRecentlyUsedPage();                    bufferPool[pageId] = page;        return page;    }}

磁盘存储结构:

-- 表空间文件结构-- 系统表空间: ibdata1-- 独立表空间: db/table.ibd-- 查看表空间信息SELECT     table_name,    engine,    table_rows,    avg_row_length,    data_length,    index_length,    data_freeFROM information_schema.tables WHERE table_schema = 'your_database';

3. 安装部署与配置优化

多平台安装指南

Linux安装(Ubuntu为例):

# 更新包管理器sudo apt update# 安装MySQL服务器sudo apt install mysql-server-8.0# 安全配置sudo mysql_secure_installation# 启动服务sudo systemctl start mysqlsudo systemctl enable mysql# 验证安装mysql --version

Docker部署:

# docker-compose.ymlversion: '3.8'services:  mysql:    image: mysql:8.0    container_name: mysql-server    environment:      MYSQL_ROOT_PASSWORD: your_secure_password      MYSQL_DATABASE: app_db      MYSQL_USER: app_user      MYSQL_PASSWORD: app_password    ports:      - "3306:3306"    volumes:      - mysql_data:/var/lib/mysql      - ./conf.d:/etc/mysql/conf.d    command:       - --default-authentication-plugin=mysql_native_password      - --character-set-server=utf8mb4      - --collation-server=utf8mb4_unicode_civolumes:  mysql_data:

配置文件详解(my.cnf/my.ini)

生产环境配置示例:

[mysqld]# 基础配置datadir=/var/lib/mysqlsocket=/var/lib/mysql/mysql.sockport=3306# 内存配置innodb_buffer_pool_size=16G           # 建议为系统内存的70-80%innodb_log_file_size=2G               # 重做日志文件大小innodb_log_buffer_size=256M           # 日志缓冲区大小# 连接配置max_connections=1000                  # 最大连接数thread_cache_size=100                 # 线程缓存大小table_open_cache=4000                 # 表缓存大小# InnoDB配置innodb_file_per_table=ON              # 每个表独立表空间innodb_flush_log_at_trx_commit=1      # 事务提交时刷盘innodb_flush_method=O_DIRECT          # I/O方式innodb_buffer_pool_instances=8        # 缓冲池实例数# 复制配置(如果使用主从)server_id=1log_bin=mysql-binbinlog_format=ROW# 性能配置query_cache_type=0                    # 8.0已移除查询缓存sort_buffer_size=2Mread_buffer_size=2Mread_rnd_buffer_size=2M[mysql]default-character-set=utf8mb4[client]default-character-set=utf8mb4

系统参数调优实战

性能诊断查询:

-- 查看关键性能指标SHOW STATUS WHERE `variable_name` IN (    'Questions', 'Com_select', 'Com_insert', 'Com_update', 'Com_delete',    'Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests',    'Threads_connected', 'Threads_running',    'Key_reads', 'Key_read_requests');-- 计算缓冲池命中率SELECT     ROUND(1 - (variable_value / (        SELECT variable_value         FROM information_schema.global_status         WHERE variable_name = 'innodb_buffer_pool_read_requests'    )), 4) * 100 as buffer_pool_hit_rateFROM information_schema.global_status WHERE variable_name = 'innodb_buffer_pool_reads';-- 检查慢查询SHOW VARIABLES LIKE 'slow_query_log%';SHOW VARIABLES LIKE 'long_query_time';

安全配置与权限管理

基础安全配置:

-- 创建应用用户(遵循最小权限原则)CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'secure_password_123';-- 授予精确权限GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'192.168.1.%';-- 创建只读用户用于报表CREATE USER 'report_user'@'%' IDENTIFIED BY 'readonly_password';GRANT SELECT ON app_db.* TO 'report_user'@'%';-- 查看用户权限SHOW GRANTS FOR 'app_user'@'192.168.1.%';-- 密码策略配置SET GLOBAL validate_password.policy = MEDIUM;SET GLOBAL validate_password.length = 12;

网络安全配置:

-- 限制连接来源RENAME USER 'root'@'%' TO 'root'@'localhost';-- 删除测试数据库和匿名用户DROP DATABASE IF EXISTS test;DELETE FROM mysql.user WHERE User = '';-- 刷新权限FLUSH PRIVILEGES;

监控工具与性能基线建立

系统监控查询:

-- 性能模式监控(MySQL 5.6+)SELECT * FROM performance_schema.events_statements_summary_by_digest ORDER BY sum_timer_wait DESC LIMIT 10;-- 查看锁信息SELECT * FROM information_schema.INNODB_LOCKS;SELECT * FROM information_schema.INNODB_LOCK_WAITS;-- 表统计信息SELECT     table_name,    table_rows,    data_length,    index_length,    ROUND((data_length + index_length) / 1024 / 1024, 2) as total_size_mbFROM information_schema.tables WHERE table_schema = 'your_database'ORDER BY total_size_mb DESC;

建立性能基线:

-- 创建性能基线表CREATE TABLE performance_baseline (    id INT AUTO_INCREMENT PRIMARY KEY,    metric_name VARCHAR(100),    metric_value DECIMAL(20,4),    collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    notes TEXT);-- 收集基线数据INSERT INTO performance_baseline (metric_name, metric_value)SELECT     'qps' as metric_name,    VARIABLE_VALUE as metric_valueFROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Queries';-- 定期收集其他关键指标...

总结

通过本篇的学习,我们深入了解了MySQL的核心概念和架构设计:

  1. MySQL的演进历程:从简单的数据库系统发展到功能丰富的企业级解决方案
  2. 分层架构设计:连接层、SQL层、存储引擎层的明确分工
  3. InnoDB的核心地位:作为默认存储引擎的先进特性
  4. 配置优化原则:根据硬件和工作负载进行针对性调优
  5. 安全最佳实践:权限最小化和网络安全配置

关键收获:

  • 理解MySQL的架构有助于更好地进行性能调优和故障排查
  • 合理的配置可以显著提升数据库性能和稳定性
  • 安全配置不是可选项,而是生产部署的必备条件

在接下来的篇章中,我们将深入探讨MySQL的数据类型、表设计、索引优化等高级主题,帮助你构建高性能的数据库应用。

思考与实践:

  1. 在你的环境中安装MySQL 8.0,并尝试不同的配置参数
  2. 使用性能模式监控数据库的运行状态
  3. 设计一个符合最小权限原则的用户权限体系
  4. 建立关键性能指标的监控基线

欢迎在评论区分享你的MySQL配置经验和遇到的问题!

🔲 ☆

Redis配置文件及常用命令详解

Redis完全指南:配置文件详解与常用命令大全

本文深入解析Redis核心配置,并提供全面的命令参考手册,助你彻底掌握Redis使用技巧。

📖 概述

Redis作为高性能的键值数据库,在缓存、消息队列、会话存储等场景中广泛应用。掌握其配置文件和常用命令是每个开发者必备的技能。

⚙️ Redis配置文件深度解析

配置文件位置与加载

# 默认配置文件路径/etc/redis/redis.conf# 指定配置文件启动redis-server /path/to/your/redis.conf# 检查当前配置redis-cli config get *

核心配置项详解

🔒 网络与安全配置

# 绑定IP地址(生产环境建议指定)bind 127.0.0.1 192.168.1.100# 端口配置port 6379# 保护模式(外网访问需关闭)protected-mode no# 连接密码requirepass "your_strong_password_here"# 最大连接数maxclients 10000

💡 生产环境建议

  • 务必设置强密码
  • 限制绑定IP,避免暴露到公网
  • 适当调整最大连接数

💾 持久化配置

RDB持久化配置

# 自动保存条件save 900 1      # 15分钟内至少1个变更save 300 10     # 5分钟内至少10个变更  save 60 10000   # 1分钟内至少10000个变更# RDB文件配置dbfilename dump.rdbdir /var/lib/redis# 压缩配置rdbcompression yesrdbchecksum yes

AOF持久化配置

# 开启AOFappendonly yesappendfilename "appendonly.aof"# 同步策略appendfsync everysec    # 推荐配置# AOF重写配置auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb

🎯 持久化策略选择

  • 缓存场景:仅使用RDB
  • 数据安全要求高:RDB+AOF
  • 性能优先:调整AOF同步策略为everysec

🧠 内存管理配置

# 内存限制maxmemory 2gb# 内存淘汰策略maxmemory-policy volatile-lru# 淘汰策略说明:# volatile-lru    -> 从过期键中淘汰最近最少使用# allkeys-lru     -> 从所有键中淘汰最近最少使用  # volatile-ttl    -> 从过期键中淘汰存活时间最短# volatile-random -> 从过期键中随机淘汰# noeviction      -> 不淘汰,返回错误

性能优化配置

# 内核参数优化vm.overcommit_memory = 1# 禁用透明大页echo never > /sys/kernel/mm/transparent_hugepage/enabled# 网络优化tcp-backlog 511timeout 0tcp-keepalive 300

⌨️ Redis常用命令大全

1. 🔑 键(Key)操作命令

基础操作

# 设置键值(带过期时间)SET user:1001 "John Doe" EX 3600# 批量操作MSET user:1001 "John" user:1002 "Jane" user:1003 "Bob"# 获取值GET user:1001# 删除键DEL user:1001 user:1002

高级特性

# 设置带过期时间的键(原子操作)SETEX session:token 1800 "encrypted_data"# 仅当键不存在时设置(分布式锁基础)SETNX lock:resource_1 "owner_id"# 获取并设置(原子操作)GETSET counter:clicks "100"

2. 📝 字符串(String)操作

# 数值操作INCR article:1001:views    # 阅读量+1INCRBY user:1001:points 10 # 积分+10DECR inventory:item_001    # 库存-1# 字符串操作APPEND user:1001:bio " Additional info"STRLEN user:1001:name      # 字符串长度GETRANGE user:1001:bio 0 4 # 获取子串

3. 🗂️ 哈希(Hash)操作

用户信息存储示例

# 设置用户信息HSET user:1001 name "John" age 30 email "john@example.com"# 批量设置HMSET product:1001 name "Laptop" price 999.99 stock 50 category "Electronics"# 获取信息HGET user:1001 nameHMGET user:1001 name age emailHGETALL user:1001# 数值操作HINCRBY user:1001:stats login_count 1HINCRBYFLOAT product:1001 price -50.5

4. 📋 列表(List)操作

消息队列实现

# 生产者:推送消息LPUSH message:queue "task_1"LPUSH message:queue "task_2"# 消费者:获取消息RPOP message:queue# 阻塞式获取(推荐)BRPOP message:queue 30# 查看队列LRANGE message:queue 0 -1LLEN message:queue

5. 🔄 集合(Set)操作

标签系统实现

# 添加标签SADD article:1001:tags "tech" "programming" "redis"SADD user:1001:interests "coding" "gaming"# 查找共同兴趣SINTER user:1001:interests user:1002:interests# 推荐相关文章SUNION article:1001:tags article:1002:tags# 随机推荐SRANDMEMBER article:1001:tags 3

6. 📊 有序集合(Sorted Set)操作

排行榜实现

# 添加分数ZADD leaderboard 1500 "player_1"ZADD leaderboard 3200 "player_2" 2800 "player_3"# 获取排名ZREVRANGE leaderboard 0 9 WITHSCORES  # 前10名ZRANK leaderboard "player_1"          # 升序排名ZREVRANK leaderboard "player_1"       # 降序排名# 范围查询ZRANGEBYSCORE leaderboard 2000 3000 WITHSCORES

🛠️ 实战应用场景

缓存策略实现

# 缓存查询结果SETEX cache:user:1001:profile 300 "{user_data}"# 缓存穿透防护SETNX cache_mutex:user:9999 1 EX 5

分布式会话

# 存储会话HSET session:abc123 user_id 1001 last_active 1635789000EXPIRE session:abc123 1800# 更新活跃时间EXPIRE session:abc123 1800

限流器实现

# 简单限流INCR rate_limit:api:1001EXPIRE rate_limit:api:1001 60# 复杂限流(使用Lua脚本)EVAL "local current = redis.call('incr', KEYS[1]) if current == 1 then redis.call('expire', KEYS[1], ARGV[1]) end return current" 1 rate_limit:complex 60

📈 监控与维护命令

系统状态检查

# 基础信息redis-cli info# 内存分析redis-cli info memory# 持久化状态redis-cli info persistence# 查看慢查询redis-cli slowlog get 10

性能监控

# 实时监控redis-cli monitor# 客户端连接管理redis-cli client listredis-cli client kill 127.0.0.1:53422# 内存分析redis-cli --bigkeysredis-cli --memkeys

备份与恢复

# 手动RDB备份redis-cli bgsave# AOF重写redis-cli bgrewriteaof# 数据迁移redis-cli --rdb dump.rdb

🚀 性能优化技巧

1. 连接池配置

# Python示例import redispool = redis.ConnectionPool(    max_connections=50,    host='localhost',     port=6379,    decode_responses=True)r = redis.Redis(connection_pool=pool)

2. 管道(Pipeline)优化

# 批量操作,减少网络往返pipe = r.pipeline()for user_id in user_ids:    pipe.hgetall(f"user:{user_id}")results = pipe.execute()

3. Lua脚本使用

# 原子性操作示例EVAL "local current = redis.call('get', KEYS[1]) if current then return redis.call('incr', KEYS[1]) else return nil end" 1 counter:test

⚠️ 常见问题排查

内存问题

# 查看内存使用详情redis-cli info memory | grep used_memory_human# 查找大Keyredis-cli --bigkeys# 内存碎片率redis-cli info memory | grep mem_fragmentation_ratio

连接问题

# 查看连接数redis-cli info clients# 客户端列表redis-cli client list# 网络统计redis-cli info stats | grep -E "(total_connections_received|rejected_connections)"

📚 总结

通过本文的学习,你应该已经掌握了:

  • ✅ Redis配置文件的各项参数含义及调优方法
  • ✅ 各类数据结构的适用场景及操作命令
  • ✅ 常见业务场景的Redis实现方案
  • ✅ 性能监控与问题排查技巧

Redis的强大之处在于其丰富的数据结构和原子操作,合理运用可以极大提升系统性能。建议在实际项目中多实践,逐步深入理解各个特性的使用场景。


欢迎在评论区留言交流,如果你觉得这篇文章有帮助,请点赞收藏支持!

🔲 ☆

Redis入门:总结与展望

Redis总结与展望:从入门到生产实践的完整指南

经过前面五篇深入的学习,我们已经完成了从Redis小白到生产级应用开发者的蜕变。在这最后一篇中,让我们回顾整个学习旅程,总结关键知识点,并展望Redis未来的发展方向和生态体系。

一、系列回顾:我们的Redis学习之旅

让我们简要回顾这个系列涵盖的核心内容:

第一篇:Redis核心概念与快速入门

  • 理解了Redis为什么快(内存存储、单线程模型、I/O多路复用)
  • 学会了使用Docker快速搭建Redis环境
  • 掌握了基本的键值操作和通用命令
  • 认识了Redis的典型应用场景

第二篇:玩转Redis五大核心数据结构

  • String:不仅仅是文本,支持计数器、位图等高级用法
  • Hash:存储对象的最佳选择,内存效率高
  • List:实现消息队列和最新列表的利器
  • Set:无序唯一集合,强大的集合运算能力
  • Sorted Set:有序集合,排行榜和时间轴的核心

第三篇:Redis的持久化与高可用

  • RDB:快照式持久化,适合备份和快速恢复
  • AOF:日志式持久化,保证数据安全
  • 主从复制:数据冗余和读写分离的基础
  • 哨兵模式:实现自动故障转移的高可用方案

第四篇:Redis在Asp.Net Core项目中的实战应用

  • 集成StackExchange.Redis客户端
  • 实现商品信息缓存和缓存策略
  • 解决缓存穿透、击穿、雪崩问题
  • 使用分布式锁控制并发访问
  • 配置分布式Session和消息队列

第五篇:进阶知识与运维管理

  • 内存优化和淘汰策略配置
  • Redis Cluster集群搭建和管理
  • 性能监控、慢查询分析和调优
  • 备份恢复、安全配置和故障诊断

二、Redis最佳实践总结

1. 键名设计规范

// 好的键名设计"user:1001:profile"          // 用户信息"product:2024:hotlist"       // 商品热榜"order:20240101:123456"      // 订单信息"session:abc123def456"       // 会话数据// 避免的键名设计"user_info_1001"             // 不一致的分隔符"data"                       // 过于简单,容易冲突"very_long_key_name_that_is_hard_to_read_and_remember" // 过长

键名设计原则:

  • 使用统一的命名空间和分隔符(推荐冒号)
  • 保持简洁但具有描述性
  • 避免特殊字符和过长的键名

2. 避免大Key和热Key

大Key问题解决方案:

// 拆分大Hashpublic async Task SetLargeUserDataAsync(int userId, UserLargeData data){    // 拆分为多个Hash    await _database.HashSetAsync($"user:{userId}:basic", new[] {        new HashEntry("name", data.Name),        new HashEntry("email", data.Email)    });        await _database.HashSetAsync($"user:{userId}:profile", new[] {        new HashEntry("bio", data.Bio),        new HashEntry("avatar", data.AvatarUrl)    });}// 使用SCAN替代KEYSpublic async Task<List<string>> ScanKeysAsync(string pattern, int pageSize = 1000){    var keys = new List<string>();    var cursor = 0;        do    {        var result = await _database.ExecuteAsync("SCAN", cursor.ToString(), "MATCH", pattern, "COUNT", pageSize.ToString());        var innerResult = (RedisResult[])result;                cursor = int.Parse((string)innerResult[0]);        var pageKeys = (string[])innerResult[1];        keys.AddRange(pageKeys);            } while (cursor != 0);        return keys;}

热Key解决方案:

// 本地缓存 + Redis多级缓存public class MultiLevelCacheService{    private readonly IMemoryCache _memoryCache;    private readonly IRedisService _redisService;    private readonly TimeSpan _localCacheDuration = TimeSpan.FromMinutes(1);        public async Task<T> GetWithLocalCacheAsync<T>(string key)    {        // 先查本地缓存        if (_memoryCache.TryGetValue(key, out T localValue))            return localValue;                    // 本地缓存未命中,查询Redis        var redisValue = await _redisService.GetAsync<T>(key);        if (redisValue != null)        {            // 写入本地缓存            _memoryCache.Set(key, redisValue, _localCacheDuration);        }                return redisValue;    }}

3. 连接池与资源管理

public static class RedisConnectionManager{    private static Lazy<ConnectionMultiplexer> _lazyConnection;        static RedisConnectionManager()    {        _lazyConnection = new Lazy<ConnectionMultiplexer>(() =>        {            var configuration = new ConfigurationOptions            {                EndPoints = { "localhost:6379" },                AbortOnConnectFail = false,                ConnectRetry = 3,                ConnectTimeout = 5000,                KeepAlive = 180,                SyncTimeout = 5000,                // 连接池配置                AllowAdmin = false,                ClientName = $"{Environment.MachineName}:{Guid.NewGuid()}"            };                        return ConnectionMultiplexer.Connect(configuration);        });    }        public static ConnectionMultiplexer Connection => _lazyConnection.Value;        public static IDatabase GetDatabase()    {        return Connection.GetDatabase();    }}

4. 监控与告警配置

在Asp.Net Core中实现完整的监控:

public class RedisMetricsCollector : BackgroundService{    private readonly IConnectionMultiplexer _redis;    private readonly ILogger<RedisMetricsCollector> _logger;    private readonly IMetricsPublisher _metricsPublisher;        protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        while (!stoppingToken.IsCancellationRequested)        {            try            {                var server = _redis.GetServer(_redis.GetEndPoints().First());                var info = await server.InfoAsync("all");                                // 收集关键指标                var metrics = new RedisMetrics                {                    Timestamp = DateTime.UtcNow,                    ConnectedClients = long.Parse(info.First(x => x.Key == "Clients")                        .First(x => x.Key == "connected_clients").Value),                    UsedMemory = long.Parse(info.First(x => x.Key == "Memory")                        .First(x => x.Key == "used_memory").Value),                    OpsPerSecond = long.Parse(info.First(x => x.Key == "Stats")                        .First(x => x.Key == "instantaneous_ops_per_sec").Value),                    HitRate = CalculateHitRate(info),                    NetworkInput = long.Parse(info.First(x => x.Key == "Stats")                        .First(x => x.Key == "total_net_input_bytes").Value),                    NetworkOutput = long.Parse(info.First(x => x.Key == "Stats")                        .First(x => x.Key == "total_net_output_bytes").Value)                };                                await _metricsPublisher.PublishAsync(metrics);                                // 检查告警条件                await CheckAlerts(metrics);            }            catch (Exception ex)            {                _logger.LogError(ex, "收集Redis指标时发生错误");            }                        await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);        }    }        private double CalculateHitRate(ILookup<string, KeyValuePair<string, string>> info)    {        var hits = long.Parse(info.First(x => x.Key == "Stats")            .First(x => x.Key == "keyspace_hits").Value);        var misses = long.Parse(info.First(x => x.Key == "Stats")            .First(x => x.Key == "keyspace_misses").Value);                    return hits + misses == 0 ? 0 : (double)hits / (hits + misses);    }        private async Task CheckAlerts(RedisMetrics metrics)    {        // 内存使用率超过80%        if (metrics.UsedMemory > 0.8 * 1024 * 1024 * 1024) // 假设1GB内存        {            _logger.LogWarning("Redis内存使用率过高: {UsedMemory} bytes", metrics.UsedMemory);        }                // 命中率低于90%        if (metrics.HitRate < 0.9)        {            _logger.LogWarning("Redis缓存命中率过低: {HitRate:P2}", metrics.HitRate);        }    }}

三、Redis生态与工具介绍

1. 常用Redis可视化工具

RedisInsight(官方推荐)

  • 功能全面的GUI工具
  • 支持数据浏览、CLI操作、性能监控
  • 免费使用,跨平台支持

Another Redis Desktop Manager

  • 开源免费的桌面管理器
  • 直观的界面,支持多种数据类型展示
  • 跨平台支持

Redis Commander

  • 基于Web的管理界面
  • 适合部署在服务器环境
  • 轻量级,功能齐全

2. 监控与运维平台

Prometheus + Grafana

# Redis Exporter配置scrape_configs:  - job_name: 'redis'    static_configs:      - targets: ['redis-exporter:9121']    metrics_path: /scrape    params:      target: ['redis-server:6379']

DataDog / New Relic

  • 商业APM工具
  • 提供深度性能分析和告警
  • 企业级功能支持

3. Redis模块系统简介

Redis 4.0引入了模块系统,允许开发者扩展Redis功能:

RedisJSON

  • 原生支持JSON文档
  • 提供JSONPath查询语法
# 存储和查询JSON文档127.0.0.1:6379> JSON.SET user:1001 $ '{"name":"Alice","age":30}'127.0.0.1:6379> JSON.GET user:1001 $.name

RedisSearch

  • 全文搜索功能
  • 二级索引支持
# 创建全文搜索索引127.0.0.1:6379> FT.CREATE productIdx ON HASH PREFIX 1 product: SCHEMA name TEXT WEIGHT 5.0 description TEXT

RedisBloom

  • 概率数据结构
  • 布隆过滤器、基数估算等
# 使用布隆过滤器127.0.0.1:6379> BF.ADD visited:users user123127.0.0.1:6379> BF.EXISTS visited:users user123

RedisTimeSeries

  • 时间序列数据处理
  • 支持聚合和降采样
# 存储时间序列数据127.0.0.1:6379> TS.ADD temperature:room1 1620000000 25.5127.0.0.1:6379> TS.RANGE temperature:room1 1620000000 1620003600

四、Redis未来发展趋势

1. Redis 7.0+ 新特性

Functions(替代Lua脚本)

#!lua name=mylibredis.register_function('my_hset', function(keys, args)    return redis.call('HSET', keys[1], args[1], args[2])end)

ACL增强

  • 更细粒度的权限控制
  • 键模式权限管理
  • 用户角色管理

性能优化

  • 多线程I/O(非数据操作)
  • 更高效的内存管理
  • 改进的集群性能

2. 云原生与Kubernetes集成

Redis Operator

  • 自动化Redis集群部署
  • 故障自愈和弹性伸缩
  • 备份和恢复管理

服务网格集成

  • 与Istio、Linkerd的深度集成
  • 智能流量路由和负载均衡
  • 可观测性增强

3. AI与机器学习集成

向量搜索

# 使用Redis作为向量数据库127.0.0.1:6379> FT.CREATE vec_idx ON HASH PREFIX 1 vec: SCHEMA vector VECTOR127.0.0.1:6379> HSET vec:1 vector "0.1,0.2,0.3"

实时特征存储

  • 机器学习特征工程
  • 在线推理数据准备
  • 实时推荐系统

五、Redis的局限性及替代方案

虽然Redis功能强大,但也有其局限性:

不适合使用Redis的场景

大量数据存储

  • Redis主要依赖内存,成本较高
  • 替代方案:Cassandra、HBase

复杂查询和分析

  • Redis查询能力相对有限
  • 替代方案:Elasticsearch、ClickHouse

强一致性事务

  • Redis事务非ACID兼容
  • 替代方案:关系型数据库

新兴竞品分析

KeyDB

  • Redis的多线程版本
  • 更好的多核CPU利用率
  • 完全兼容Redis协议

Dragonfly

  • 新型高性能内存数据库
  • 声称比Redis快25倍
  • 创新的数据结构设计

AWS ElastiCache for Redis

  • 托管Redis服务
  • 自动备份、故障转移
  • 企业级功能支持

六、结语:持续学习的建议

通过这个系列的学习,你已经建立了坚实的Redis知识体系。但技术的道路永无止境,以下是一些持续学习的建议:

1. 实践是最好的老师

  • 在自己的项目中积极应用Redis
  • 尝试解决真实世界的性能问题
  • 参与开源项目,阅读优秀的Redis使用案例

2. 关注社区动态

  • 关注Redis官方博客和GitHub仓库
  • 参与Redis Conf等技术大会
  • 加入相关的技术社区和论坛

3. 深入原理研究

  • 阅读《Redis设计与实现》
  • 分析Redis源码,理解内部机制
  • 尝试自己实现简单的内存数据库

4. 拓展技术视野

  • 学习其他类型的数据库(关系型、文档型、图数据库等)
  • 了解分布式系统理论
  • 掌握云原生技术栈

最后的思考

Redis不仅仅是一个缓存工具,它已经发展成为现代应用架构中的多功能数据平台。从简单的键值存储到复杂的数据结构服务,从单机部署到全球分布式集群,Redis一直在演进。

记住这个核心理念:

“选择合适的工具解决正确的问题,并深入理解你所使用的工具。”

希望这个Redis系列教程能够成为你技术成长道路上有价值的参考资料。无论你是初学者还是经验丰富的开发者,对Redis的深入理解都将为你的职业生涯带来显著的提升。

感谢你坚持学完这个系列!如果在学习过程中有任何疑问或心得,欢迎在评论区分享交流。技术的道路需要同行者,让我们共同进步!


“学无止境,实践出真知。愿你在技术的道路上越走越远,不断突破自我!”


这个完整的Redis系列教程到这里就全部结束了。从基础概念到生产实践,从简单使用到深度优化,希望这个系列能够成为你在Redis学习道路上的得力助手。祝你编程愉快,技术精进!

🔲 ☆

Redis入门:进阶知识与运维管理

Redis进阶知识与运维管理:构建生产级应用

在前几篇中,我们已经掌握了Redis的核心概念和基本应用。但当Redis真正走向生产环境时,我们需要面对更复杂的挑战:如何优化内存使用?如何保证集群的高可用?如何监控和调优性能?今天,我们将深入Redis的进阶主题,帮助你构建真正稳定、高效的Redis应用。

一、Redis内存优化与淘汰策略

1. Redis内存消耗深度分析

在生产环境中,理解Redis内存使用情况至关重要。让我们从几个关键指标开始:

# 查看详细内存信息127.0.0.1:6379> INFO memory# Memoryused_memory:1024000used_memory_human:1000.00Kused_memory_rss:2048000used_memory_peak:1048576used_memory_peak_human:1.00Mused_memory_lua:37888mem_fragmentation_ratio:2.00mem_allocator:jemalloc-5.1.0

关键指标解读:

  • used_memory:Redis分配器分配的内存总量(字节)
  • used_memory_rss:从操作系统角度显示Redis进程占用的物理内存
  • mem_fragmentation_ratio:内存碎片率 = used_memory_rss / used_memory
    • 1.0 - 1.5:良好状态
    • 1.5 - 2.0:需要关注
    • 2.0:严重碎片,考虑重启

2. 内存优化实战技巧

a) 缩短键值对长度

// 不推荐 - 键名过长await _database.StringSetAsync("user:session:1001:shopping:cart:items", cartData);// 推荐 - 精简键名await _database.StringSetAsync("u:1001:cart", cartData);// 对于值,考虑使用压缩public async Task SetCompressedAsync(string key, string value, TimeSpan? expiry = null){    var compressedBytes = CompressString(value);    await _database.StringSetAsync(key, compressedBytes, expiry);}public async Task<string> GetCompressedAsync(string key){    var bytes = (byte[]?)await _database.StringGetAsync(key);    return bytes != null ? DecompressString(bytes) : null;}

b) 使用适当的数据结构编码

Redis会自动为小规模数据选择更高效的编码方式:

# 查看Key的编码方式127.0.0.1:6379> OBJECT ENCODING user:1001"hashtable"127.0.0.1:6379> OBJECT ENCODING small:hash"ziplist"

优化配置(在redis.conf中):

# Hash配置 - 当字段数≤512且所有值≤64字节时使用ziplisthash-max-ziplist-entries 512hash-max-ziplist-value 64# List配置list-max-ziplist-size -2# Set配置 - 当元素都是整数且数量≤512时使用intsetset-max-intset-entries 512# Sorted Set配置zset-max-ziplist-entries 128zset-max-ziplist-value 64

c) 使用位图和HyperLogLog

对于特定场景,使用特殊数据结构可以大幅节省内存:

// 位图 - 用户签到系统public class SignInService{    private readonly IDatabase _database;        public async Task SignInAsync(int userId, DateTime date)    {        var key = $"signin:{userId}:{date:yyyyMM}";        var offset = date.Day - 1; // 0-30                await _database.StringSetBitAsync(key, offset, true);    }        public async Task<int> GetSignInCountAsync(int userId, int year, int month)    {        var key = $"signin:{userId}:{year:0000}{month:00}";        // 使用BITCOUNT统计签到天数        return (int)await _database.StringBitCountAsync(key);    }}// HyperLogLog - 统计UV(独立访客)public class VisitorService{    public async Task AddVisitorAsync(string pageId, string visitorId)    {        var key = $"uv:{pageId}";        await _database.HyperLogLogAddAsync(key, visitorId);    }        public async Task<long> GetVisitorCountAsync(string pageId)    {        var key = $"uv:{pageId}";        return await _database.HyperLogLogLengthAsync(key);    }}

3. 内存淘汰策略详解

当内存达到上限时,Redis提供了8种淘汰策略:

# redis.conf配置maxmemory 1gbmaxmemory-policy allkeys-lru

淘汰策略对比:

策略作用范围淘汰机制适用场景
noeviction-不淘汰,返回错误数据绝对不能丢失
allkeys-lru所有Key最近最少使用通用场景
volatile-lru过期Key最近最少使用部分数据可丢失
allkeys-random所有Key随机淘汰访问模式随机
volatile-random过期Key随机淘汰部分数据可丢失
volatile-ttl过期Key剩余时间最短需要优先淘汰旧数据
allkeys-lfu所有Key最不经常使用访问频率差异大
volatile-lfu过期Key最不经常使用部分数据可丢失

生产环境推荐:

# 对于缓存场景maxmemory-policy allkeys-lru# 对于混合使用(缓存+持久化数据)maxmemory-policy volatile-lru

二、Redis集群(Cluster)模式:走向分布式

1. 为什么需要Cluster?

当面临以下场景时,单机Redis无法满足需求:

  • 数据量超过单机内存容量
  • 写并发超过单机处理能力
  • 需要更高的可用性保障

2. Hash Slot(哈希槽)分片原理

Redis Cluster采用虚拟槽分区,共有16384个槽:

  • 每个Key通过CRC16哈希后对16384取模,得到对应的槽
  • 每个节点负责一部分槽的范围
  • 支持动态重新分片
# 计算Key的槽位置127.0.0.1:6379> CLUSTER KEYSLOT "user:1001"(integer) 14982

3. 搭建6节点Redis Cluster

集群规划:

  • 3个主节点:7000, 7001, 7002
  • 3个从节点:7003, 7004, 7005

创建节点配置文件:

redis-7000.conf:

port 7000cluster-enabled yescluster-config-file nodes-7000.confcluster-node-timeout 15000appendonly yesappendfilename "appendonly-7000.aof"dbfilename dump-7000.rdblogfile "redis-7000.log"

重复创建7001-7005的配置文件。

启动所有节点:

redis-server redis-7000.confredis-server redis-7001.conf# ... 启动所有6个节点

创建集群:

redis-cli --cluster create \  127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \  127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \  --cluster-replicas 1

4. 集群管理与运维

查看集群状态:

# 查看集群节点信息redis-cli -p 7000 cluster nodes# 查看集群信息redis-cli -p 7000 cluster info# 查看槽分配情况redis-cli -p 7000 cluster slots

节点管理:

# 添加新主节点redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000# 添加新从节点redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000 --cluster-slave --cluster-master-id <master-node-id># 重新分片redis-cli --cluster reshard 127.0.0.1:7000# 修复节点redis-cli --cluster fix 127.0.0.1:7000

5. 在Asp.Net Core中连接集群

public static class RedisClusterServiceExtensions{    public static IServiceCollection AddRedisCluster(this IServiceCollection services, IConfiguration configuration)    {        var redisOptions = new ConfigurationOptions        {            EndPoints =             {                { "127.0.0.1", 7000 },                { "127.0.0.1", 7001 },                { "127.0.0.1", 7002 },                { "127.0.0.1", 7003 },                { "127.0.0.1", 7004 },                { "127.0.0.1", 7005 }            },            Password = configuration["Redis:Password"],            AbortOnConnectFail = false,            ConnectRetry = 3,            ConnectTimeout = 5000,            SyncTimeout = 5000        };        services.AddSingleton<IConnectionMultiplexer>(sp =>             ConnectionMultiplexer.Connect(redisOptions)        );        return services;    }}

三、性能调优与监控

1. 使用INFO命令深度监控

public class RedisMonitorService{    private readonly IConnectionMultiplexer _redis;        public async Task<RedisMetrics> GetMetricsAsync()    {        var database = _redis.GetDatabase();        var server = _redis.GetServer(_redis.GetEndPoints().First());                var info = await server.InfoAsync("all");                return new RedisMetrics        {            ConnectedClients = info.First(x => x.Key == "Clients").First(x => x.Key == "connected_clients").Value,            UsedMemory = info.First(x => x.Key == "Memory").First(x => x.Key == "used_memory").Value,            OpsPerSecond = info.First(x => x.Key == "Stats").First(x => x.Key == "instantaneous_ops_per_sec").Value,            KeyspaceHits = info.First(x => x.Key == "Stats").First(x => x.Key == "keyspace_hits").Value,            KeyspaceMisses = info.First(x => x.Key == "Stats").First(x => x.Key == "keyspace_misses").Value,            NetworkInput = info.First(x => x.Key == "Stats").First(x => x.Key == "total_net_input_bytes").Value,            NetworkOutput = info.First(x => x.Key == "Stats").First(x => x.Key == "total_net_output_bytes").Value        };    }        public double CalculateHitRate(RedisMetrics metrics)    {        var hits = long.Parse(metrics.KeyspaceHits);        var misses = long.Parse(metrics.KeyspaceMisses);        return hits + misses == 0 ? 0 : (double)hits / (hits + misses);    }}public record RedisMetrics{    public string ConnectedClients { get; init; }    public string UsedMemory { get; init; }    public string OpsPerSecond { get; init; }    public string KeyspaceHits { get; init; }    public string KeyspaceMisses { get; init; }    public string NetworkInput { get; init; }    public string NetworkOutput { get; init; }}

2. Slow Log(慢查询日志)分析与优化

配置慢查询日志:

# redis.conf配置slowlog-log-slower-than 10000  # 超过10毫秒的记录slowlog-max-len 1000           # 最多保存1000条慢查询

分析慢查询:

# 查看慢查询日志127.0.0.1:6379> SLOWLOG GET 101) 1) (integer) 14               # 日志ID   2) (integer) 1600000000       # 时间戳   3) (integer) 15000            # 执行时间(微秒)   4) 1) "KEYS"                  # 命令      2) "user:*:session"   5) "127.0.0.1:58234"          # 客户端   6) ""                         # 客户端名称

优化建议:

  • 避免使用KEYS命令,使用SCAN替代
  • 对大集合的操作进行分片
  • 使用Pipeline减少网络往返

3. Pipeline(管道)提升性能

public class RedisPipelineService{    private readonly IDatabase _database;        public async Task<List<object>> BatchGetAsync(List<string> keys)    {        var batch = _database.CreateBatch();                var tasks = new List<Task<RedisValue>>();        foreach (var key in keys)        {            tasks.Add(batch.StringGetAsync(key));        }                batch.Execute();        var results = await Task.WhenAll(tasks);                return results.Select(r => (object)r).ToList();    }        public async Task BatchSetAsync(Dictionary<string, string> keyValues)    {        var batch = _database.CreateBatch();                var tasks = new List<Task>();        foreach (var kv in keyValues)        {            tasks.Add(batch.StringSetAsync(kv.Key, kv.Value));        }                batch.Execute();        await Task.WhenAll(tasks);    }}

4. Lua脚本实现复杂原子操作

public class RedisLuaService{    private readonly IDatabase _database;        // 实现分布式限流    public async Task<bool> RateLimitAsync(string key, int maxRequests, TimeSpan window)    {        var luaScript = @"            local key = KEYS[1]            local max_requests = tonumber(ARGV[1])            local window = tonumber(ARGV[2])            local current_time = tonumber(ARGV[3])                        -- 移除时间窗口之外的请求            redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)                        -- 获取当前请求数量            local current_requests = redis.call('ZCARD', key)                        if current_requests >= max_requests then                return 0            end                        -- 添加当前请求            redis.call('ZADD', key, current_time, current_time)            redis.call('EXPIRE', key, window)            return 1        ";                var result = await _database.ScriptEvaluateAsync(            luaScript,             new RedisKey[] { key },             new RedisValue[] { maxRequests, window.TotalSeconds, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }        );                return (int)result == 1;    }        // 实现原子性的库存扣减    public async Task<bool> DeductStockAsync(string stockKey, int quantity)    {        var luaScript = @"            local stock_key = KEYS[1]            local quantity = tonumber(ARGV[1])                        local current_stock = tonumber(redis.call('GET', stock_key) or '0')                        if current_stock < quantity then                return 0            end                        redis.call('DECRBY', stock_key, quantity)            return 1        ";                var result = await _database.ScriptEvaluateAsync(            luaScript,            new RedisKey[] { stockKey },            new RedisValue[] { quantity }        );                return (int)result == 1;    }}

四、备份与恢复策略

1. 自动化备份方案

public class RedisBackupService{    private readonly IConnectionMultiplexer _redis;    private readonly ILogger<RedisBackupService> _logger;        public async Task<bool> CreateRdbBackupAsync(string backupPath)    {        try        {            var server = _redis.GetServer(_redis.GetEndPoints().First());                        // 执行BGSAVE            await server.SaveAsync(SaveType.BackgroundSave);                        _logger.LogInformation("RDB备份创建成功");            return true;        }        catch (Exception ex)        {            _logger.LogError(ex, "RDB备份创建失败");            return false;        }    }        public async Task<string> CreateAofBackupAsync()    {        try        {            var server = _redis.GetServer(_redis.GetEndPoints().First());                        // 执行AOF重写            await server.SaveAsync(SaveType.AppendOnlyFileRewrite);                        _logger.LogInformation("AOF备份创建成功");            return "success";        }        catch (Exception ex)        {            _logger.LogError(ex, "AOF备份创建失败");            return "failed";        }    }}

2. 备份验证与恢复测试

定期验证备份文件的完整性和可恢复性:

# 验证RDB文件redis-check-rdb dump.rdb# 验证AOF文件redis-check-aof appendonly.aof# 修复AOF文件redis-check-aof --fix appendonly.aof

五、安全配置最佳实践

1. 网络安全配置

# redis.conf安全配置# 绑定IP地址bind 127.0.0.1 10.0.0.1# 保护模式protected-mode yes# 认证密码requirepass "YourStrongPassword123!"# 重命名危险命令rename-command FLUSHDB ""rename-command FLUSHALL ""rename-command CONFIG "CONFIG_SECRET"rename-command SHUTDOWN "SHUTDOWN_SECRET"

2. 在Asp.Net Core中安全连接

public static class SecureRedisConfiguration{    public static IServiceCollection AddSecureRedis(this IServiceCollection services, IConfiguration configuration)    {        var redisConfig = new ConfigurationOptions        {            EndPoints = { configuration["Redis:Endpoint"] },            Password = configuration["Redis:Password"],            Ssl = bool.Parse(configuration["Redis:UseSsl"] ?? "false"),            AbortOnConnectFail = false,            ConnectRetry = 3,            ConnectTimeout = 5000,            SyncTimeout = 5000        };                // 添加客户端名称便于审计        redisConfig.ClientName = $"{Environment.MachineName}:{Guid.NewGuid()}";                services.AddSingleton<IConnectionMultiplexer>(sp =>             ConnectionMultiplexer.Connect(redisConfig)        );                return services;    }}

六、故障诊断与问题排查

1. 常见问题诊断命令

# 查看客户端连接127.0.0.1:6379> CLIENT LIST# 查看内存详情127.0.0.1:6379> MEMORY STATS# 查看大Key127.0.0.1:6379> MEMORY USAGE keyname# 监控实时命令127.0.0.1:6379> MONITOR# 查看延迟redis-cli --latency -h host -p port

2. 在Asp.Net Core中实现健康检查

public class RedisHealthCheck : IHealthCheck{    private readonly IConnectionMultiplexer _redis;        public RedisHealthCheck(IConnectionMultiplexer redis)    {        _redis = redis;    }        public async Task<HealthCheckResult> CheckHealthAsync(        HealthCheckContext context,         CancellationToken cancellationToken = default)    {        try        {            if (!_redis.IsConnected)                return HealthCheckResult.Unhealthy("Redis连接已断开");                            var database = _redis.GetDatabase();            var pong = await database.PingAsync();                        if (pong > TimeSpan.FromMilliseconds(1000))                return HealthCheckResult.Degraded($"Redis响应缓慢: {pong.TotalMilliseconds}ms");                            return HealthCheckResult.Healthy($"Redis连接正常: {pong.TotalMilliseconds}ms");        }        catch (Exception ex)        {            return HealthCheckResult.Unhealthy("Redis健康检查失败", ex);        }    }}// 注册健康检查builder.Services.AddHealthChecks()    .AddCheck<RedisHealthCheck>("redis");

总结

通过本篇的深入学习,我们掌握了构建生产级Redis应用所需的关键知识:

  1. 内存优化:通过合理的数据结构选择、编码配置和淘汰策略,最大化内存利用率
  2. 集群部署:理解哈希槽分片原理,掌握集群的搭建、管理和扩展
  3. 性能调优:利用监控工具、Pipeline和Lua脚本提升系统性能
  4. 备份恢复:建立可靠的备份策略,确保数据安全
  5. 安全配置:从网络、认证、命令等多个维度保障Redis安全
  6. 故障诊断:掌握常见问题的排查方法和健康监控

关键收获:

  • 生产环境的Redis需要综合考虑性能、可用性、安全性和可维护性
  • 监控和预警是保障服务稳定的关键
  • 合理的数据结构和配置可以大幅提升系统性能
  • 安全配置不是可选项,而是生产部署的必备条件

现在,你已经具备了构建和管理生产级Redis应用的全套技能,可以自信地应对各种复杂的业务场景和运维挑战!


系列总结

通过这五篇系列教程,我们从Redis的基础概念开始,逐步深入到了数据结构、持久化、Asp.Net Core集成、高级特性和生产运维。希望这个完整的系列能够帮助你在实际项目中充分发挥Redis的威力,构建出高性能、高可用的应用系统。

记住,技术的学习永无止境,保持好奇心和实践精神,你将在技术的道路上越走越远!


延伸学习建议:

  1. 深入了解Redis模块系统(RedisJSON、RedisSearch等)
  2. 学习Redis Streams实现更复杂的消息处理模式
  3. 探索Redis在微服务架构中的服务发现和配置管理应用
  4. 研究Redis在实时数据分析场景的应用

欢迎在评论区分享你在生产环境中使用Redis的经验和挑战!

🔲 ☆

Redis入门:Redis在项目中的实战应用

Redis在Asp.Net Core项目中的实战应用:从缓存到分布式锁

通过前几篇的学习,我们已经掌握了Redis的核心概念和数据持久化。但理论终究要落地到实践。今天,我们将把Redis真正集成到Asp.Net Core项目中,解决真实业务场景中的性能瓶颈和分布式难题。

在现代Web开发中,Redis早已不是可选项,而是构建高性能、高可用系统的必备组件。作为.Net开发者,掌握如何在Asp.Net Core中熟练使用Redis,是你进阶高级开发的必经之路。

一、环境准备:在Asp.Net Core中集成Redis

1. 安装必要的NuGet包

首先,在你的Asp.Net Core项目中安装最常用的Redis客户端:

# 使用Package Manager ConsoleInstall-Package StackExchange.Redis# 或使用.NET CLIdotnet add package StackExchange.Redis

为什么选择StackExchange.Redis?

  • 高性能且线程安全
  • 支持同步和异步操作
  • 活跃的社区支持和持续更新
  • Microsoft官方推荐

2. 配置Redis服务

appsettings.json中添加Redis连接字符串:

{  "ConnectionStrings": {    "Redis": "localhost:6379,password=your_password,abortConnect=false,connectTimeout=30000"  },  // 其他配置...}

Program.cs中注册Redis服务:

using StackExchange.Redis;var builder = WebApplication.CreateBuilder(args);// 添加Redis服务builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>     ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")));// 注册自定义Redis服务(推荐)builder.Services.AddScoped<IRedisService, RedisService>();var app = builder.Build();

3. 创建Redis服务封装

为了更好地使用Redis,我们创建一个服务封装类:

public interface IRedisService{    Task<T> GetAsync<T>(string key);    Task SetAsync<T>(string key, T value, TimeSpan? expiry = null);    Task<bool> RemoveAsync(string key);    Task<bool> ExistsAsync(string key);}public class RedisService : IRedisService{    private readonly IConnectionMultiplexer _redis;    private readonly IDatabase _database;    public RedisService(IConnectionMultiplexer redis)    {        _redis = redis;        _database = redis.GetDatabase();    }    public async Task<T> GetAsync<T>(string key)    {        var value = await _database.StringGetAsync(key);        if (value.IsNullOrEmpty)            return default;        return JsonSerializer.Deserialize<T>(value);    }    public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)    {        var serializedValue = JsonSerializer.Serialize(value);        await _database.StringSetAsync(key, serializedValue, expiry);    }    public async Task<bool> RemoveAsync(string key)    {        return await _database.KeyDeleteAsync(key);    }    public async Task<bool> ExistsAsync(string key)    {        return await _database.KeyExistsAsync(key);    }}

现在,我们的基础环境已经搭建完成,可以开始实战应用了!

二、缓存实战:提升系统性能的利器

缓存是Redis最经典的应用场景。让我们来看几个实际的例子。

1. 商品信息缓存

假设我们有一个电商系统,商品信息的查询非常频繁:

public interface IProductService{    Task<Product> GetProductByIdAsync(int productId);    Task UpdateProductAsync(Product product);}public class ProductService : IProductService{    private readonly IProductRepository _productRepository;    private readonly IRedisService _redisService;    private readonly ILogger<ProductService> _logger;    public ProductService(IProductRepository productRepository,                          IRedisService redisService,                         ILogger<ProductService> logger)    {        _productRepository = productRepository;        _redisService = redisService;        _logger = logger;    }    public async Task<Product> GetProductByIdAsync(int productId)    {        var cacheKey = $"product:{productId}";                // 1. 先查缓存        var product = await _redisService.GetAsync<Product>(cacheKey);        if (product != null)        {            _logger.LogInformation("从缓存获取商品 {ProductId}", productId);            return product;        }        // 2. 缓存不存在,查询数据库        _logger.LogInformation("缓存未命中,从数据库查询商品 {ProductId}", productId);        product = await _productRepository.GetByIdAsync(productId);        if (product == null)            return null;        // 3. 写入缓存,设置30分钟过期        await _redisService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));                return product;    }    public async Task UpdateProductAsync(Product product)    {        // 更新数据库        await _productRepository.UpdateAsync(product);                // 删除缓存,保证数据一致性        var cacheKey = $"product:{product.Id}";        await _redisService.RemoveAsync(cacheKey);                _logger.LogInformation("更新商品 {ProductId} 并清除缓存", product.Id);    }}

缓存策略分析

  • 读取时:先查缓存,命中则返回;未命中查数据库并回写缓存
  • 更新时:先更新数据库,再删除缓存(Cache-Aside模式)
  • 过期时间:设置合理的过期时间,防止数据长期不更新

2. 在Controller中使用缓存服务

[ApiController][Route("api/[controller]")]public class ProductsController : ControllerBase{    private readonly IProductService _productService;    public ProductsController(IProductService productService)    {        _productService = productService;    }    [HttpGet("{id}")]    public async Task<ActionResult<Product>> GetProduct(int id)    {        var product = await _productService.GetProductByIdAsync(id);        if (product == null)            return NotFound();                    return product;    }    [HttpPut("{id}")]    public async Task<IActionResult> UpdateProduct(int id, Product product)    {        if (id != product.Id)            return BadRequest();                    await _productService.UpdateProductAsync(product);        return NoContent();    }}

三、应对缓存"三剑客":穿透、击穿、雪崩

在实际生产环境中,仅仅实现基础缓存是不够的,我们还需要应对三个经典问题。

1. 缓存穿透:查询不存在的数据

问题:恶意请求查询数据库中不存在的数据,导致请求直接打到数据库。

解决方案:缓存空对象

public async Task<Product> GetProductByIdWithNullCacheAsync(int productId){    var cacheKey = $"product:{productId}";        var product = await _redisService.GetAsync<Product>(cacheKey);    if (product != null)    {        // 如果是特殊的空对象标记,返回null        if (product.Id == -1)            return null;                    return product;    }    product = await _productRepository.GetByIdAsync(productId);    if (product == null)    {        // 缓存空对象,设置较短的过期时间        var nullProduct = new Product { Id = -1 }; // 特殊标记        await _redisService.SetAsync(cacheKey, nullProduct, TimeSpan.FromMinutes(5));        return null;    }    await _redisService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));    return product;}

2. 缓存击穿:热点Key突然失效

问题:某个热点Key在失效的瞬间,大量请求同时到达数据库。

解决方案:使用互斥锁

public async Task<Product> GetProductWithMutexAsync(int productId){    var cacheKey = $"product:{productId}";    var mutexKey = $"mutex:product:{productId}";        // 尝试获取缓存    var product = await _redisService.GetAsync<Product>(cacheKey);    if (product != null)        return product;    // 使用Redis实现分布式锁    var lockToken = Guid.NewGuid().ToString();    var locked = await _redisService.AcquireLockAsync(mutexKey, lockToken, TimeSpan.FromSeconds(5));        if (!locked)    {        // 获取锁失败,稍后重试        await Task.Delay(100);        return await GetProductWithMutexAsync(productId);    }    try    {        // 双重检查,防止重复查询数据库        product = await _redisService.GetAsync<Product>(cacheKey);        if (product != null)            return product;        // 查询数据库        product = await _productRepository.GetByIdAsync(productId);        if (product != null)        {            await _redisService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));        }                return product;    }    finally    {        // 释放锁        await _redisService.ReleaseLockAsync(mutexKey, lockToken);    }}

3. 缓存雪崩:大量Key同时失效

问题:大量缓存Key在同一时间失效,导致所有请求直接访问数据库。

解决方案:设置不同的过期时间

public async Task SetWithRandomExpiryAsync<T>(string key, T value, TimeSpan baseExpiry){    // 在基础过期时间上增加随机偏差(±10%)    var random = new Random();    var variance = (int)(baseExpiry.TotalMinutes * 0.1); // 10% 偏差    var actualExpiry = baseExpiry.Add(TimeSpan.FromMinutes(random.Next(-variance, variance)));        await _redisService.SetAsync(key, value, actualExpiry);}

四、分布式锁:控制分布式环境下的资源访问

在分布式系统中,我们需要控制多个服务实例对共享资源的访问。

1. 扩展Redis服务支持分布式锁

public interface IRedisService{    // ... 其他方法        Task<bool> AcquireLockAsync(string key, string value, TimeSpan expiry);    Task<bool> ReleaseLockAsync(string key, string value);    Task<bool> ExtendLockAsync(string key, string value, TimeSpan expiry);}public class RedisService : IRedisService{    // ... 其他实现    public async Task<bool> AcquireLockAsync(string key, string value, TimeSpan expiry)    {        // 使用SET NX EX命令原子性地获取锁        return await _database.StringSetAsync(key, value, expiry, When.NotExists);    }    public async Task<bool> ReleaseLockAsync(string key, string value)    {        // 使用Lua脚本保证原子性:只有锁的值匹配时才删除        var luaScript = @"            if redis.call('GET', KEYS[1]) == ARGV[1] then                return redis.call('DEL', KEYS[1])            else                return 0            end";        var result = await _database.ScriptEvaluateAsync(luaScript, new RedisKey[] { key }, new RedisValue[] { value });        return (int)result == 1;    }    public async Task<bool> ExtendLockAsync(string key, string value, TimeSpan expiry)    {        var luaScript = @"            if redis.call('GET', KEYS[1]) == ARGV[1] then                return redis.call('EXPIRE', KEYS[1], ARGV[2])            else                return 0            end";        var result = await _database.ScriptEvaluateAsync(luaScript,             new RedisKey[] { key },             new RedisValue[] { value, (int)expiry.TotalSeconds });                    return (bool)result;    }}

2. 使用分布式锁实现秒杀功能

public class SeckillService{    private readonly IRedisService _redisService;    private readonly IOrderRepository _orderRepository;    public SeckillService(IRedisService redisService, IOrderRepository orderRepository)    {        _redisService = redisService;        _orderRepository = orderRepository;    }    public async Task<bool> ProcessSeckillAsync(int productId, int userId)    {        var lockKey = $"seckill:lock:{productId}";        var lockValue = Guid.NewGuid().ToString();        var stockKey = $"product:stock:{productId}";        try        {            // 获取分布式锁            var locked = await _redisService.AcquireLockAsync(lockKey, lockValue, TimeSpan.FromSeconds(10));            if (!locked)                return false; // 获取锁失败,稍后重试            // 检查库存            var stock = await _redisService.GetAsync<int>(stockKey);            if (stock <= 0)                return false;            // 扣减库存            await _redisService.SetAsync(stockKey, stock - 1);            // 创建订单            await _orderRepository.CreateAsync(new Order            {                ProductId = productId,                UserId = userId,                CreatedAt = DateTime.UtcNow            });            return true;        }        finally        {            // 释放锁            await _redisService.ReleaseLockAsync(lockKey, lockValue);        }    }}

五、会话存储:实现分布式Session

在微服务架构中,我们需要在多台服务器之间共享用户会话状态。

1. 配置Redis作为分布式Session存储

Program.cs中:

// 添加Redis分布式缓存builder.Services.AddStackExchangeRedisCache(options =>{    options.Configuration = builder.Configuration.GetConnectionString("Redis");    options.InstanceName = "MyApp_";});// 配置Sessionbuilder.Services.AddSession(options =>{    options.IdleTimeout = TimeSpan.FromMinutes(30);    options.Cookie.HttpOnly = true;    options.Cookie.IsEssential = true;});

在Controller中使用:

public class AccountController : Controller{    [HttpPost]    public async Task<IActionResult> Login(LoginModel model)    {        // 验证用户...        var user = await AuthenticateUserAsync(model);        if (user == null)            return Unauthorized();        // 存储用户信息到Session        HttpContext.Session.SetString("UserId", user.Id.ToString());        HttpContext.Session.SetString("UserName", user.UserName);        HttpContext.Session.SetInt32("UserRole", (int)user.Role);        return RedirectToAction("Index", "Home");    }    [HttpGet]    public IActionResult GetUserInfo()    {        if (!HttpContext.Session.TryGetValue("UserId", out _))            return Unauthorized();        var userInfo = new        {            UserId = HttpContext.Session.GetString("UserId"),            UserName = HttpContext.Session.GetString("UserName"),            Role = HttpContext.Session.GetInt32("UserRole")        };        return Ok(userInfo);    }}

六、消息队列:实现异步任务处理

虽然Redis不是专业的消息队列,但对于简单的场景非常实用。

1. 基于List实现简单消息队列

public interface IMessageQueueService{    Task PublishAsync<T>(string queueName, T message);    Task<T> ConsumeAsync<T>(string queueName, TimeSpan? timeout = null);}public class RedisMessageQueueService : IMessageQueueService{    private readonly IConnectionMultiplexer _redis;    private readonly IDatabase _database;    public RedisMessageQueueService(IConnectionMultiplexer redis)    {        _redis = redis;        _database = redis.GetDatabase();    }    public async Task PublishAsync<T>(string queueName, T message)    {        var serializedMessage = JsonSerializer.Serialize(message);        await _database.ListLeftPushAsync(queueName, serializedMessage);    }    public async Task<T> ConsumeAsync<T>(string queueName, TimeSpan? timeout = null)    {        var value = await _database.ListRightPopAsync(queueName);        if (value.IsNullOrEmpty)        {            if (timeout.HasValue)            {                // 可以实现阻塞版本的消费                // 这里简化处理,返回默认值                await Task.Delay(timeout.Value);            }            return default;        }        return JsonSerializer.Deserialize<T>(value);    }}

2. 使用消息队列处理耗时任务

public class EmailService{    private readonly IMessageQueueService _messageQueue;    private readonly ILogger<EmailService> _logger;    public EmailService(IMessageQueueService messageQueue, ILogger<EmailService> logger)    {        _messageQueue = messageQueue;        _logger = logger;    }    // 发送邮件到队列(非阻塞)    public async Task SendWelcomeEmailAsync(string email, string userName)    {        var emailMessage = new EmailMessage        {            To = email,            Subject = "欢迎注册",            Body = $"亲爱的 {userName},欢迎使用我们的服务!"        };        await _messageQueue.PublishAsync("email_queue", emailMessage);        _logger.LogInformation("欢迎邮件已加入队列,收件人: {Email}", email);    }}// 后台服务处理队列中的邮件public class EmailBackgroundService : BackgroundService{    private readonly IMessageQueueService _messageQueue;    private readonly IEmailSender _emailSender;    private readonly ILogger<EmailBackgroundService> _logger;    public EmailBackgroundService(IMessageQueueService messageQueue,                                  IEmailSender emailSender,                                 ILogger<EmailBackgroundService> logger)    {        _messageQueue = messageQueue;        _emailSender = emailSender;        _logger = logger;    }    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        while (!stoppingToken.IsCancellationRequested)        {            try            {                var message = await _messageQueue.ConsumeAsync<EmailMessage>("email_queue");                if (message != null)                {                    await _emailSender.SendEmailAsync(message.To, message.Subject, message.Body);                    _logger.LogInformation("邮件发送成功: {To}", message.To);                }                else                {                    // 队列为空,等待一段时间                    await Task.Delay(1000, stoppingToken);                }            }            catch (Exception ex)            {                _logger.LogError(ex, "处理邮件队列时发生错误");                await Task.Delay(5000, stoppingToken); // 错误时等待更长时间            }        }    }}

七、性能优化与最佳实践

1. 连接复用

确保在整个应用程序中复用IConnectionMultiplexer实例:

// 在Program.cs中注册为Singletonbuilder.Services.AddSingleton<IConnectionMultiplexer>(sp =>     ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")));

2. 使用Pipeline批量操作

public async Task<bool> SetMultipleAsync(Dictionary<string, object> keyValuePairs, TimeSpan? expiry = null){    var batch = _database.CreateBatch();        var tasks = new List<Task>();    foreach (var kvp in keyValuePairs)    {        var serializedValue = JsonSerializer.Serialize(kvp.Value);        tasks.Add(batch.StringSetAsync(kvp.Key, serializedValue, expiry));    }        batch.Execute();    await Task.WhenAll(tasks);        return tasks.All(t => t.IsCompletedSuccessfully);}

3. 合理的序列化选择

// 对于简单类型,考虑使用更高效的序列化方式public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null){    // 对于字符串,直接存储,避免JSON序列化开销    await _database.StringSetAsync(key, value, expiry);}public async Task<string> GetStringAsync(string key){    return await _database.StringGetAsync(key);}

总结

通过本篇的实战演练,我们掌握了在Asp.Net Core项目中集成和使用Redis的完整方案:

  1. 环境搭建:配置StackExchange.Redis客户端
  2. 缓存应用:商品信息缓存及缓存策略
  3. 问题解决:应对缓存穿透、击穿、雪崩的完整方案
  4. 分布式锁:实现秒杀等并发控制场景
  5. 会话存储:配置分布式Session
  6. 消息队列:实现异步任务处理
  7. 性能优化:连接复用、批量操作等最佳实践

关键收获

  • Redis在Asp.Net Core中的集成非常简单直接
  • 合理使用缓存可以大幅提升系统性能
  • 分布式锁是解决并发问题的利器
  • 选择合适的序列化方式对性能有重要影响

现在,你可以自信地在你的Asp.Net Core项目中使用Redis来解决实际的性能瓶颈和分布式协调问题了!

欢迎在评论区分享你在集成过程中遇到的问题和解决方案!

🔲 ☆

Redis入门:Redis的持久化与高可用

Redis的持久化与高可用:如何避免"内存失忆"?

在之前的文章中,我们领略了Redis基于内存的极速性能。但这也引出了一个关键问题:如果服务器重启或宕机,内存中的数据岂不是会全部丢失? 今天,我们就来深入探讨Redis如何解决这个"阿喀琉斯之踵",以及如何构建高可用的Redis架构。

一、数据持久化:为什么需要"记忆备份"?

想象一下,Redis就像一个拥有"超强记忆力"的天才,但它的记忆只存在于脑海中(内存)。一旦受到冲击(重启/宕机),所有记忆都会消失。为了避免这种"失忆"悲剧,Redis提供了两种"记忆备份"机制:RDBAOF

持久化的本质:将内存中的数据以某种形式保存到磁盘中,确保在服务重启后能够恢复数据。

二、RDB持久化:给数据拍"快照"

1. 工作原理

RDB(Redis DataBase)的机制很简单:在特定时间点,将内存中所有数据生成一个快照文件保存到磁盘。这个文件通常以.rdb为后缀。

你可以把它理解为给整个数据库拍一张全家福,照片记录了那个瞬间的所有数据状态。

2. 触发机制

RDB有三种主要的触发方式:

自动触发(配置策略)

redis.conf配置文件中,我们可以设置自动触发快照的条件:

# 在900秒(15分钟)内,如果至少有1个key发生变化,则触发bgsavesave 900 1# 在300秒(5分钟)内,如果至少有10个key发生变化,则触发bgsave  save 300 10# 在60秒内,如果至少有10000个key发生变化,则触发bgsavesave 60 10000

手动触发

# 1. SAVE命令(同步)- 阻塞主进程,直到快照完成,期间不处理任何请求127.0.0.1:6379> SAVEOK# 2. BGSAVE命令(异步)- 后台执行快照,主进程继续处理请求127.0.0.1:6379> BGSAVEBackground saving started

其他情况

  • 执行SHUTDOWN命令关闭Redis时,如果没有开启AOF,会自动执行RDB快照
  • 主从复制时,主节点会向从节点发送RDB文件进行全量同步

3. RDB的工作流程(BGSAVE)

当我们执行BGSAVE时,Redis会:

  1. Fork子进程:主进程创建一个子进程(copy-on-write机制,内存占用不会翻倍)
  2. 子进程写盘:子进程将内存数据写入临时RDB文件
  3. 替换旧文件:写入完成后,用新的RDB文件替换旧的
  4. 清理工作:子进程退出,通知主进程完成

4. 优缺点分析

优点:

  • 性能影响小:BGSAVE通过子进程操作,主进程几乎不受影响
  • 文件紧凑:二进制格式,文件较小,适合备份和传输
  • 恢复速度快:恢复大数据集时比AOF快很多

缺点:

  • 可能丢失数据:两次快照之间的数据修改会丢失
  • Fork可能阻塞:数据集很大时,fork操作本身可能耗时较长

三、AOF持久化:记录每一个"成长瞬间"

1. 工作原理

AOF(Append Only File)采用了一种完全不同的思路:记录每一个写操作命令,以日志的形式追加到文件末尾。

这就像是写日记,记录下每一天发生的事情,而不是只拍几张照片。

2. 配置详解

要开启AOF,需要在配置文件中设置:

# 开启AOF持久化appendonly yes# AOF文件名appendfilename "appendonly.aof"# 同步策略appendfsync everysec

3. AOF的三种同步策略

策略机制数据安全性性能影响
always每个写命令都同步到磁盘最高,最多丢失一个命令最差,每次写都要磁盘IO
everysec每秒同步一次(默认)平衡,最多丢失一秒数据良好,性能与安全的折中
no由操作系统决定同步时机最低,可能丢失较多数据最好,完全异步

生产环境推荐使用everysec,在性能和数据安全之间取得最佳平衡。

4. AOF重写机制

随着运行时间增长,AOF文件会越来越大,而且包含很多已经过期的命令(比如对同一个key的多次set)。为了解决这个问题,Redis提供了AOF重写机制。

重写的本质:基于当前内存数据,生成一个新的、更精简的AOF文件,只包含恢复当前数据所需的最小命令集合。

# 手动触发AOF重写127.0.0.1:6379> BGREWRITEAOFBackground append only file rewriting started

自动重写配置

# 当AOF文件体积比上次重写后体积增长100%时,自动触发重写auto-aof-rewrite-percentage 100# AOF文件体积至少达到64MB时才会触发重写auto-aof-rewrite-min-size 64mb

5. 优缺点分析

优点:

  • 数据安全:配置合理时最多丢失1秒数据
  • 可读性强:AOF文件是文本格式,可以人工阅读和修改
  • 容错性好:即使文件尾部有损坏,也可以用redis-check-aof工具修复

缺点:

  • 文件较大:通常比RDB文件大
  • 恢复速度慢:需要重新执行所有命令,恢复大数据集时较慢
  • 性能影响:在高负载下,AOF可能比RDB稍慢

四、RDB vs AOF:如何选择?

特性RDBAOF
数据安全性可能丢失几分钟数据最多丢失1秒数据
恢复速度
文件大小(压缩的二进制)大(文本命令)
性能影响BGSAVE时影响小写入时有一定开销
灾难恢复适合更适合
可读性不可读可读

生产环境推荐策略

两者结合使用,发挥各自优势:

# 在redis.conf中同时开启RDB和AOFsave 900 1save 300 10save 60 10000appendonly yesappendfsync everysecauto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb

这种组合的优势:

  • AOF保证数据安全性,最多丢失1秒数据
  • RDB用于冷备份、快速重启和主从同步
  • 重启时优先使用AOF恢复(数据更完整),其次使用RDB

五、主从复制:数据备份与读写分离

单机Redis存在单点故障风险,主从复制是构建高可用架构的第一步。

1. 什么是主从复制?

  • 主节点(Master):负责写操作,将数据变化同步给从节点
  • 从节点(Slave/Replica):复制主节点数据,负责读操作

2. 主从复制的工作原理

  1. 建立连接:从节点连接到主节点,发送SYNC命令
  2. 全量同步:主节点执行BGSAVE生成RDB文件,发送给从节点
  3. 增量同步:主节点将期间的写命令缓存起来,RDB传输完成后发送给从节点
  4. 命令传播:之后主节点每收到写命令,就异步发送给从节点

3. 如何配置主从复制?

假设我们有:

  • 主节点:127.0.0.1:6379
  • 从节点:127.0.0.1:6380

方法一:配置文件
在从节点的redis.conf中添加:

replicaof 127.0.0.1 6379# 或者老版本使用:slaveof 127.0.0.1 6379

方法二:运行时命令

# 在从节点上执行127.0.0.1:6380> REPLICAOF 127.0.0.1 6379OK

4. 验证主从状态

# 在主节点查看复制信息127.0.0.1:6379> INFO replication# Replicationrole:masterconnected_slaves:1slave0:ip=127.0.0.1,port=6380,state=online,offset=1234,lag=0# 在从节点查看复制信息  127.0.0.1:6380> INFO replication# Replicationrole:slavemaster_host:127.0.0.1master_port:6379master_link_status:up

5. 主从架构的优势

  1. 数据冗余:从节点是主节点的完整备份
  2. 读写分离:主节点负责写,从节点负责读,提升读性能
  3. 故障恢复基础:为自动故障转移做准备

六、哨兵(Sentinel)模式:实现自动故障转移

主从复制解决了数据备份问题,但如果主节点宕机,需要手动切换,这期间服务会不可用。哨兵模式就是为了解决这个问题。

1. 哨兵是什么?

Redis Sentinel是一个分布式系统,用于管理多个Redis实例,主要功能包括:

  • 监控:持续检查主从节点是否正常运行
  • 通知:当被监控的Redis实例出现问题时,向管理员发送告警
  • 自动故障转移:主节点故障时,自动将一个从节点提升为新主节点,并让其他从节点复制新主节点
  • 配置提供者:客户端连接哨兵获取当前的主节点地址

2. 哨兵集群架构

通常我们会部署奇数个哨兵实例(如3个或5个),通过投票机制来决定是否进行故障转移,避免误判。

3. 搭建哨兵模式

假设我们有:

  • Redis主节点:127.0.0.1:6379
  • Redis从节点:127.0.0.1:6380、127.0.0.1:6381
  • 哨兵节点:127.0.0.1:26379、127.0.0.1:26380、127.0.0.1:26381

创建哨兵配置文件 sentinel-26379.conf

port 26379sentinel monitor mymaster 127.0.0.1 6379 2sentinel down-after-milliseconds mymaster 5000sentinel failover-timeout mymaster 60000sentinel parallel-syncs mymaster 1

参数解释:

  • sentinel monitor mymaster 127.0.0.1 6379 2:监控名为mymaster的主节点,至少需要2个哨兵同意才判定主观下线
  • down-after-milliseconds:5000毫秒无响应认为节点主观下线
  • failover-timeout:故障转移超时时间
  • parallel-syncs:故障转移后,同时向新主节点同步的从节点数量

启动哨兵:

redis-sentinel sentinel-26379.confredis-sentinel sentinel-26380.conf  redis-sentinel sentinel-26381.conf

4. 故障转移过程

  1. 主观下线:某个哨兵认为主节点不可用
  2. 客观下线:多个哨兵(达到quorum数量)都认为主节点不可用
  3. 选举领导者:哨兵之间选举一个领导者来执行故障转移
  4. 故障转移:领导者哨兵选择一个合适的从节点提升为新主节点
  5. 切换配置:通知其他从节点复制新主节点,更新客户端配置

5. 客户端如何连接哨兵?

客户端不再直接连接Redis节点,而是连接哨兵集群来获取当前的主节点地址。

Java客户端示例(使用Jedis):

Set<String> sentinels = new HashSet<>();sentinels.add("127.0.0.1:26379");sentinels.add("127.0.0.1:26380"); sentinels.add("127.0.0.1:26381");JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);try (Jedis jedis = pool.getResource()) {    // 现在操作的是当前的主节点    jedis.set("key", "value");}

七、持久化与高可用配置总结

方案数据安全可用性复杂度适用场景
单机+持久化开发测试、非核心业务
主从复制读多写少,需要备份
哨兵模式生产环境通用方案
Redis集群极高海量数据、高并发

总结

通过本篇的学习,我们掌握了构建可靠Redis系统的核心技术:

  1. 持久化是基础:RDB提供快照备份,AOF保证命令不丢失,两者结合使用最稳妥
  2. 主从复制提供冗余:数据多副本,读写分离提升性能
  3. 哨兵实现高可用:自动故障转移,服务不中断

记住这个演进路径:
单机Redis → 开启持久化 → 搭建主从复制 → 部署哨兵集群

现在,你的Redis已经不再是那个"内存失忆"的脆弱系统,而是一个具备数据持久化和自动故障恢复能力的高可用服务!


思考与实践:

  1. 在你的开发环境中尝试配置RDB和AOF,观察文件生成情况
  2. 搭建一个一主二从的Redis环境,并验证数据同步
  3. 部署三节点哨兵集群,模拟主节点宕机,观察自动故障转移过程

欢迎在评论区分享你在配置过程中遇到的问题和解决方案!

🔲 ☆

Redis入门:玩转Redis五大核心数据结构

玩转Redis五大核心数据结构:从计数器到排行榜

在上一篇中,我们知道了Redis为什么这么快,以及如何搭建环境。但Redis真正的威力,来自于它丰富的数据结构。如果说简单的键值对是Redis的"骨架",那么这些数据结构就是它的"肌肉",让Redis能够优雅地解决各种复杂的业务场景。

Redis最吸引人的地方在于,它不仅仅是一个简单的键值存储,而是一个数据结构服务器。今天,我们将深入探索Redis的五大核心数据结构:String(字符串)Hash(哈希)List(列表)Set(集合)Sorted Set(有序集合)

一、String(字符串):不止是文本

String是Redis最基本的数据类型,一个Key对应一个Value。但别被它的名字骗了——它不仅可以存储文本,还可以存储数字(整数或浮点数)甚至是二进制数据(如图片或序列化对象)。

核心命令与特性

# 1. 基础设置与获取127.0.0.1:6379> SET username "redis_learner"OK127.0.0.1:6379> GET username"redis_learner"# 2. 数字操作 - Redis知道你是数字时会允许数学运算127.0.0.1:6379> SET page_views 100OK127.0.0.1:6379> INCR page_views        # 增加1(integer) 101127.0.0.1:6379> INCRBY page_views 10   # 增加指定数值(integer) 111127.0.0.1:6379> DECR page_views        # 减少1(integer) 110# 3. 批量操作 - 提升效率127.0.0.1:6379> MSET user:1001:name "Alice" user:1001:age 25 user:1001:city "Beijing"OK127.0.0.1:6379> MGET user:1001:name user:1001:age user:1001:city1) "Alice"2) "25"3) "Beijing"# 4. 条件设置 - 实现分布式锁的基础127.0.0.1:6379> SETNX lock:order_123 "client_1"  # 只有当key不存在时才设置(integer) 1  # 设置成功127.0.0.1:6379> SETNX lock:order_123 "client_2"(integer) 0  # 设置失败,因为key已存在

实战应用场景

  1. 缓存:存储序列化的用户信息、页面片段等
  2. 计数器:文章阅读量、用户点赞数、网站访问量
  3. 分布式锁:通过SETNX实现简单的互斥锁
  4. 会话存储:存储用户Session数据

性能提示:对于多个相关的键值对,使用MSET/MGET比多次SET/GET更高效,因为它减少了网络往返次数。


二、Hash(哈希表):存储对象的最佳选择

如果你需要存储一个对象(如用户信息、商品信息),Hash是你的最佳选择。它类似于编程语言中的字典或Map,适合存储字段-值对的集合。

为什么用Hash而不用多个String?

假设我们要存储用户信息,有两种方案:

  • 方案一(多个String)

    SET user:1001:name "Bob"SET user:1001:age 30SET user:1001:email "bob@example.com"
  • 方案二(一个Hash)

    HSET user:1001 name "Bob" age 30 email "bob@example.com"

Hash的优势

  • 内存效率更高:Redis对Hash有特殊优化,特别是字段较少时
  • 原子性操作:可以一次性获取或修改整个对象
  • 更少的Key:避免键空间膨胀,管理更方便

核心命令详解

# 1. 设置和获取字段127.0.0.1:6379> HSET product:1001 name "iPhone 15" price 5999 stock 100(integer) 3  # 返回设置的字段数量127.0.0.1:6379> HGET product:1001 name"iPhone 15"127.0.0.1:6379> HGETALL product:1001  # 获取所有字段和值1) "name"2) "iPhone 15"3) "price"4) "5999"5) "stock"6) "100"# 2. 批量操作127.0.0.1:6379> HMGET product:1001 name price  # 获取多个字段1) "iPhone 15"2) "5999"# 3. 数字运算127.0.0.1:6379> HINCRBY product:1001 stock -1  # 库存减1(售出一件)(integer) 99# 4. 检查字段127.0.0.1:6379> HEXISTS product:1001 name(integer) 1127.0.0.1:6379> HKEYS product:1001  # 获取所有字段名1) "name"2) "price"3) "stock"

实战应用场景

  1. 用户信息存储:将用户的所有属性存储在一个Hash中
  2. 商品信息:商品的名称、价格、库存等信息
  3. 配置信息:系统的各种配置参数
  4. 购物车:用户ID作为Key,商品ID和数量作为字段-值对

三、List(列表):实现简单的消息队列

List是一个按插入顺序排序的字符串元素集合,你可以在列表的头部(左边)或尾部(右边)添加元素。Redis的List底层实现是双向链表,这意味着在头部和尾部添加元素的速度极快,但通过索引访问中间元素相对较慢。

核心命令详解

# 1. 从两端添加元素127.0.0.1:6379> LPUSH tasks "send_email"      # 从左边添加(integer) 1127.0.0.1:6379> LPUSH tasks "process_image"(integer) 2127.0.0.1:6379> RPUSH tasks "generate_report" # 从右边添加(integer) 3# 此时列表:["process_image", "send_email", "generate_report"]#               头(左)   <--------->   尾(右)# 2. 从两端弹出元素127.0.0.1:6379> LPOP tasks  # 从左边弹出"process_image"127.0.0.1:6379> RPOP tasks  # 从右边弹出"generate_report"# 3. 查看列表范围(不会弹出元素)127.0.0.1:6379> LRANGE tasks 0 -1  # 查看所有元素,0表示开始,-1表示末尾1) "send_email"# 4. 阻塞操作 - 消息队列的核心# 从一个空列表中阻塞地等待元素,最多等待10秒127.0.0.1:6379> BLPOP message_queue 10(nil)  # 10秒内没有元素,返回nil# 在另一个客户端执行:LPUSH message_queue "new_message"# 此时BLPOP会立即返回:"message_queue" "new_message"

实战应用场景

  1. 消息队列:使用LPUSH添加任务,BRPOP阻塞获取任务
  2. 最新消息列表:使用LPUSH添加新消息,LRANGE 0 9获取最新的10条
  3. 历史记录:用户浏览历史、搜索历史
  4. 文章评论列表:文章的评论按时间顺序排列

重要特性:List的阻塞操作(BLPOP, BRPOP)使其成为实现简单消息队列的理想选择,消费者可以在队列为空时等待,而不需要轮询。


四、Set(集合):无序与唯一性的力量

Set是String类型的无序集合,它最大的特点是:元素唯一且无序。底层通过哈希表实现,添加、删除、查找的时间复杂度都是O(1)。

核心命令与集合运算

# 1. 基本操作127.0.0.1:6379> SADD tags "java" "python" "redis" "java"(integer) 3  # "java"重复,只添加了3个元素127.0.0.1:6379> SMEMBERS tags  # 获取所有元素(顺序不确定)1) "redis"2) "python"3) "java"127.0.0.1:6379> SISMEMBER tags "python"  # 检查元素是否存在(integer) 1# 2. 集合运算 - Set的精华所在127.0.0.1:6379> SADD user:1001:follows "user:1002" "user:1003" "user:1004"(integer) 3127.0.0.1:6379> SADD user:1002:follows "user:1003" "user:1005"(integer) 2# 交集 - 共同关注127.0.0.1:6379> SINTER user:1001:follows user:1002:follows1) "user:1003"# 并集 - 所有的关注127.0.0.1:6379> SUNION user:1001:follows user:1002:follows1) "user:1002"2) "user:1003"3) "user:1004"4) "user:1005"# 差集 - A有但B没有的127.0.0.1:6379> SDIFF user:1001:follows user:1002:follows1) "user:1002"2) "user:1004"# 3. 随机元素 - 抽奖功能127.0.0.1:6379> SADD lottery_users "user1" "user2" "user3" "user4" "user5"(integer) 5127.0.0.1:6379> SRANDMEMBER lottery_users 2  # 随机返回2个元素,不删除1) "user3"2) "user5"127.0.0.1:6379> SPOP lottery_users 1         # 随机弹出1个元素并删除1) "user2"

实战应用场景

  1. 标签系统:给文章、用户打标签
  2. 社交关系:共同好友、共同关注
  3. 数据去重:防止重复提交、重复处理
  4. 随机抽奖:从参与用户中随机抽取中奖者
  5. 黑白名单:IP白名单、用户黑名单

五、Sorted Set(有序集合):排行榜的灵魂

Sorted Set是Set的增强版,它在保证元素唯一性的基础上,为每个元素关联了一个分数(Score),元素按照分数进行排序。这是Redis中最复杂但也最强大的数据结构之一。

底层实现:跳表(Skip List)

Sorted Set使用跳表(一种类似链表但有多级索引的数据结构)实现,可以在O(logN)时间内完成插入、删除和按分数范围查找,兼具了链表和二分查找的优点。

核心命令详解

# 1. 添加元素(带分数)127.0.0.1:6379> ZADD leaderboard 2500 "Alice" 1800 "Bob" 3200 "Charlie" 1500 "David"(integer) 4# 2. 按分数范围查询(升序)127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES  # 获取所有元素(分数从低到高)1) "David"2) "1500"3) "Bob"4) "1800"5) "Alice"6) "2500"7) "Charlie"8) "3200"# 3. 按分数范围查询(降序 - 排行榜常用)127.0.0.1:6379> ZREVRANGE leaderboard 0 2 WITHSCORES  # 获取前三名1) "Charlie"2) "3200"3) "Alice"4) "2500"5) "Bob"6) "1800"# 4. 按分数范围查询127.0.0.1:6379> ZRANGEBYSCORE leaderboard 2000 3000 WITHSCORES  # 分数在2000-3000之间的玩家1) "Alice"2) "2500"# 5. 获取排名和分数127.0.0.1:6379> ZRANK leaderboard "Alice"    # 获取升序排名(从0开始)(integer) 2127.0.0.1:6379> ZREVRANK leaderboard "Alice" # 获取降序排名(排行榜名次)(integer) 1127.0.0.1:6379> ZSCORE leaderboard "Alice"   # 获取分数"2500"# 6. 分数操作127.0.0.1:6379> ZINCRBY leaderboard 500 "Alice"  # Alice增加500分"3000"

实战应用场景

  1. 排行榜:游戏积分榜、销量排行榜、热搜榜
  2. 带权重的队列:优先级任务调度
  3. 时间轴:按时间排序的消息列表(时间戳作为Score)
  4. 范围查询:查找分数/价格在某个区间的数据

性能提示:Sorted Set的范围查询(ZRANGEBYSCORE)非常高效,特别适合需要按范围检索数据的场景。


数据结构选择指南

面对具体业务场景时,如何选择合适的数据结构?这里有一个快速参考:

需求场景推荐数据结构理由
缓存简单数据String简单直接,性能最佳
存储对象Hash内存效率高,支持部分更新
消息队列List支持阻塞操作,顺序保证
最新N条记录ListLPUSH + LTRIM 实现固定长度列表
去重、标签、共同好友Set天然去重,支持集合运算
排行榜、范围查询Sorted Set按分数排序,范围查询高效
时间序列数据Sorted Set时间戳作为Score,天然排序

总结

通过本篇的学习,我们已经掌握了Redis五大核心数据结构的特性和应用:

  • String:简单但强大,支持数字操作
  • Hash:存储对象的理想选择,内存效率高
  • List:顺序数据结构,适合消息队列和时间线
  • Set:无序唯一集合,强大的集合运算能力
  • Sorted Set:有序唯一集合,排行榜和范围查询的利器

Redis的哲学是:将复杂的数据操作下推到存储层,而不是在应用层处理。理解每种数据结构的特性和适用场景,能够让你在设计系统时做出更优雅、更高效的决策。

现在,当你需要实现计数器时,不会选择在应用层读取-计算-保存,而是直接使用INCR命令;当你需要排行榜时,不会在数据库中排序,而是使用Sorted Set。这就是Redis数据结构的威力所在!


动手练习:尝试用你学到的数据结构实现以下功能:

  1. 使用String实现一个文章阅读量计数器
  2. 使用Hash存储你的个人简历信息
  3. 使用List实现一个简单的待办事项列表
  4. 使用Set找出你和你朋友共同喜欢的电影
  5. 使用Sorted Set创建一个游戏分数排行榜

欢迎在评论区分享你的实现代码和心得体会!

🔲 ☆

Redis入门:Redis核心概念与快速入门

Redis核心概念与快速入门:为什么它堪称"程序员必修课"?

在当今这个数据爆炸、高并发无处不在的时代,你是否曾好奇,像淘宝双十一、微博热搜、微信朋友圈这样的亿级流量应用,是如何在瞬间处理海量请求的?背后的秘密武器之一,就是今天我们要深入探讨的——Redis

一、引言:为什么Redis是开发者必备的技能?

想象这样一个场景:“限量球鞋抢购”

  • 晚上8点整,10万用户同时点击"立即购买"。
  • 系统需要检查库存、生成订单、扣减库存…
  • 如果所有请求都直接涌向数据库,数据库很可能在瞬间被击垮,页面卡死,用户体验极差。

Redis的使命,就是成为那个站在数据库前面的"超级英雄"。它用内存存储热点数据,能在微秒级别内完成读写,轻松应对这种高并发冲击。无论是缓存、计数器还是分布式锁,Redis都提供了优雅的解决方案。

因此,无论你是后端开发者、架构师还是运维工程师,深入理解Redis都已成为职业生涯中不可或缺的一环。它不仅仅是缓存,更是一个高性能的数据结构服务器。

二、Redis是什么?它解决了什么核心痛点?

1. 官方定义

Redis(Remote Dictionary Server),即远程字典服务。顾名思义,你可以把它理解成一个通过网络提供访问的、超级快的"大字典"。

2. 核心特性

  • 基于内存:数据主要存储在内存(RAM)中,读写速度极快(读可达10万+/秒,写可达8万+/秒)。
  • 键值存储:使用简单的Key-Value模式存储数据。
  • 丰富的数据结构:Value不仅仅是字符串,还支持列表、哈希、集合等复杂类型。
  • 持久化:可以将内存中的数据异步保存到磁盘,防止数据丢失。
  • 单线程架构:核心操作采用单线程,避免了多线程的竞争和锁问题,简化设计且保证原子性。

3. 解决的核心痛点

  • 性能瓶颈:缓解后端数据库(如MySQL)的读/写压力。
  • 高并发:轻松应对瞬时大量并发请求。
  • 复杂操作:提供原子性的自增、集合运算等,简化业务代码。

三、Redis vs. MySQL:定位与差异

很多初学者会困惑:“既然有了MySQL,为什么还要用Redis?” 这是一个非常好的问题。它们不是替代关系,而是互补关系

特性RedisMySQL
数据存储主要存储在内存存储在硬盘
数据结构支持String, Hash, List, Set等主要是表结构,行列固定
性能极高,微秒级响应相对较慢,毫秒级响应
使用场景缓存、会话、排行榜、消息队列等持久化存储、复杂关系数据、事务
数据容量受物理内存限制受硬盘空间限制,远大于内存

一个形象的比喻:

  • MySQL 像是家里的保险柜,安全、可靠,用于存放最重要的财物(核心数据)。
  • Redis 像是你的书桌桌面,存取极其方便,用来放你当前正在使用的书籍和文具(热点数据)。

四、Redis的典型应用场景全景图

Redis的应用远不止缓存,它几乎无处不在:

  1. 缓存最核心的用途。缓存热点数据(如用户信息、商品详情),减轻数据库压力。
  2. 会话存储(Session):在分布式系统中,将用户登录状态集中存储在Redis中,实现多台应用服务器的会话共享。
  3. 排行榜:利用有序集合(Sorted Set)轻松实现实时更新的积分榜、热搜榜。
  4. 消息队列:利用列表(List)的阻塞操作实现简单的异步任务队列。
  5. 计数器/速率限制:利用字符串(String)的INCR命令实现文章阅读量、API调用频率限制。
  6. 分布式锁:在分布式系统中,控制多个服务实例对同一资源的访问。
  7. 社交功能:利用集合(Set)实现共同关注、好友推荐。

五、手把手搭建Redis环境(Docker篇)

为了快速开始,我们使用Docker来安装Redis,这是最便捷、跨平台的方式。

步骤1:安装Docker

请访问 Docker官网 下载并安装对应你操作系统的Docker Desktop。

步骤2:拉取并运行Redis镜像

打开你的终端(Terminal / Command Prompt / PowerShell),执行以下命令:

# 拉取最新的Redis官方镜像docker pull redis:latest# 运行Redis容器,并将容器的6379端口映射到本机的6379端口docker run --name my-redis -p 6379:6379 -d redis
  • --name my-redis:给你的容器起一个名字,方便管理。
  • -p 6379:6379:端口映射(主机端口:容器端口)。Redis默认服务端口是6379。
  • -d:在后台运行容器。

执行 docker ps 命令,如果看到名为 my-redis 的容器正在运行,说明安装成功!

六、redis-serverredis-cli:你的第一个Redis服务与客户端

现在,Redis服务已经在你的机器上运行起来了。我们如何与它交互呢?

  • redis-server:这是Redis的服务器。它负责监听端口,存储和管理数据。我们刚才通过Docker已经启动了它。
  • redis-cli:这是Redis的命令行客户端。我们通过它来向redis-server发送命令。

让我们进入容器内部,使用redis-cli

# 进入正在运行的my-redis容器docker exec -it my-redis redis-cli

看到提示符变成 127.0.0.1:6379> 了吗?恭喜,你已经成功连接到了Redis服务器!现在可以开始"玩"数据了。

七、使用 redis-cli 进行基本操作和测试

让我们像学习编程语言的"Hello, World!"一样,完成Redis的第一次对话。

# 1. 设置一个键值对:key是"greeting",value是"Hello, Redis!"127.0.0.1:6379> SET greeting "Hello, Redis!"OK# 2. 获取key为"greeting"的值127.0.0.1:6379> GET greeting"Hello, Redis!"# 3. 尝试获取一个不存在的key127.0.0.1:6379> GET non_existing_key(nil)# 4. 让我们试一下计数器功能(文章阅读量)127.0.0.1:6379> INCR article:readcount:1001(integer) 1127.0.0.1:6379> INCR article:readcount:1001(integer) 2127.0.0.1:6379> GET article:readcount:1001"2"

是不是非常简单直观?你已经掌握了Redis最基础的两个命令:SETGET,以及一个高级命令INCR

八、理解Redis的"灵魂":核心数据模型

Redis的整个世界都围绕着 Key-Value 模型。

  • Key(键):一个字符串,用于唯一标识一条数据。好的Key设计是使用Redis的最佳实践之一。例如:user:1001:profile, article:2024:hotlist
  • Value(值):可以是我们在第二章提到的多种数据结构,如字符串、哈希、列表等。

重要概念:Redis万物皆字节。 无论你存入的是什么类型的数据,Redis最终都是以二进制字节流的形式安全地存储它们。

九、通用命令:操作Redis的"瑞士军刀"

在深入学习各种数据结构之前,有一些命令是通用的,适用于所有的Key。

# 1. KEYS pattern:查找所有符合给定模式pattern的key(生产环境慎用,可能阻塞服务)127.0.0.1:6379> KEYS article*1) "article:readcount:1001"127.0.0.1:6379> KEYS *1) "greeting"2) "article:readcount:1001"# 2. EXISTS key:检查某个key是否存在127.0.0.1:6379> EXISTS greeting(integer) 1  # 存在返回1127.0.0.1:6379> EXISTS no_this_key(integer) 0  # 不存在返回0# 3. DEL key [key ...]:删除一个或多个key127.0.0.1:6379> DEL greeting(integer) 1  # 成功删除1个# 4. EXPIRE key seconds:为key设置过期时间(秒),超时后自动删除127.0.0.1:6379> SET temporary_data "I will disappear in 10 seconds"OK127.0.0.1:6379> EXPIRE temporary_data 10(integer) 1# 5. TTL key:查看key剩余的生存时间(Time To Live)127.0.0.1:6379> TTL temporary_data(integer) 6  # 还剩6秒127.0.0.1:6379> TTL temporary_data(integer) -2 # -2表示key已经不存在了# 也可以在SET的时候直接设置过期时间127.0.0.1:6379> SET session_id "abc123" EX 3600 # EX后跟秒数OK

注意KEYS *命令在Key数量巨大时可能会阻塞服务器,在生产环境中应使用SCAN命令代替。

总结

至此,你已经踏入了Redis的神奇世界。我们了解了:

  • 为什么需要Redis:解决高并发、高性能场景下的数据读写瓶颈。
  • Redis是什么:一个基于内存的、支持多种数据结构键值对存储系统。
  • 如何安装和运行Redis:通过Docker,我们快速搭建了实验环境。
  • 最基本的操作:使用redis-cli进行SET, GET, INCR等操作。
  • 通用命令KEYS, EXISTS, DEL, EXPIRE, TTL

这仅仅是Redis强大能力的冰山一角。在下一篇教程中,我们将深入探索Redis的五大核心数据结构(String, Hash, List, Set, Sorted Set),解锁Redis真正的威力。

思考题:根据你今天学到的知识,想想你当前参与的项目中,有哪些场景可以引入Redis来提升性能或简化逻辑?欢迎在评论区留言讨论!

希望这篇详细的入门教程能帮助你建立起对Redis的清晰认知。如果有任何疑问,欢迎随时交流。

🔲 ☆

SqlServer高频面试题(持续更新251114)

基础题

1. 主键、外键、超键、候选键的区别和用途

  • 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
  • 候选键:是最小超键,即没有冗余元素的超键。
  • 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
  • 外键:在一个表中存在的另一个表的主键称此表的外键。

2. 为什么使用自增列作为主键?

自增列作为主键可以简化数据的插入操作,避免因插入非顺序的主键值导致的索引分裂和碎片化,从而提高数据库性能。自增列也易于分配和管理,且不会与其他记录的主键冲突。

3. 触发器的作用是什么?

触发器是一种特殊的存储过程,它在特定数据库操作(如INSERT、UPDATE、DELETE)执行之前或之后自动触发执行。触发器可以用于维护数据完整性、实施复杂的业务规则、自动更新表中的数据等。

4. 什么是存储过程?使用什么来调用?

存储过程是一组为了执行特定任务而预编译的SQL语句。它们可以提高性能,因为只需编译一次,之后可以重复调用。存储过程可以通过SQL命令直接调用,也可以被应用程序通过特定的API调用来执行。

5. 存储过程的优缺点有哪些?

存储过程的优点包括提高性能(预编译)、减少网络传输、增强安全性(需要特定权限才能执行)、便于代码复用。缺点包括移植性差,因为它们通常与特定的数据库系统紧密相关。

6. 存储过程与函数的区别是什么?

存储过程是一系列为了完成特定功能的SQL语句集合,可以通过参数传递数据,并且可以有多个返回值。函数通常返回一个单一的数据值,并且在使用时作为表达式的一部分。存储过程使用更灵活,而函数则更适用于需要返回特定数据结构的场景。

7. 视图是什么?游标是什么?

视图是基于SQL查询的虚拟表,它像实际的表一样可以进行查询和更新操作,但是不存储数据,而是在查询视图时动态生成结果。游标是一种数据库对象,用于逐行处理查询结果集,常用于需要对结果集进行循环处理的场景。

8. 视图的优缺点有哪些?

视图的优点包括简化复杂的查询、提高数据安全性、实现数据逻辑抽象。缺点包括可能影响性能(尤其是在复杂的视图上执行查询时),以及在某些情况下限制了数据的更新操作。

9. 什么是临时表?临时表什么时候删除?

临时表是在当前会话或事务中创建的表,仅对当前会话可见。当会话结束或事务提交时,临时表及其数据会自动删除。

10. 非关系型数据库和关系型数据库的区别和优势比较是什么?

非关系型数据库(NoSQL)和关系型数据库在数据模型、查询方式、扩展性等方面有本质区别。非关系型数据库通常提供更高的扩展性和灵活性,适合处理大规模分布式数据。关系型数据库则在数据一致性、复杂查询和事务管理方面表现更好。

11. 数据库范式是什么?如何根据某个场景设计数据表?

数据库范式是一套用于指导数据库设计的规范,包括第一范式(1NF)、第二范式(2NF)、第三范式(3NF)等,目的是减少数据冗余和提高数据完整性。设计数据表时,应根据业务需求和数据关系来确定表结构,确保满足相应的范式要求。

12. 内连接、外连接、交叉连接、笛卡尔积等的区别是什么?

内连接只返回两个表中匹配的行;外连接(左外连接、右外连接)会返回一个表的全部行,另一个表中匹配的行,不匹配的行用NULL填充;交叉连接返回两个表的笛卡尔积,即每行与另一个表中每行的组合;笛卡尔积是两个集合所有可能的组合。

13. varchar和char的使用场景是什么?

VARCHAR适用于长度可变的数据,如用户输入的评论或描述,因为它可以根据实际内容长度存储,节省空间。CHAR适用于长度固定的数据,如性别或国家代码,因为它可以提供更快的存取速度,但会使用固定长度的存储空间。

14. SQL语言分类有哪些?

SQL语言主要分为数据查询语言(DQL),数据操纵语言(DML),数据定义语言(DDL)和数据控制语言(DCL)。DQL用于查询数据,如SELECT;DML用于数据的增删改,如INSERT、UPDATE、DELETE;DDL用于数据库对象的定义,如CREATE、ALTER、DROP;DCL用于控制数据库访问权限,如GRANT、REVOKE。

15. like '%xxx%'和’xxx%'的区别是什么?

LIKE '%xxx%'表示匹配包含xxx的任意字符串,无论xxx出现在哪一部分。LIKE 'xxx%'表示匹配以xxx结尾的字符串。两者在模糊匹配时使用不同的通配符,%代表任意字符出现任意次数,而_仅代表单个字符。

16. count(*)、count(1)、count(column)的区别是什么?

COUNT()用于计算表中的总行数,包括NULL值。COUNT(1)是COUNT()的等价操作,用于计算行数。COUNT(column)用于计算特定列中非NULL值的数量。

17. 最左前缀原则是什么?

最左前缀原则是索引创建和使用的一个重要原则,它指的是在多列索引中,数据库查询优化器只会使用索引的最左部分列。这意味着如果查询条件没有使用到索引的第一个列,那么即使后面的列被使用到,索引也可能不会被利用。

18. 索引的作用是什么?它的优点和缺点有哪些?

索引的作用是加快数据检索速度,排序和分组数据,以及保证数据的唯一性。优点包括提高查询速度、加速表连接、支持数据的排序和分组。缺点包括增加存储空间、降低数据更新(INSERT、UPDATE、DELETE)的速度,以及维护索引本身需要额外的开销。

19. 什么样的字段适合建索引?

适合建索引的字段包括经常需要搜索的列、作为主键的列、经常用于连接的列、经常需要进行范围搜索的列、经常需要排序的列,以及经常使用在WHERE子句中的列。

20. 聚集索引和非聚集索引的区别是什么?

聚集索引决定了表中数据的物理存储顺序,使得相关列的数据在物理上连续存放,查询效率较高,但修改数据时可能较慢。非聚集索引指定了表中数据的逻辑顺序,但物理存储顺序与索引可能不一致,通常用于频繁更新的数据列。

21. SQL注入式攻击是什么?

SQL注入式攻击是一种网络安全攻击手段,攻击者通过在Web表单输入域或页面请求的查询字符串中插入恶意SQL命令,欺骗服务器执行这些命令,从而获取、篡改或删除数据库中的数据。

22. 如何防范SQL注入式攻击?

防范SQL注入式攻击的方法包括:对用户输入进行过滤和验证,替换或转义特殊字符;使用预处理语句(参数化查询);限制数据库权限,使用最小权限原则;使用存储过程;以及在服务器端进行输入验证等。

23. 内存泄漏是什么?

内存泄漏是指在程序运行过程中,由于未能适当释放不再使用的内存,导致随着程序的持续运行,可用内存逐渐减少的现象。在动态内存分配的语言中,如C或C++,如果使用new分配了内存,却忘记使用delete释放,就可能发生内存泄漏。

24. 维护数据库的完整性和一致性,使用触发器还是自写业务逻辑?

维护数据库的完整性和一致性,通常首选使用数据库提供的约束,如CHECK、PRIMARY KEY、FOREIGN KEY等。其次是使用触发器,因为它们可以自动执行,确保数据的完整性和一致性,无论哪种业务逻辑访问数据库。最后考虑自写业务逻辑,但这种方法编程复杂,效率较低。

25. 什么是事务?什么是锁?

事务是一系列操作,它们作为一个整体被执行,以确保数据的完整性。如果事务中的任何操作失败,整个事务将回滚到执行前的状态。锁是数据库管理系统用来保证事务的隔离性和并发控制的一种机制,它可以防止多个事务同时修改同一数据,从而避免数据冲突。

26. 过多索引对数据库性能的影响

过多的索引虽然可以提高查询速度,但在数据的插入、更新和删除操作时,数据库引擎需要更多的时间来维护这些索引,这可能会导致性能下降。因此,需要在索引创建时进行权衡,以确保数据库操作的整体性能。

27. 相关子查询是什么?如何使用这些查询?

相关子查询是一种特殊类型的子查询,它在查询中使用外部查询的值。这种子查询通常用于WHERE或HAVING子句中,可以基于外部查询的结果来动态地定义查询条件。

28. 操作会使⽤到TempDB

TempDB是SQL Server的一个系统数据库,用于存储临时数据,如临时表和表变量。许多操作,包括创建表时的临时数据、执行某些类型的JOIN操作、使用游标以及存储过程和批处理中的一些操作,都可能会用到TempDB。

29. 如果TempDB异常变大,可能的原因是什么,该如何处理?

TempDB异常变大可能是由于大量使用临时表或返回的记录集过大造成的。处理方法包括优化查询以减少返回的数据量,使用分批处理,或者调整TempDB的大小和配置。

30. Index有哪些类型,它们的区别和实现原理是什么,索引有什么优点和缺点

索引类型主要包括聚集索引和非聚集索引。聚集索引决定了表中数据的物理存储顺序,非聚集索引则不改变数据的物理存储顺序。索引的优点包括提高查询速度、确保数据的唯一性和排序。缺点是增加了存储空间和维护成本,降低了数据更新的速度。

31. Job信息可以通过哪些表获取;系统正在运行的语句可以通过哪些视图获取;如何获取某个T-SQL语句的IO、Time等信息

Job信息可以通过SQL Server的msdb数据库中的表,如sysjobs和sysjobhistory获取。系统正在运行的语句可以通过动态管理视图如sys.dm_exec_requests获取。要获取某个T-SQL语句的IO和Time等信息,可以使用SQL Server Profiler或相关的动态管理视图。

确保字段只接受特定范围内的值
可以通过在字段上设置CHECK约束来确保只接受特定范围内的值。CHECK约束允许定义字段值的范围或条件,确保插入或更新数据时满足这些条件。

32. CHAR、VARCHAR、NCHAR 和 NVARCHAR 的区别是什么?

  • CHAR(n): 固定长度,非 Unicode 字符数据。无论实际内容多长,它都会占用 n 个字节的存储空间。适合存储长度相对固定的数据(如身份证号、电话号码)。
  • VARCHAR(n): 可变长度,非 Unicode 字符数据。它只占用实际数据长度 + 2 个字节(用于存储长度信息)的存储空间。适合存储长度变化较大的数据。
  • NCHAR(n): 固定长度,Unicode 字符数据。存储 Unicode 字符(如中文、日文等),每个字符占用 2 个字节。长度为 n,表示最多可存储 n 个字符(无论中英文)。
  • NVARCHAR(n): 可变长度,Unicode 字符数据。同样存储 Unicode 字符,每个字符 2 字节,但只占用(实际字符数 * 2) + 2 字节的空间。

核心区别: CHAR/VARCHAR 用于非 Unicode,一个英文字符占1字节,一个中文字符可能占2字节(取决于编码)。NCHAR/NVARCHAR 用于 Unicode,任何字符都占2字节,能全球通用。

33. TRUNCATE、DELETE 和 DROP 的区别?

特性DELETETRUNCATEDROP
类型DML(数据操作语言)DDL(数据定义语言)DDL(数据定义语言)
条件可以带 WHERE 子句不能带条件,清空所有数据删除整个表(结构和数据)
事务操作会被记录在事务日志中,可回滚操作记录最少,不可回滚操作不可回滚
触发器会触发 DELETE 触发器不会触发触发器-
标识列不影响标识列的当前值重置标识列的种子值-
性能较慢(逐行删除并记录日志)非常快(直接释放数据页)
行级锁表锁表锁

34. 什么是索引?聚集索引和非聚集索引的区别?

  • 索引:相当于书籍的目录,它能帮助数据库引擎快速找到数据,而无需扫描整个表。
  • 聚集索引
    • 决定了表中数据的物理存储顺序。一张表只能有一个聚集索引。
    • 叶子节点存储的是实际的数据行
    • 例如,在主键上默认创建的通常是聚集索引。
  • 非聚集索引
    • 不影响数据的物理存储顺序。一张表可以有多个非聚集索引。
    • 叶子节点存储的是索引键值 + 指向数据行的指针(聚集索引键或RID)
    • 查询时需要先查非聚集索引,再通过指针去查找实际数据,这个过程称为 “键查找”“书签查找”

35. 内连接(INNER JOIN)和外连接(OUTER JOIN)的区别?

  • 内连接:返回两个表中连接条件匹配的所有行。不匹配的行不会出现在结果中。
  • 外连接
    • 左外连接(LEFT JOIN):返回左表的所有行,以及右表中连接条件匹配的行。如果右表无匹配,则右表部分为 NULL。
    • 右外连接(RIGHT JOIN):返回右表的所有行,以及左表中连接条件匹配的行。如果左表无匹配,则左表部分为 NULL。
    • 全外连接(FULL JOIN):返回左表和右表中的所有行。当某一行在另一个表中没有匹配时,另一个表的部分为 NULL。

36. 什么是执行计划?如何查看和分析?

  • 执行计划:是 SQL Server 查询优化器生成的、关于如何执行一个查询的“路线图”。它显示了数据获取的步骤、使用的索引、连接类型、数据量估计和成本等。
  • 查看方法
    • 在 SSMS 中,在查询前按下 Ctrl + M(显示实际执行计划)或 Ctrl + L(显示估计执行计划),然后执行查询。
    • 使用 SET 语句:SET SHOWPLAN_TEXT ONSET STATISTICS PROFILE ON
  • 分析要点
    • 高成本操作:找到成本最高的步骤。
    • 表扫描(Table Scan):警惕!这通常意味着没有合适的索引。
    • 索引扫描(Index Scan) vs 索引查找(Index Seek): Seek 效率远高于 Scan。Scan 意味着遍历了整个索引。
    • 键查找(Key Lookup):如果开销很大,考虑创建覆盖索引。
    • 警告标志:如转换警告(隐式类型转换)等。

37. 什么是覆盖索引?

一个覆盖索引是指一个非聚集索引,它包含了查询中需要的所有字段。当查询的所有列都包含在索引的键或包含列中时,引擎可以直接从索引页中获取数据,而无需再去查找数据页,从而避免昂贵的键查找操作,极大提升性能。

创建覆盖索引示例:

CREATE INDEX IX_Covering ON Orders (CustomerID) INCLUDE (OrderDate, TotalAmount);-- 对于查询: SELECT OrderDate, TotalAmount FROM Orders WHERE CustomerID = @ID-- 这个索引就是覆盖索引。

38. 什么是索引碎片?如何维护?

  • 索引碎片:当索引页的逻辑顺序与物理顺序不匹配,或者页的数据填充度很低时,就产生了碎片。碎片会导致更多的物理 I/O,降低查询性能。
  • 类型
    • 外部碎片:页的逻辑顺序与物理顺序不符。
    • 内部碎片:页中存在大量空闲空间。
  • 维护方法
    • 重组(REORGANIZE):对叶级页以物理方式重新排序,并压缩索引页。是在线操作,干扰小。适用于轻度碎片。
    • 重建(REBUILD):删除旧索引并创建一个新的索引。可以最大限度地减少碎片,是离线操作(在 Enterprise 版中可以在线)。适用于重度碎片。

39. 什么时候不适合创建索引?

  • 表非常小(数据量很少)。
  • 列的值重复度很低(如性别列,只有‘男’,‘女’),索引效果不佳。
  • 列经常被频繁进行 INSERT/UPDATE/DELETE 操作,因为维护索引需要开销。
  • 不会在查询的 WHERE 或 JOIN 条件中使用的列。

40. 谈谈 ACID 属性。

  • 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  • 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。
  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。
  • 持久性(Durability):一旦事务提交,它对数据库中数据的改变就是永久性的。

41. SQL Server 的隔离级别有哪些?脏读、不可重复读、幻读分别是什么?

  • 读未提交(Read Uncommitted):可以读取其他事务未提交的数据。会导致脏读
  • 读已提交(Read Committed):只能读取其他事务已提交的数据。这是 SQL Server 的默认级别。避免了脏读,但可能导致不可重复读
  • 可重复读(Repeatable Read):保证在同一个事务中,多次读取同一数据的结果是一致的。避免了脏读和不可重复读,但可能导致幻读
  • 快照(Snapshot):在事务开始时提供数据的一个一致性版本。读取的是事务开始时的数据快照,不会阻塞写操作。避免了脏读、不可重复读和幻读。
  • 可序列化(Serializable):最高隔离级别,强制事务串行执行。避免了所有并发问题,但性能最差。

名词解释:

  • 脏读:事务A读取了事务B未提交的修改数据,之后B回滚了,A读到的就是脏数据。
  • 不可重复读:事务A多次读取同一数据,在此期间事务B修改并提交了该数据,导致A多次读取的结果不一致。
  • 幻读:事务A多次读取一个范围的数据,在此期间事务B插入或删除了该范围内的数据并提交,导致A多次读取时发现“凭空”多出或少了一些行。

42. 什么是死锁?如何避免和解决?

  • 死锁:两个或更多事务相互等待对方释放资源,导致它们都无法继续执行的状态。
  • 避免
    • 以相同的顺序访问表。
    • 保持事务简短,尽快提交。
    • 使用较低的隔离级别(如 Read Committed)。
    • 使用 LOCK_TIMEOUT 设置。
  • 解决:SQL Server 内置的死锁监视器会检测到死锁,并选择一个作为“牺牲品”将其回滚,从而让其他事务继续进行。牺牲品事务会收到 1205 错误。

43. 谈谈 CTE(公用表表达式)、临时表和表变量。

  • CTE
    • 更像一个临时的视图,只在查询期间存在。
    • 可读性好,特别适合递归查询。
    • 不能创建索引。
  • 临时表(#Temp)
    • 存储在 TempDB 中,存在于会话或嵌套作用域中。
    • 可以创建索引和统计信息。
    • 适合存储较大的中间结果集。
  • 表变量(@Table)
    • 也存储在 TempDB 中,存在于批处理/函数/存储过程的作用域中。
    • 通常认为它更快(对于小数据量),因为它没有统计信息,导致优化器总是假设它只有1行。
    • 不能创建索引(除了主键和唯一约束)。

44. 行版本控制和乐观并发控制是什么?

这是基于快照隔离级别的机制。当数据被修改时,SQL Server 会在 TempDB 中保存被修改行的旧版本。其他正在读取的事务可以从 TempDB 中读取这个旧版本,从而不会与写事务发生阻塞。READ_COMMITTED_SNAPSHOTALLOW_SNAPSHOT_ISOLATION 数据库选项与此相关。

45. SQL Server 的高可用性方案有哪些?

  • AlwaysOn 故障转移集群实例(FCI):基于 Windows 故障转移集群,共享存储。实例级别的高可用。
  • AlwaysOn 可用性组(AG):SQL Server 的核心高可用和灾难恢复解决方案。数据库级别,不共享存储,可读副本,功能最强大。
  • 数据库镜像(已弃用,被AG取代):主库和镜像库之间同步数据。
  • 日志传送:通过定期备份主数据库的事务日志并还原到辅助服务器来实现。恢复时间较长。

46. 你知道哪些 SQL Server 的新特性?(根据面试公司使用的版本准备)

  • JSON 支持FOR JSON PATH/AUTO, OPENJSON 等。
  • STRING_AGG 函数:将多行字符串值合并成一个字符串。
  • 查询存储(Query Store):用于跟踪查询执行计划、性能历史,并强制特定计划。
  • 时态表(Temporal Tables):自动跟踪和管理数据的历史变化。
  • 内存优化表(In-Memory OLTP):将表和存储过程放入内存,极大提升性能。

47. 存储过程和函数的区别是什么?

特性存储过程函数
返回值可以没有返回值,或通过 OUTPUT 参数返回多个值必须有返回值(标量或表)
使用场景执行业务逻辑、数据处理计算并返回一个值,或在查询中作为表使用
在 SELECT 中调用不可以可以
DML 操作可以对表进行所有 DML 操作在函数内部不能执行 DML 操作(除了表变量)
事务管理可以在内部使用事务(BEGIN TRANSACTION)不能在函数内使用事务
执行方式EXEC/EXECUTE 过程名SELECT dbo.函数名()

48. 什么时候应该使用存储过程?什么时候应该使用函数?**

  • 使用存储过程

    • 执行复杂的业务逻辑
    • 需要返回多个结果集
    • 需要进行 DML 操作(INSERT/UPDATE/DELETE)
    • 需要事务控制
    • 性能要求高(预编译、执行计划重用)
  • 使用函数

    • 封装可重用的计算逻辑
    • 在查询中作为列使用
    • 简化复杂的 JOIN 或 WHERE 条件
    • 返回表值供 FROM 子句使用

49. 什么是触发器?INSTEAD OF 和 AFTER 触发器的区别?**

  • 触发器:一种特殊的存储过程,在特定数据库事件(INSERT/UPDATE/DELETE)发生时自动执行。

  • AFTER 触发器(FOR 触发器)

    • DML 操作执行完成后 触发
    • 可以访问 inserteddeleted 魔术表
    • 常用于审计、日志记录、数据一致性检查
  • INSTEAD OF 触发器

    • 取代 原始的 DML 操作执行
    • 在约束检查之前触发
    • 常用于实现复杂的视图更新逻辑,或对不可更新视图进行更新

50. inserted 和 deleted 魔术表是什么?**

这两个是触发器中的特殊内存表:

  • inserted:包含 INSERT 或 UPDATE 操作的数据
  • deleted:包含 DELETE 或 UPDATE 操作的数据
-- 在 UPDATE 触发器中CREATE TRIGGER trg_AuditUpdate ON Employees AFTER UPDATEASBEGIN    INSERT INTO AuditTable (EmployeeID, OldSalary, NewSalary)    SELECT d.EmployeeID, d.Salary, i.Salary    FROM deleted d     INNER JOIN inserted i ON d.EmployeeID = i.EmployeeID    WHERE d.Salary <> i.Salary;END;

51. ROW_NUMBER()、RANK()、DENSE_RANK() 的区别?**

这三个都是窗口函数,用于为结果集的行分配排名:

  • ROW_NUMBER():为每一行分配一个唯一的连续序号(1, 2, 3, 4…)
  • RANK():相同的值获得相同排名,但会跳过后续排名(1, 2, 2, 4…)
  • DENSE_RANK():相同的值获得相同排名,但不跳过后续排名(1, 2, 2, 3…)
SELECT     Name, Score,    ROW_NUMBER() OVER (ORDER BY Score DESC) as RowNum,    RANK() OVER (ORDER BY Score DESC) as Rank,    DENSE_RANK() OVER (ORDER BY Score DESC) as DenseRankFROM Students;

52. 什么是公用表表达式(CTE)的递归查询?

递归 CTE 用于处理层次结构数据(如组织结构、菜单树等):

-- 查询某个部门及其所有子部门WITH DepartmentCTE AS (    -- 锚定成员:根节点    SELECT DepartmentID, DepartmentName, ParentDepartmentID    FROM Departments    WHERE DepartmentID = @RootDepartmentID        UNION ALL        -- 递归成员:子节点    SELECT d.DepartmentID, d.DepartmentName, d.ParentDepartmentID    FROM Departments d    INNER JOIN DepartmentCTE cte ON d.ParentDepartmentID = cte.DepartmentID)SELECT * FROM DepartmentCTE;

53. 什么是参数嗅探问题?如何解决?

  • 参数嗅探:SQL Server 在编译存储过程时,使用第一次执行时的参数值来生成执行计划。如果后续执行的参数值数据分布差异很大,可能导致性能问题。

  • 解决方案

    • 使用 OPTION (RECOMPILE):每次执行都重新编译
    • 使用 OPTION (OPTIMIZE FOR UNKNOWN):使用平均数据分布
    • 使用局部变量:将参数赋值给局部变量,在查询中使用局部变量
    • 使用 WITH RECOMPILE 选项创建存储过程

54. 如何查找和优化慢查询?

  • 查找慢查询

    • 使用 SQL Server Profiler
    • 使用扩展事件(Extended Events)
    • 查询动态管理视图(DMV):
    -- 查找最耗时的查询SELECT TOP 10     total_elapsed_time/execution_count AS avg_elapsed_time,    execution_count,    SUBSTRING(st.text, (qs.statement_start_offset/2)+1,         ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)            ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) + 1) AS statement_textFROM sys.dm_exec_query_stats qsCROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) stORDER BY avg_elapsed_time DESC;
  • 优化方法

    • 添加合适的索引
    • 重写查询逻辑
    • 避免在 WHERE 子句中对字段进行函数操作
    • 减少不必要的列查询

55. 数据库的三大范式是什么?

  • 第一范式(1NF):每个列都是原子的,不可再分
  • 第二范式(2NF):满足 1NF,且非主属性完全依赖于主键(消除部分依赖)
  • 第三范式(3NF):满足 2NF,且非主属性之间没有传递依赖

56. 什么时候应该反范式化?

虽然范式化减少了数据冗余,但在以下情况可以考虑反范式化:

  • 频繁的 JOIN 操作影响性能时
  • 需要提高查询性能的读密集型场景
  • 数据仓库或报表数据库
  • 历史数据表,数据不再变更

57. 完整备份、差异备份和事务日志备份的区别?

  • 完整备份:备份整个数据库,是其他备份的基础
  • 差异备份:只备份自上次完整备份以来发生变化的数据页
  • 事务日志备份:备份事务日志,允许时间点恢复

恢复场景示例

完整备份 (周日) → 差异备份 (周一) → 日志备份 (周二 10:00) → 日志备份 (周二 11:00)

如果周二 11:30 发生故障,可以恢复到:周日完整备份 + 周一差异备份 + 周二 10:00 日志 + 周二 11:00 日志

58. 简单恢复模式 vs 完整恢复模式

  • 简单恢复模式

    • 不备份事务日志
    • 不能进行时间点恢复
    • 日志空间自动回收
    • 适合测试环境或可接受数据丢失的场景
  • 完整恢复模式

    • 需要定期备份事务日志
    • 支持时间点恢复
    • 可以防止数据丢失
    • 生产环境推荐使用

59. 如何设计一个支持软删除的系统?

-- 在表中添加删除标记字段ALTER TABLE Products ADD IsDeleted BIT NOT NULL DEFAULT 0;ALTER TABLE Products ADD DeletedDate DATETIME NULL;-- 使用视图过滤已删除的记录CREATE VIEW vw_ActiveProducts ASSELECT * FROM Products WHERE IsDeleted = 0;-- 使用 INSTEAD OF DELETE 触发器实现软删除CREATE TRIGGER trg_SoftDeleteProductON Products INSTEAD OF DELETEASBEGIN    UPDATE Products     SET IsDeleted = 1, DeletedDate = GETDATE()    WHERE ProductID IN (SELECT ProductID FROM deleted);END;

60. 如何处理数据库中的循环引用?

  • 方案1:使用延迟约束检查

    ALTER TABLE TableA ADD CONSTRAINT FK_TableA_TableB FOREIGN KEY (BID) REFERENCES TableB(BID)-- 在某些版本中可以使用 DEFERRABLE
  • 方案2:允许 NULL 值,先插入部分数据再更新

  • 方案3:使用触发器代替外键约束

  • 方案4:重新设计表结构,消除循环引用

61. 如何实现数据库的审计功能?

  • 方法1:使用触发器记录数据变更
  • 方法2:使用 SQL Server 的变更数据捕获(CDC)功能
  • 方法3:使用 SQL Server Audit 功能(企业版)
  • 方法4:在应用层实现审计逻辑

62. SQL Server 2019 的新特性有哪些?

  • 智能查询处理:自适应连接、行模式内存授予反馈等
  • 数据虚拟化:通过 PolyBase 查询外部数据源
  • Java 语言扩展:在 SQL Server 中执行 Java 代码
  • 加速数据库恢复:大幅减少数据库恢复时间
  • 列存储索引增强:可更新的非聚集列存储索引

63. 什么是内存优化表?适用场景?

内存优化表将数据完全存储在内存中,提供极高的吞吐量:

  • 适用场景

    • 高频读写的高并发场景
    • 会话状态管理
    • 实时数据处理
    • 需要亚毫秒级响应的应用
  • 创建示例

CREATE TABLE dbo.SessionState(    SessionID nvarchar(64) NOT NULL PRIMARY KEY NONCLUSTERED,    UserData varbinary(MAX) NOT NULL,    CreatedDate datetime2 NOT NULL) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

数据库表题

1. 电商产品有颜色(红、蓝、黑)、尺寸(S、M、L)等变体信息,买家购买一个红色中码的商品对应的变体名为:红_M。那么给定一组变体信息,用程序生成所有变体组合数据。

变体示例:Color: Red,Green Size: S,M Style: A
变体组合结果:Red_S_A; Red_M_A; Green_S_A; Green_M_A

//测试用例 var list = new List<string[]>{ new string[]{"Red","Green"}, new string[]{"S","M"}, new string[]{"A"}};var result = Combine(list);//期望result为:Red_S_A; Red_M_A; Green_S_A; Green_M_Apublic List<string> Combine(List<string[]> list){ … }
  • 答案
public static List<string> Combine(List<string[]> list){    List<string> result = new List<string>();    int[] indices = new int[list.Count]; // 用于跟踪每个字符串数组中当前选取的元素的索引    while (true)    {        string combined = "";        for (int i = 0; i < list.Count; i++)        {            combined += "_" + list[i][indices[i]]; // 将当前索引对应的元素添加到组合中        }        result.Add(combined); // 将组合添加到结果列表中        // 更新索引        int j = list.Count - 1;        while (j >= 0 && indices[j] == list[j].Length - 1)        {            indices[j] = 0;            j--;        }        // 检查是否所有索引都已经达到最大值        if (j < 0)        {            break;        }        indices[j]++; // 增加索引    }    return result;}

2. 内部系统的产品库中有一个产品拥有三个维度的变体,分别是:Color、Size 和 Style。现在要将其上传到平台 A,但平台 A 仅支持两个维度的变体,因此需要对变体进行降维。那么给定一组变体信息,用程序实现变体降维操作。

变体示例: Color: Red,Green Size: S,M Style: A,B

降维后:Color: Red,Green Size: S_A,S_B,M_A,M_B

var pair = new Dictionary<string, List<string>> { {"Color",new List<string>{ "Red","Green" }}, {"Size",new List<string>{ "S","M" }}, {"Style",new List<string>{ "A","B" }},};var result = Reduce(pair);public Dictionary<string, List<string>> Reduce(Dictionary<string, List<string>> pair){...}
  • 答案
class Program { static void Main() { // 定义变体维度 string[] colors = { "Red", "Green" }; string[] sizes = { "S", "M" }; string[] styles = { "A", "B" };    // 降维操作    Dictionary<string, string[]> reducedDimensions = ReduceDimensions(colors, sizes, styles);        // 打印降维后的变体    Console.WriteLine("Color: " + string.Join(",", reducedDimensions["Color"]));    Console.WriteLine("Size: " + string.Join(",", reducedDimensions["Size"]));}static Dictionary<string, string[]> ReduceDimensions(string[] colors, string[] sizes, string[] styles){    var reduced = new Dictionary<string, string[]>    {        { "Color", colors },        { "Size", sizes.SelectMany(size => styles.Select(style => size + "_" + style)).ToArray() }    };    return reduced;}}

3. 试用SQL查询语句表达下列对教学数据库中三个基本表 S、SC 、C 的查询:

  • S(sno, sname, sage, ssex):学号、姓名、年龄、性别
  • SC(sno, cno, grade):学号、课程号、成绩
  • C(cno, cname, teacher):课程号、课程名、教师名

3.1. 求年龄大于所有女同学年龄的男学生姓名和年龄

SELECT sname, sage FROM S AS XWHERE x.ssex = '男' AND x.sage > ALL (  SELECT sage FROM S AS Y WHERE y.ssex = '女');

3.2. 求年龄大于女同学平均年龄的男学生姓名和年龄

SELECT sname, sage FROM SWHERE ssex = '男' AND sage > (  SELECT AVG(sage) FROM S WHERE ssex = '女');

3.3. 在SC中检索成绩为空值的学生学号和课程号

SELECT sno, cno FROM SC WHERE grade IS NULL;

3.4. 检索姓名以WANG打头的所有学生的姓名和年龄

SELECT sname, sage FROM S WHERE sname LIKE 'WANG%';

3.5. 检索学号比WANG同学大,而年龄比他小的学生姓名

SELECT sname FROM sWHERE sno > (SELECT sno FROM s WHERE sname = 'WANG')  AND sage < (SELECT sage FROM s WHERE sname = 'WANG');

3.6. 统计每门课程的学生选修人数(超过2人的课程才统计)

SELECT cno, COUNT(sno) AS 人数 FROM SCGROUP BY cno HAVING COUNT(sno) > 2ORDER BY 人数 DESC, cno ASC;

3.7. 求LIU老师所授课程的每门课程的学生平均成绩

SELECT cname, AVG(grade) FROM SC, CWHERE SC.cno = C.cno AND teacher = 'liu'GROUP BY c.cno, cname;

3.8. 求选修C4课程的学生的平均年龄

SELECT AVG(sage) FROM S, SCWHERE S.sno = SC.sno AND cno = '4';

3.9. 统计有学生选修的课程门数

SELECT COUNT(DISTINCT cno) FROM SC;

3.10. 在基本表SC中修改4号课程的成绩

UPDATE SC SET grade = grade * 1.05 WHERE cno = '4' AND grade <= 75;UPDATE SC SET grade = grade * 1.04 WHERE cno = '4' AND grade > 75;

3.11. 把低于总平均成绩的女同学成绩提高5%

UPDATE SC SET grade = grade * 1.05WHERE grade < (SELECT AVG(grade) FROM SC)  AND sno IN (SELECT sno FROM S WHERE ssex = '女');

3.12. 把选修数据库原理课不及格的成绩改为空值

UPDATE SC SET grade = NULLWHERE grade < 60 AND cno IN (  SELECT cno FROM C WHERE cname = '数据库原理');

3.13. 把WANG同学的学习选课和成绩全部删去

DELETE FROM SC WHERE sno IN (  SELECT sno FROM S WHERE sname = 'WANG');

3.14. 在基本表SC中删除尚无成绩的选课元组

DELETE FROM SC WHERE grade IS NULL;

3.15. 在基本表S中插入一个学生元组

INSERT INTO S(sno, sname, sage) VALUES('S9', 'WU', 18);
🔲 ☆

Lucene.Net 分布式索引实现方案

Lucene.Net 本身是一个单机版全文搜索引擎库,不直接支持分布式索引,但通过合理的架构设计,可以实现分布式索引与搜索。以下是常见的分布式索引实现方案及其优缺点:

主从复制

原理

  • 主节点(Master):负责写入索引,定期将索引快照同步到从节点。
  • 从节点(Slave):只读副本,处理查询请求,提升查询吞吐量和可用性。

实现步骤

  1. 主节点写入:所有增删改操作由主节点处理。
  2. 索引同步
    • 方案1:主节点定期将索引文件复制到从节点(如通过 Rsync)。
    • 方案2:通过消息队列(如 RabbitMQ)广播增量变更,从节点实时更新。
  3. 查询负载均衡:查询请求通过负载均衡器分发到多个从节点。

优点

  • 提升读取性能和可用性(从节点可故障转移)。
  • 实现相对简单,无需修改 Lucene.Net 核心逻辑。

缺点

  • 写入能力受限于单主节点,可能成为瓶颈。
  • 同步延迟导致短暂数据不一致。

适用场景

  • 读多写少的场景(如新闻网站、知识库)。

方案2 实现

1. RabbitMQ Fanout 路由模式介绍
1. 核心原理
  • 交换器类型fanout 类型的交换器。
  • 行为规则
    生产者发送到 fanout 交换器的消息,会被复制并分发到所有绑定到该交换器的队列,无论队列绑定时是否指定了路由键。
  • 关键特点
    • 广播机制:消息无差别发送到所有队列,类似“发布-订阅”模式。
    • 路由键无效:消息的路由键(如 user.created)会被忽略,仅交换器类型决定分发行为。

2. 适用场景
  • 广播通知

    • 用户注册成功后,同时发送邮件、短信、站内信(多个消费者独立处理)。
    • 系统配置更新时,通知所有微服务刷新本地缓存。
  • 日志收集
    将日志消息广播到多个处理队列,分别用于实时监控、持久化存储、错误告警等。

  • 事件驱动架构
    解耦事件发布者与订阅者,新增订阅者只需绑定队列,无需修改生产者代码。


3. 对比其他路由模式
模式行为典型场景
Fanout广播到所有绑定队列日志分发、多通知渠道
Direct按精确匹配路由键发送到指定队列订单状态更新、任务分类处理
Topic按通配符匹配路由键(如 user.*复杂事件路由、分类订阅
Headers根据消息头键值对匹配高级路由逻辑(较少使用)

4. 总结

Fanout 模式是 RabbitMQ 中最简单的广播机制,适合需要将同一消息分发给多个消费者的场景。其优势在于快速实现解耦和扩展,但需注意无差别广播可能带来的资源浪费。对于需要精细化路由控制的场景,应选择 directtopic 模式。

2. 技术栈组成
组件作用
Lucene.Net单机版全文搜索核心
RabbitMQ消息广播中间件
MassTransit.NET消息总线框架
Docker容器化部署基础
3. 示意图

image

4. 安装MassTransit.RabbitMQ
5. docker启动RabbitMq服务
# 启动带管理界面的RabbitMQdocker run -d --name rabbitmq \  -p 5672:5672 -p 15672:15672 \  rabbitmq:3-management
6. 示例
services.AddMassTransit(x =>        {            x.AddConsumer<SyncIndexEventConsumer>();            x.UsingRabbitMq((context, cfg) =>            {                cfg.Host("127.0.0.1", $"/",h =>                {                    h.Username("guest");                    h.Password("guest");                });                cfg.Publish<SyncIndexEventConsumer>(x =>                {                    x.ExchangeType = "fanout";                });                // 配置接收端点并绑定到 Fanout 交换机                cfg.ReceiveEndpoint(Guid.NewGuid().ToString(), e =>                {                    e.Bind<SyncIndexEvent>(p =>                    {                        p.ExchangeType = "fanout";                        p.RoutingKey = "";                                         });                    e.ConfigureConsumer<SyncIndexEventConsumer>(context);                    e.PrefetchCount = 50; // 控制消费速率                });            });        });

SyncIndexEventConsumer.cs

public class SyncIndexEventConsumer: IConsumer<SyncIndexEvent>{    public async Task Consume(ConsumeContext<SyncIndexEvent> context)    {        var contextInput = context.Message;        Console.WriteLine("触发索引同步");        await Task.CompletedTask;    }}

SyncIndexEvent.cs

public class SyncIndexEvent{        /// <summary>    /// 表名    /// </summary>    public string TableName { get; set; }        /// <summary>    /// 实际实体对象    /// </summary>    public object Data { get; set; }}

RabbitMQController

[Route("api/[controller]/[action]")][ApiController]public class RabbitMqController : BaseApiController{    private readonly IPublishEndpoint _publishEndpoint;    public RabbitMqController(IPublishEndpoint publishEndpoint)    {        _publishEndpoint = publishEndpoint;    }    /// <summary>    /// 触发全局同步索引    /// </summary>    [HttpGet]    public async Task SyncIndex()    {        await _publishEndpoint.Publish<SyncIndexEvent>(new SyncIndexEvent(), x =>        {            x.Durable = true; // 持久化存储            x.Mandatory = true; // 强制路由            x.SetPriority(1); // 消息优先级        });    }}

方案对比分析

1. 同步机制对比
特性文件复制方案RabbitMQ Fanout方案
实时性分钟级延迟毫秒级延迟
扩展性需手动调整同步脚本动态增删从节点
可靠性依赖文件系统完整性消息持久化+确认机制
资源消耗高(全量复制)低(增量传播)
2. 性能压测数据
# 测试环境:4核8G服务器集群[写入吞吐量]单主节点:1,200 docs/sec从节点扩展:10节点时 8,000 docs/sec[同步延迟]99%请求 < 50ms最大延迟 120ms

☑️ ☆

Lucene.Net 入门和简单使用

Lucene.Net 是什么

Lucene.Net 是一个高性能、全功能的全文检索搜索引擎框架库,它是 Apache Lucene 项目的.NET语言版本。完全使用C#开发,适用于微软.NET环境。Lucene.Net允许开发者在他们的应用程序中实现强大的搜索功能,包括文本分析、索引创建、搜索查询处理、排序和过滤等功能。

以下是一些Lucene.Net的主要特性和功能:

  1. 文本分析:支持对文本内容进行分词、去除停用词、词干提取等预处理操作,以提高搜索精度。

  2. 索引构建:能够高效地创建包含文档内容、元数据和其它相关信息的索引。

  3. 检索能力:提供丰富的查询语法,支持布尔查询、短语查询、模糊查询、通配符查询等多种查询类型。

  4. 排序和过滤:可以根据多个字段进行排序,并支持各种过滤条件来限制搜索结果。

  5. 高性能:设计为高度优化和快速,能够在大型数据集上执行快速的搜索操作。

  6. 扩展性:可以通过插件和模块化设计来扩展和定制功能,例如添加自定义的分词器或查询解析器。

  7. 多语言支持:包括对多种语言的分词和分析支持。

Lucene.Net已经被广泛应用于各种需要搜索功能的场景,包括企业内部搜索、网站搜索、文档管理系统、数据分析和大数据处理等。尽管上述信息可能基于较早的日期,但Lucene.Net的基本原理和主要功能依然保持其重要性和相关性。随着时间的推移,该项目可能会有新的版本发布和功能更新。

Lucene.Net 能用来做什么

Lucene.Net 主要用于构建和实现高性能的全文搜索功能在.NET应用程序中。以下是一些具体的用途和应用场景:

  1. 搜索引擎开发Lucene.Net 可以作为构建自定义搜索引擎的核心组件,为网站、企业内部系统、文档管理系统等提供快速、准确的搜索服务。

  2. 数据索引和检索:它可以用来对大量文本数据进行索引,使得后续的查询操作能够快速找到相关的信息。这在处理大规模数据集时尤其有用。

  3. 文档管理和检索:在文档管理系统中,Lucene.Net 可用于索引和搜索各种类型的文档,如 PDF、Word、Excel、HTML 等。

  4. 日志分析和监控:对于大量的日志数据,Lucene.Net 可以帮助快速搜索和分析特定的日志事件或模式。

  5. 电子商务搜索:在线购物平台可以使用 Lucene.Net 实现商品搜索,支持按名称、描述、价格、评价等多种条件进行过滤和排序。

  6. 新闻和内容聚合:新闻网站和内容聚合平台可以利用 Lucene.Net 实现实时的新闻搜索和相关内容推荐。

  7. 数据分析和大数据处理:在大数据环境中,Lucene.Net 可以与其他工具和技术(如 Hadoop)结合,用于高效地搜索和分析大规模数据集。

  8. 本地化搜索:由于支持多语言分词和分析,Lucene.Net 也可用于实现对多种语言内容的搜索。

  9. 知识管理系统:在企业知识管理系统中,Lucene.Net 可以帮助员工快速查找和访问相关的知识文档和资料。

总的来说,任何需要在大量文本数据中进行快速、准确搜索的应用场景,都可能是 Lucene.Net 的用武之地。通过灵活的配置和扩展,开发者可以根据具体需求定制搜索功能,以满足不同项目和业务的需求。

安装

dotnet add package Lucene.Net --prerelease

常见数据类型

以下是在 Lucene.Net 中常见的一些字段类型:

  1. TextField:用于存储全文搜索的文本数据。这种类型的字段会被分析器(Analyzer)处理,进行分词、去除停用词等操作。

  2. StringField:用于存储不需要进行全文搜索的文本数据,如标题、作者名等。这些字段通常会被当作一个整体进行索引,不进行分词。

  3. NumericField:用于存储数值类型的数据,如整数、长整数、浮点数和双精度数。Lucene.Net 提供了不同的子类来处理不同类型的数值。

  4. DateField:用于存储日期和时间数据。日期通常被转换为 long 型数值表示,以便于索引和排序。

  5. BinaryField:用于存储二进制数据,如图片、音频、视频等非文本内容。实际的二进制数据会被编码为字符串并存储在索引中。

  6. StoredField:用于存储需要在搜索结果中返回的任何类型的数据。这些字段不会被索引,但会在检索时从原始文档中获取并返回。

Lucene.Net 中,字段的数据类型主要通过使用不同的 Field 类的实例来表示和处理。这些字段类型的选择和使用取决于你的应用程序的具体需求,例如你希望如何搜索和排序这些数据。需要注意的是,Lucene.Net 主要专注于文本搜索,对于复杂的数据类型和结构化数据,可能需要额外的设计和处理。

常见查询方法

以下是一些 Lucene.Net 中常见的查询类型及其示例:

  1. TermQuery

    Term term = new Term("fieldName", "searchTerm");Query query = new TermQuery(term);

    这将搜索在 “fieldName” 字段中包含 “searchTerm” 的文档。

  2. BooleanQuery

    TermQuery query1 = new TermQuery(new Term("fieldName1", "term1"));TermQuery query2 = new TermQuery(new Term("fieldName2", "term2"));BooleanClause clause1 = new BooleanClause(query1, Occur.MUST);BooleanClause clause2 = new BooleanClause(query2, Occur.MUST);BooleanQuery booleanQuery = new BooleanQuery.Builder()    .add(clause1)    .add(clause2)    .build();

    这将搜索在 “fieldName1” 字段中包含 “term1” 且在 “fieldName2” 字段中包含 “term2” 的文档。

  3. PhraseQuery

    PhraseQuery phraseQuery = new PhraseQuery();phraseQuery.add(new Term("fieldName", "term1"));phraseQuery.add(new Term("fieldName", "term2"));// 或者使用带位置的构造方式PhraseQuery.Builder builder = new PhraseQuery.Builder();builder.add(new Term("fieldName", "term1"), 0);builder.add(new Term("fieldName", "term2"), 1);PhraseQuery phraseQuery = builder.build();

    这将搜索在 “fieldName” 字段中包含短语 “term1 term2”(按顺序)的文档。

  4. WildcardQuery

    WildcardQuery wildcardQuery = new WildcardQuery(new Term("fieldName", "term*"));

    这将搜索在 “fieldName” 字段中以 “term” 开头的所有文档。

  5. PrefixQuery

    PrefixQuery prefixQuery = new PrefixQuery(new Term("fieldName", "prefix"));

    这将搜索在 “fieldName” 字段中以 “prefix” 开头的所有文档。

  6. FuzzyQuery

    FuzzyQuery fuzzyQuery = new FuzzyQuery(new Term("fieldName", "fuzzyTerm"), 2);

    这将搜索在 “fieldName” 字段中与 “fuzzyTerm” 相似(根据 Levenshtein 距离计算,参数 2 表示最大编辑距离为 2)的文档。

  7. RangeQuery

    TermRangeQuery rangeQuery = TermRangeQuery.newStringRange(    "fieldName",     new BytesRef("lowerBound"),     new BytesRef("upperBound"),     true, // 是否包括下边界    true); // 是否包括上边界

    这将搜索在 “fieldName” 字段中值在 “lowerBound” 和 “upperBound” 范围内的文档(包括边界)。

以上示例展示了如何创建不同类型的查询对象。在实际使用时,通常会将这些查询对象传递给 IndexSearcher 对象的 Search() 方法来执行搜索操作。

高级用法

Lucene.Net 提供了许多高级用法,以下是一些常见的例子:

  1. 使用 Analyzer 自定义文本分析

    // 创建自定义 Analyzerpublic class CustomAnalyzer : Analyzer{    protected override TokenStreamComponents CreateComponents(String fieldName, TextReader reader)    {        Tokenizer tokenizer = new StandardTokenizer(LuceneVersion.LUCENE_48, reader);        TokenFilter filter = new LowerCaseFilter(LuceneVersion.LUCENE_48, tokenizer);        return new TokenStreamComponents(tokenizer, filter);    }}// 在创建 IndexWriter 时使用自定义 AnalyzerAnalyzer analyzer = new CustomAnalyzer();Directory directory = new RAMDirectory();IndexWriterConfig config = new IndexWriterConfig(LuceneVersion.LUCENE_48, analyzer);IndexWriter writer = new IndexWriter(directory, config);
  2. 使用 MultiFieldQueryParser 进行多字段搜索

    String[] fields = {"title", "content"};Analyzer analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);Query query = MultiFieldQueryParser.Parse(    LuceneVersion.LUCENE_48,     searchText,     fields,     analyzer);
  3. 使用 SpanQueries 进行精确的短语和 proximity 搜索

    SpanTermQuery term1 = new SpanTermQuery(new Term("fieldName", "term1"));SpanTermQuery term2 = new SpanTermQuery(new Term("fieldName", "term2"));SpanNearQuery spanQuery = new SpanNearQuery(    new SpanQuery[] { term1, term2 },         // 子查询    0,                                       // 距离(0表示相邻)    true);                                   // 是否包含边界Query query = spanQuery;
  4. 使用 Boost 设置字段和文档的权重

    Document doc = new Document();Field titleField = new TextField("title", "Document Title", Field.Store.YES);titleField.Boost = 2.0f;                      // 设置字段权重doc.Add(titleField);Field contentField = new TextField("content", "Document Content", Field.Store.YES);doc.Add(contentField);IndexWriter writer = new IndexWriter(directory, config);writer.AddDocument(doc);
  5. 使用 Collector 自定义搜索结果收集

    TopScoreDocCollector collector = TopScoreDocCollector.Create(10, false); // 收集前10个最高分文档searcher.Search(query, collector);ScoreDoc[] hits = collector.TopDocs().ScoreDocs;foreach (ScoreDoc hit in hits){    Document doc = searcher.Doc(hit.Doc);    // 处理搜索结果}
  6. 使用 Filter 进行过滤搜索

    Query query = new TermQuery(new Term("fieldName", "term"));Filter filter = new PrefixFilter(new Term("anotherField", "prefix"));IndexSearcher filteredSearcher = new IndexSearcher(reader);filteredSearcher.Similarity = new DefaultSimilarity(); // 可选:设置相似度算法filteredSearcher.Search(query, filter, collector);
  7. 使用 Faceting 进行分类统计

    FacetsConfig config = new FacetsConfig();config.SetHierarchical("category", true); // 设置 category 字段为层级结构Directory directory = FSDirectory.Open(indexPath);IndexReader reader = DirectoryReader.Open(directory);TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoDir);IndexSearcher searcher = new IndexSearcher(reader);FacetsCollector fc = new FacetsCollector();searcher.Search(query, fc);Facets facets = new FastTaxonomyFacetCounts(taxoReader, config, fc);List<FacetResult> results = facets.GetAllDims(10); // 获取前10个最频繁的分类foreach (FacetResult result in results){    Console.WriteLine(result);}

相关示例代码

using Lucene.Net.Analysis;using Lucene.Net.Index;using Lucene.Net.Store;using Lucene.Net.Util;using Microsoft.AspNetCore.Mvc;using SqlSugar;using WebApplication4.Entities;using LuceneDirectory = Lucene.Net.Store.Directory;using Lucene.Net.Analysis.Standard;using Lucene.Net.Documents;using Lucene.Net.QueryParsers.Classic;using Lucene.Net.Search;using System.Text;using Lucene.Net.Analysis.Cn.Smart;namespace WebApplication4.Controllers;[ApiController][Route("[controller]")]public class WeatherForecastController : ControllerBase{    [HttpGet("initDb")]    public IActionResult InitDb()    {        const LuceneVersion luceneVersion = LuceneVersion.LUCENE_48;//Open the Directory using a Lucene Directory class        string indexName = "example_index";        string indexPath = Path.Combine(Environment.CurrentDirectory, indexName);        using LuceneDirectory indexDir = FSDirectory.Open(indexPath);// Create an analyzer to process the text         Analyzer standardAnalyzer = new StandardAnalyzer(luceneVersion);        //Create an index writer        IndexWriterConfig indexConfig = new IndexWriterConfig(luceneVersion, standardAnalyzer);        indexConfig.OpenMode = OpenMode.CREATE_OR_APPEND;                             // create/overwrite index        IndexWriter writer = new IndexWriter(indexDir, indexConfig);            // 创建文档        var doc1 = new Document();        doc1.Add(new StringField("content", "Mobile Game Virginia Cloud Hosting Server", Field.Store.YES));        writer.AddDocument(doc1);                var doc2 = new Document();        doc2.Add(new StringField("content", "PC Game Virginia Cloud Hosting Server", Field.Store.YES));        writer.AddDocument(doc2);                var doc3 = new Document();        doc3.Add(new StringField("content", "Console Game Virginia Cloud Hosting Server", Field.Store.YES));        writer.AddDocument(doc3);                var doc4 = new Document();        doc4.Add(new StringField("content", "Game Testing Near Me", Field.Store.YES));        writer.AddDocument(doc4);        writer.Commit();        writer.Dispose();        return Ok();    }    [NonAction]    public List<string> GetKeyWords(string q)    {        List<string> keyworkds = new List<string>();        Analyzer analyzer = new SmartChineseAnalyzer(LuceneVersion.LUCENE_48);        using (var ts = analyzer.GetTokenStream(null, q))        {            ts.Reset();            var ct = ts.GetAttribute<Lucene.Net.Analysis.TokenAttributes.ICharTermAttribute>();            while (ts.IncrementToken())            {                StringBuilder keyword = new StringBuilder();                for (int i = 0; i < ct.Length; i++)                {                    keyword.Append(ct.Buffer[i]);                }                string item = keyword.ToString();                if (!keyworkds.Contains(item))                {                    keyworkds.Add(item);                }            }        }        return keyworkds;    }    public static IndexWriter  _writer { get; set; }    const LuceneVersion luceneVersion = LuceneVersion.LUCENE_48;    Analyzer standardAnalyzer = new StandardAnalyzer(luceneVersion);    [NonAction]    public IndexWriter GetWrite()    {        if (_writer != null)        {            return _writer;        }        else        {            string indexName = "example_index";            string indexPath = Path.Combine(Environment.CurrentDirectory, indexName);            LuceneDirectory indexDir = FSDirectory.Open(indexPath);            IndexWriterConfig indexConfig = new IndexWriterConfig(luceneVersion, standardAnalyzer);            indexConfig.OpenMode = OpenMode.CREATE_OR_APPEND;                             // create/overwrite index            IndexWriter writer = new IndexWriter(indexDir, indexConfig);            _writer = writer;            return writer;        }            }        [HttpGet("keyword")]    public IActionResult KeyWord(string key)    {                using DirectoryReader reader = GetWrite().GetReader(applyAllDeletes: true);        IndexSearcher searcher = new IndexSearcher(reader);        QueryParser parser = new QueryParser(luceneVersion, "content", standardAnalyzer);        var wildcardQuery = new WildcardQuery(new Term("content", $"*{key}*"));                var hits = searcher.Search(wildcardQuery, 100);        var list = new List<string>();        // 遍历搜索结果        foreach (var hit in hits.ScoreDocs)        {            var doc = searcher.Doc(hit.Doc);            list.Add(doc.Get("content"));        }        return Ok(list);    }}
☑️ ☆

JavaScript授权Gps,音频,视频踩坑

授权

GPS 授权

function getLocation(){    if (navigator.geolocation)    {        navigator.geolocation.getCurrentPosition(showPosition, showError);    }    else    {        console.log("该浏览器不支持获取地理位置。");    }}function showPosition(position){    console.log("纬度: " + position.coords.latitude +    " 经度: " + position.coords.longitude);    }function showError(error) {    console.log(error);}

音频授权

if (navigator.mediaDevices.getUserMedia) {  const constraints = { audio: true };  navigator.mediaDevices.getUserMedia(constraints).then(    stream => {      console.log("授权成功!");    },    () => {      console.error("授权失败!");    }  );} else {  console.error("浏览器不支持 getUserMedia");}

摄像头视频授权

const constraints = {  video: true // 请求视频流};const videoElement = document.getElementById('video');navigator.mediaDevices.getUserMedia(constraints)  .then(stream => {    videoElement.srcObject = stream;  })  .catch(error => {    console.error('获取摄像头权限失败:', error);  });

遇到问题

浏览器安全机制

按照上面的步骤去做,理论上是可以实现我们的功能。但事实并非如此,不信你可以起个服务验证一下看看。

通过验证,你会发现在Chrome 浏览器中使用http://localhost:8080 或者 http://127.0.0.1:8080 可以正常获取到浏览器的地理位置,但通过IP或者域名的形式,如:http://172.21.3.82:8080http://b.cunzhang.com进行访问时却获取不到

为什么呢?打开控制台,你会发现有以下错误信息:
Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).

“只有在安全来源的情况才才被允许”。错误信息里还包含了一个提示链接,我们不妨打开这个链接(https://goo.gl/Y0ZkNV)看看。原来,为了保障用户的安全,Chrome浏览器认为只有安全的来源才能开启定位服务。那什么样才算是安全的来源呢?在打开链接的页面上有这么一段话:

“Secure origins” are origins that match at least one of the following (scheme, host, port) patterns:

  • (https, *, *)

  • (wss, *, *)

  • (*, localhost, *)

  • (*, 127/8, *)

  • (*, ::1/128, *)

  • (file, *, —)

  • (chrome-extension, *, —)

This list may be incomplete, and may need to be changed. Please discuss!

大概意思是说只有包含上述列表中的scheme、host或者port才会被认为是安全的来源,现在这个列表还不够完整,后续可能还会有变动,有待讨论。

这就可以解释了为什么在http://localhost:8080 和 http://127.0.0.1:8080访问下可以获取到浏览器的地理位置,而在http://172.21.3.82:8080 和 http://b.cunzhang.com 确获取不到了。

方法一

如果需要在域名访问的基础上实现地位位置的定位,那我们只能把http协议升级为https了。

方法二

在浏览器地址栏中输入chrome://flags/#unsafely-treat-insecure-origin-as-secure,回车,如下图,将该选项置为
,在输入框中输入需要访问的地址,多个地址使用“,”隔开,然后点击右下角弹出的
按钮,自动重启浏览器之后就可以在添加的http地址下调用摄像头和麦克风,地址了。

image

☑️ ☆

.Net7 针对Utc时区转换问题中间件

为什么要存储UTC时间

存储UTC(协调世界时)时间是为了在跨时区和时间转换的情况下保持时间的一致性和准确性。以下是一些原因:

  1. 时区独立性:UTC时间是不受时区影响的,它是全球标准时间。通过存储UTC时间,可以避免在不同时区之间进行转换和处理,确保时间的一致性,无论用户位于何处。

  2. 跨时区应用:对于涉及跨时区操作的应用程序,使用UTC时间可以简化时间计算和比较。如果存储本地时间,可能会面临时区转换和夏令时变更等问题,导致时间计算不准确。

  3. 数据一致性:当多个系统或数据库需要共享时间信息时,使用UTC时间可以确保数据的一致性。不同系统和数据库可以根据需要将UTC时间转换为本地时间进行显示,以满足用户的需求,而不会引起数据不一致的问题。

  4. 日志和审计:在日志记录和审计方面,使用UTC时间可以提供统一的时间戳,使得跨系统和跨地点的日志可以进行准确的时间排序和比较。

  5. 时间计算的准确性:在进行时间计算、持续时间计算或时间间隔比较时,使用UTC时间可以避免由于夏令时等因素引起的时间偏移和不一致性,确保计算的准确性。

总之,存储UTC时间可以提供时间的一致性、跨时区操作的便利性以及数据的一致性,尤其适用于涉及多个时区的应用程序和系统。当需要进行时间转换或比较时,可以将UTC时间转换为本地时间进行显示,以满足最终用户的需求。

代码中实现

var dateTime = Datetime.UtcNow;

如果这样实现的话那么接口请求的时间和请求返回的时间都需要手动处理,这样不不够优雅的

如何处理

安装Newtonsoft.Json
增加DateTimeFilter.cs

public class DateTimeFilter : IActionFilter{    public void OnActionExecuting(ActionExecutingContext context)    {        var parameters = context.ActionArguments;        foreach (var parameter in parameters)        {                       if (parameter.Value is DateTime dateTime)            {                dateTime = dateTime.ToUniversalTime();                context.ActionArguments[parameter.Key] = dateTime;            }        }    }    public void OnActionExecuted(ActionExecutedContext context)    {    }}

增加DateTimeJsonConverter.cs

public class DateTimeJsonConverter : DateTimeConverterBase{        public DateTimeJsonConverter()    {    }          public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)    {                        // 用户配置的时区 ,建议使用Redis 或者内存缓存        var setting = "";        if (setting.IsNullOrEmpty())        {            DateTime dateTime = (DateTime)value;            writer.WriteValue(dateTime.ToLocalTime());        }        else        {            TimeZoneInfo targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(setting);            DateTime targetTime = TimeZoneInfo.ConvertTimeFromUtc((DateTime) value, targetTimeZone);            writer.WriteValue(targetTime);        }    }    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)    {        DateTime dateTime = (DateTime)reader.Value;        return dateTime.ToUniversalTime();    }}

在Program.cs 中注册

builder.Services.AddControllers(o =>{        o.Filters.Add(typeof(DateTimeFilter));}).AddNewtonsoftJson(options =>{    options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;    options.SerializerSettings.ContractResolver = new DefaultContractResolver();    options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";    options.SerializerSettings.Converters.Add(new StringEnumConverter());    options.SerializerSettings.Converters.Add(new DateTimeJsonConverter());})

DateTimeFilter 是针对以下接口入参形式

        [HttpGet("utc")]        public IActionResult Utc(DateTime utc)        {            return Ok(utc);        }

DateTimeJsonConverter 是捕获不到接口的参数,只能捕获入参对象,和返回对象

    public class MyClass        {            public DateTime Utc { get; set; }                        public MyClass1 MyClassx { get; set; }            public class MyClass1            {                public DateTime Utc1 { get; set; }            }        }        [HttpGet("utc")]        public IActionResult Utc(MyClass utc)        {            return Ok(utc);        }                //获取所有时区         [HttpGet("TimeZone")]        public IActionResult List()        {            var list = TimeZoneInfo.GetSystemTimeZones();            return Ok(list.Select(x =>            {                return new                {                    Id = x.Id,                    DisplayName = x.DisplayName                };            }).ToList());        }

这样就实现对传入参数和返回参数 DateTime 类型转UTC时间再转回当前时区的实现
ControllerServicesRepository就不用再处理Datetime 类型时区转换的代码了, 直接可以使用

☑️ ☆

Abp.Zero框架升级

Abp.Zero框架升级

本次采用的是项目迁移,注意问题

AppPermissions 文件内容

AppSettingProvider 文件内容

UiCustomizationSettingsAppService 文件内容

Startup.cs文件

注意内容及顺序

 public IServiceProvider ConfigureServices(IServiceCollection services)        {                    services.AddControllersWithViews(options =>            {                options.Filters.Add(new AbpAutoValidateAntiforgeryTokenAttribute());            })#if DEBUG                .AddRazorRuntimeCompilation()#endif                .AddNewtonsoftJson();                       //配置Cookie策略,不然部分浏览器非SSL无法登录系统            services.Configure<CookiePolicyOptions>(options =>            {                options.MinimumSameSitePolicy = SameSiteMode.Lax;            });        }                 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)        {                    app.UseCookiePolicy(); app.UseRouting();            app.UseAuthentication();            app.UseAuthorization();            app.UseEndpoints(endpoints =>            {                endpoints.MapHub<AbpCommonHub>("/signalr");                endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}");                endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");            });        }

迁移脚本命令

迁移脚本需注意文件处理

WebContentDirectoryFinder.cs

EF Core迁移命令

dotnet ef database update --startup-project=./x.Web.Mvc --project=./x.EntityFrameworkCore --context=xDbContext

EF CoreSQL

dotnet ef migrations script Upgrated_To_ABP_4_8_0  Upgraded_To_Abp_6_4_0  --startup-project=./x.Web.Mvc --project=./x.EntityFrameworkCore --context=xDbContext
BEGIN TRANSACTION;GOALTER TABLE [AbpEditions] ADD [DailyPrice] decimal(18,2) NULL;GOALTER TABLE [AbpEditions] ADD [WeeklyPrice] decimal(18,2) NULL;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20190801133107_Updated_SubscribableEdition', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOCREATE TABLE [AppSubscriptionPaymentsExtensionData] (    [Id] bigint NOT NULL IDENTITY,    [SubscriptionPaymentId] bigint NOT NULL,    [Key] nvarchar(450) NULL,    [Value] nvarchar(max) NULL,    [IsDeleted] bit NOT NULL,    CONSTRAINT [PK_AppSubscriptionPaymentsExtensionData] PRIMARY KEY ([Id]));GOCREATE UNIQUE INDEX [IX_AppSubscriptionPaymentsExtensionData_SubscriptionPaymentId_Key_IsDeleted] ON [AppSubscriptionPaymentsExtensionData] ([SubscriptionPaymentId], [Key], [IsDeleted]) WHERE [Key] IS NOT NULL;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20191015062846_Add_Subscription_Payment_Extension_Data', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOALTER TABLE [AppSubscriptionPayments] ADD [EditionPaymentType] int NOT NULL DEFAULT 0;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20191120123128_Add-EditionPaymentType-To-SubscriptionPayment', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GODROP INDEX [IX_AbpUserLoginAttempts_TenancyName_UserNameOrEmailAddress_Result] ON [AbpUserLoginAttempts];DECLARE @var0 sysname;SELECT @var0 = [d].[name]FROM [sys].[default_constraints] [d]INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AbpUserLoginAttempts]') AND [c].[name] = N'UserNameOrEmailAddress');IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [AbpUserLoginAttempts] DROP CONSTRAINT [' + @var0 + '];');ALTER TABLE [AbpUserLoginAttempts] ALTER COLUMN [UserNameOrEmailAddress] nvarchar(256) NULL;CREATE INDEX [IX_AbpUserLoginAttempts_TenancyName_UserNameOrEmailAddress_Result] ON [AbpUserLoginAttempts] ([TenancyName], [UserNameOrEmailAddress], [Result]);GODECLARE @var1 sysname;SELECT @var1 = [d].[name]FROM [sys].[default_constraints] [d]INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AbpSettings]') AND [c].[name] = N'Value');IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [AbpSettings] DROP CONSTRAINT [' + @var1 + '];');ALTER TABLE [AbpSettings] ALTER COLUMN [Value] nvarchar(max) NULL;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20191213093244_Upgraded_To_ABP_5_1', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOCREATE TABLE [AbpWebhookEvents] (    [Id] uniqueidentifier NOT NULL,    [WebhookName] nvarchar(max) NOT NULL,    [Data] nvarchar(max) NULL,    [CreationTime] datetime2 NOT NULL,    [TenantId] int NULL,    [IsDeleted] bit NOT NULL,    [DeletionTime] datetime2 NULL,    CONSTRAINT [PK_AbpWebhookEvents] PRIMARY KEY ([Id]));GOCREATE TABLE [AbpWebhookSubscriptions] (    [Id] uniqueidentifier NOT NULL,    [CreationTime] datetime2 NOT NULL,    [CreatorUserId] bigint NULL,    [TenantId] int NULL,    [WebhookUri] nvarchar(max) NOT NULL,    [Secret] nvarchar(max) NOT NULL,    [IsActive] bit NOT NULL,    [Webhooks] nvarchar(max) NULL,    [Headers] nvarchar(max) NULL,    CONSTRAINT [PK_AbpWebhookSubscriptions] PRIMARY KEY ([Id]));GOCREATE TABLE [AbpWebhookSendAttempts] (    [Id] uniqueidentifier NOT NULL,    [WebhookEventId] uniqueidentifier NOT NULL,    [WebhookSubscriptionId] uniqueidentifier NOT NULL,    [Response] nvarchar(max) NULL,    [ResponseStatusCode] int NULL,    [CreationTime] datetime2 NOT NULL,    [LastModificationTime] datetime2 NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpWebhookSendAttempts] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpWebhookSendAttempts_AbpWebhookEvents_WebhookEventId] FOREIGN KEY ([WebhookEventId]) REFERENCES [AbpWebhookEvents] ([Id]) ON DELETE CASCADE);GOCREATE INDEX [IX_AbpWebhookSendAttempts_WebhookEventId] ON [AbpWebhookSendAttempts] ([WebhookEventId]);GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200117141413_Upgraded_To_ABP_5_2_0', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200305082815_Upgraded_To_Abp_5_3', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOCREATE TABLE [AppUserDelegations] (    [Id] bigint NOT NULL IDENTITY,    [CreationTime] datetime2 NOT NULL,    [CreatorUserId] bigint NULL,    [LastModificationTime] datetime2 NULL,    [LastModifierUserId] bigint NULL,    [IsDeleted] bit NOT NULL,    [DeleterUserId] bigint NULL,    [DeletionTime] datetime2 NULL,    [SourceUserId] bigint NOT NULL,    [TargetUserId] bigint NOT NULL,    [TenantId] int NULL,    [StartTime] datetime2 NOT NULL,    [EndTime] datetime2 NOT NULL,    CONSTRAINT [PK_AppUserDelegations] PRIMARY KEY ([Id]));GOCREATE INDEX [IX_AppUserDelegations_TenantId_SourceUserId] ON [AppUserDelegations] ([TenantId], [SourceUserId]);GOCREATE INDEX [IX_AppUserDelegations_TenantId_TargetUserId] ON [AppUserDelegations] ([TenantId], [TargetUserId]);GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200315101156_Added_UserDelegations_Entity', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOCREATE TABLE [AbpDynamicParameters] (    [Id] int NOT NULL IDENTITY,    [ParameterName] nvarchar(450) NULL,    [InputType] nvarchar(max) NULL,    [Permission] nvarchar(max) NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpDynamicParameters] PRIMARY KEY ([Id]));GOCREATE TABLE [AbpDynamicParameterValues] (    [Id] int NOT NULL IDENTITY,    [Value] nvarchar(max) NOT NULL,    [TenantId] int NULL,    [DynamicParameterId] int NOT NULL,    CONSTRAINT [PK_AbpDynamicParameterValues] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpDynamicParameterValues_AbpDynamicParameters_DynamicParameterId] FOREIGN KEY ([DynamicParameterId]) REFERENCES [AbpDynamicParameters] ([Id]) ON DELETE CASCADE);GOCREATE TABLE [AbpEntityDynamicParameters] (    [Id] int NOT NULL IDENTITY,    [EntityFullName] nvarchar(450) NULL,    [DynamicParameterId] int NOT NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpEntityDynamicParameters] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpEntityDynamicParameters_AbpDynamicParameters_DynamicParameterId] FOREIGN KEY ([DynamicParameterId]) REFERENCES [AbpDynamicParameters] ([Id]) ON DELETE CASCADE);GOCREATE TABLE [AbpEntityDynamicParameterValues] (    [Id] int NOT NULL IDENTITY,    [Value] nvarchar(max) NOT NULL,    [EntityId] nvarchar(max) NULL,    [EntityDynamicParameterId] int NOT NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpEntityDynamicParameterValues] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpEntityDynamicParameterValues_AbpEntityDynamicParameters_EntityDynamicParameterId] FOREIGN KEY ([EntityDynamicParameterId]) REFERENCES [AbpEntityDynamicParameters] ([Id]) ON DELETE CASCADE);GOCREATE UNIQUE INDEX [IX_AbpDynamicParameters_ParameterName_TenantId] ON [AbpDynamicParameters] ([ParameterName], [TenantId]) WHERE [ParameterName] IS NOT NULL AND [TenantId] IS NOT NULL;GOCREATE INDEX [IX_AbpDynamicParameterValues_DynamicParameterId] ON [AbpDynamicParameterValues] ([DynamicParameterId]);GOCREATE INDEX [IX_AbpEntityDynamicParameters_DynamicParameterId] ON [AbpEntityDynamicParameters] ([DynamicParameterId]);GOCREATE UNIQUE INDEX [IX_AbpEntityDynamicParameters_EntityFullName_DynamicParameterId_TenantId] ON [AbpEntityDynamicParameters] ([EntityFullName], [DynamicParameterId], [TenantId]) WHERE [EntityFullName] IS NOT NULL AND [TenantId] IS NOT NULL;GOCREATE INDEX [IX_AbpEntityDynamicParameterValues_EntityDynamicParameterId] ON [AbpEntityDynamicParameterValues] ([EntityDynamicParameterId]);GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200317114116_Add_Dynamic_Entity_Parameters', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200406060103_Remove_OrganizationUnit_Unique_Index', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GODROP TABLE [AbpDynamicParameterValues];GODROP TABLE [AbpEntityDynamicParameterValues];GODROP TABLE [AbpEntityDynamicParameters];GODROP TABLE [AbpDynamicParameters];GOCREATE TABLE [AbpDynamicProperties] (    [Id] int NOT NULL IDENTITY,    [PropertyName] nvarchar(450) NULL,    [InputType] nvarchar(max) NULL,    [Permission] nvarchar(max) NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpDynamicProperties] PRIMARY KEY ([Id]));GOCREATE TABLE [AbpDynamicEntityProperties] (    [Id] int NOT NULL IDENTITY,    [EntityFullName] nvarchar(450) NULL,    [DynamicPropertyId] int NOT NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpDynamicEntityProperties] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpDynamicEntityProperties_AbpDynamicProperties_DynamicPropertyId] FOREIGN KEY ([DynamicPropertyId]) REFERENCES [AbpDynamicProperties] ([Id]) ON DELETE CASCADE);GOCREATE TABLE [AbpDynamicPropertyValues] (    [Id] int NOT NULL IDENTITY,    [Value] nvarchar(max) NOT NULL,    [TenantId] int NULL,    [DynamicPropertyId] int NOT NULL,    CONSTRAINT [PK_AbpDynamicPropertyValues] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpDynamicPropertyValues_AbpDynamicProperties_DynamicPropertyId] FOREIGN KEY ([DynamicPropertyId]) REFERENCES [AbpDynamicProperties] ([Id]) ON DELETE CASCADE);GOCREATE TABLE [AbpDynamicEntityPropertyValues] (    [Id] int NOT NULL IDENTITY,    [Value] nvarchar(max) NOT NULL,    [EntityId] nvarchar(max) NULL,    [DynamicEntityPropertyId] int NOT NULL,    [TenantId] int NULL,    CONSTRAINT [PK_AbpDynamicEntityPropertyValues] PRIMARY KEY ([Id]),    CONSTRAINT [FK_AbpDynamicEntityPropertyValues_AbpDynamicEntityProperties_DynamicEntityPropertyId] FOREIGN KEY ([DynamicEntityPropertyId]) REFERENCES [AbpDynamicEntityProperties] ([Id]) ON DELETE CASCADE);GOCREATE INDEX [IX_AbpDynamicEntityProperties_DynamicPropertyId] ON [AbpDynamicEntityProperties] ([DynamicPropertyId]);GOCREATE UNIQUE INDEX [IX_AbpDynamicEntityProperties_EntityFullName_DynamicPropertyId_TenantId] ON [AbpDynamicEntityProperties] ([EntityFullName], [DynamicPropertyId], [TenantId]) WHERE [EntityFullName] IS NOT NULL AND [TenantId] IS NOT NULL;GOCREATE INDEX [IX_AbpDynamicEntityPropertyValues_DynamicEntityPropertyId] ON [AbpDynamicEntityPropertyValues] ([DynamicEntityPropertyId]);GOCREATE UNIQUE INDEX [IX_AbpDynamicProperties_PropertyName_TenantId] ON [AbpDynamicProperties] ([PropertyName], [TenantId]) WHERE [PropertyName] IS NOT NULL AND [TenantId] IS NOT NULL;GOCREATE INDEX [IX_AbpDynamicPropertyValues_DynamicPropertyId] ON [AbpDynamicPropertyValues] ([DynamicPropertyId]);GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200805083139_Upgraded_To_Abp_5_11', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOALTER TABLE [AppBinaryObjects] ADD [Description] nvarchar(max) NULL;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20200928121432_Add_Description_To_Binary_Object', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOALTER TABLE [AbpPersistedGrants] ADD [ConsumedTime] datetime2 NULL;GOALTER TABLE [AbpPersistedGrants] ADD [Description] nvarchar(200) NULL;GOALTER TABLE [AbpPersistedGrants] ADD [SessionId] nvarchar(100) NULL;GOCREATE INDEX [IX_AbpPersistedGrants_Expiration] ON [AbpPersistedGrants] ([Expiration]);GOCREATE INDEX [IX_AbpPersistedGrants_SubjectId_SessionId_Type] ON [AbpPersistedGrants] ([SubjectId], [SessionId], [Type]);GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20201020131501_Upgraded_To_IdentityServer_v4', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOALTER TABLE [AbpEntityPropertyChanges] ADD [NewValueHash] nvarchar(max) NULL;GOALTER TABLE [AbpEntityPropertyChanges] ADD [OriginalValueHash] nvarchar(max) NULL;GOALTER TABLE [AbpDynamicProperties] ADD [DisplayName] nvarchar(max) NULL;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20201111120911_Upgraded_To_Abp_6_0', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOALTER TABLE [AbpDynamicPropertyValues] DROP CONSTRAINT [PK_AbpDynamicPropertyValues];GODECLARE @var2 sysname;SELECT @var2 = [d].[name]FROM [sys].[default_constraints] [d]INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AbpDynamicPropertyValues]') AND [c].[name] = N'Id');IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [AbpDynamicPropertyValues] DROP CONSTRAINT [' + @var2 + '];');ALTER TABLE [AbpDynamicPropertyValues] DROP COLUMN [Id];GOALTER TABLE [AbpDynamicPropertyValues] ADD [Id] bigint NOT NULL IDENTITY;GOALTER TABLE [AbpDynamicPropertyValues] ADD CONSTRAINT [PK_AbpDynamicPropertyValues] PRIMARY KEY ([Id]);GOALTER TABLE [AbpDynamicEntityPropertyValues] DROP CONSTRAINT [PK_AbpDynamicEntityPropertyValues];GODECLARE @var3 sysname;SELECT @var3 = [d].[name]FROM [sys].[default_constraints] [d]INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AbpDynamicEntityPropertyValues]') AND [c].[name] = N'Id');IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [AbpDynamicEntityPropertyValues] DROP CONSTRAINT [' + @var3 + '];');ALTER TABLE [AbpDynamicEntityPropertyValues] DROP COLUMN [Id];GOALTER TABLE [AbpDynamicEntityPropertyValues] ADD [Id] bigint NOT NULL IDENTITY;GOALTER TABLE [AbpDynamicEntityPropertyValues] ADD CONSTRAINT [PK_AbpDynamicEntityPropertyValues] PRIMARY KEY ([Id]);GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20201217075257_Upgrade_To_ABP_6_1', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GODROP INDEX [IX_AbpDynamicProperties_PropertyName_TenantId] ON [AbpDynamicProperties];DECLARE @var4 sysname;SELECT @var4 = [d].[name]FROM [sys].[default_constraints] [d]INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AbpDynamicProperties]') AND [c].[name] = N'PropertyName');IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [AbpDynamicProperties] DROP CONSTRAINT [' + @var4 + '];');ALTER TABLE [AbpDynamicProperties] ALTER COLUMN [PropertyName] nvarchar(256) NULL;CREATE UNIQUE INDEX [IX_AbpDynamicProperties_PropertyName_TenantId] ON [AbpDynamicProperties] ([PropertyName], [TenantId]) WHERE [PropertyName] IS NOT NULL AND [TenantId] IS NOT NULL;GODROP INDEX [IX_AbpDynamicEntityProperties_EntityFullName_DynamicPropertyId_TenantId] ON [AbpDynamicEntityProperties];DECLARE @var5 sysname;SELECT @var5 = [d].[name]FROM [sys].[default_constraints] [d]INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]WHERE ([d].[parent_object_id] = OBJECT_ID(N'[AbpDynamicEntityProperties]') AND [c].[name] = N'EntityFullName');IF @var5 IS NOT NULL EXEC(N'ALTER TABLE [AbpDynamicEntityProperties] DROP CONSTRAINT [' + @var5 + '];');ALTER TABLE [AbpDynamicEntityProperties] ALTER COLUMN [EntityFullName] nvarchar(256) NULL;CREATE UNIQUE INDEX [IX_AbpDynamicEntityProperties_EntityFullName_DynamicPropertyId_TenantId] ON [AbpDynamicEntityProperties] ([EntityFullName], [DynamicPropertyId], [TenantId]) WHERE [EntityFullName] IS NOT NULL AND [TenantId] IS NOT NULL;GOALTER TABLE [AbpAuditLogs] ADD [ExceptionMessage] nvarchar(1024) NULL;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20210224123746_Upgraded_To_Abp_6_3', N'5.0.10');GOCOMMIT;GOBEGIN TRANSACTION;GOINSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])VALUES (N'20210622135427_Upgraded_To_Abp_6_4_0', N'5.0.10');GOCOMMIT;GO

其中x为当前项目名

 var coreAssemblyDirectoryPath = Path.GetDirectoryName(typeof(SauryCoreModule).GetAssembly().Location);            if (coreAssemblyDirectoryPath == null)            {                throw new Exception("Could not find location of Saury.Core assembly!");            }            var directoryInfo = new DirectoryInfo(coreAssemblyDirectoryPath);            while (!DirectoryContains(directoryInfo.FullName, "x.sln"))            {                if (directoryInfo.Parent == null)                {                    throw new Exception("Could not find content root folder!");                }                directoryInfo = directoryInfo.Parent;            }            var webMvcFolder = Path.Combine(directoryInfo.FullName, $"x.Web.Mvc");            if (Directory.Exists(webMvcFolder))            {                return webMvcFolder;            }            throw new Exception("Could not find root folder of the web project!");

Core.Localization 下的多语言命令

WorkFlow 升级注意

workflow 需升级

    <PackageReference Include="WorkflowCore" Version="3.6.0" />    <PackageReference Include="WorkflowCore.DSL" Version="3.6.0" />    <PackageReference Include="WorkflowCore.Persistence.SqlServer" Version="3.6.0" />

WorkflowCore.DSL 包安装

如果重写了IExecutionResultProcessor

请注意 根据官方文件补齐内容
https://github.dev/danielgerlag/workflow-core

using System;using System.Collections.Generic;using System.Linq;using Microsoft.Extensions.Logging;using WorkflowCore.Interface;using WorkflowCore.Models;using WorkflowCore.Models.LifeCycleEvents;namespace WorkflowCore.Services{    public class ExecutionResultProcessor : IExecutionResultProcessor    {        private readonly IExecutionPointerFactory _pointerFactory;        private readonly IDateTimeProvider _datetimeProvider;        private readonly ILogger _logger;        private readonly ILifeCycleEventPublisher _eventPublisher;        private readonly IEnumerable<IWorkflowErrorHandler> _errorHandlers;        private readonly WorkflowOptions _options;        public ExecutionResultProcessor(IExecutionPointerFactory pointerFactory, IDateTimeProvider datetimeProvider, ILifeCycleEventPublisher eventPublisher, IEnumerable<IWorkflowErrorHandler> errorHandlers, WorkflowOptions options, ILoggerFactory loggerFactory)        {            _pointerFactory = pointerFactory;            _datetimeProvider = datetimeProvider;            _eventPublisher = eventPublisher;            _errorHandlers = errorHandlers;            _options = options;            _logger = loggerFactory.CreateLogger<ExecutionResultProcessor>();        }        public void ProcessExecutionResult(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, ExecutionResult result, WorkflowExecutorResult workflowResult)        {            pointer.PersistenceData = result.PersistenceData;            pointer.Outcome = result.OutcomeValue;            if (result.SleepFor.HasValue)            {                pointer.SleepUntil = _datetimeProvider.UtcNow.Add(result.SleepFor.Value);                pointer.Status = PointerStatus.Sleeping;            }            if (!string.IsNullOrEmpty(result.EventName))            {                pointer.EventName = result.EventName;                pointer.EventKey = result.EventKey;                pointer.Active = false;                pointer.Status = PointerStatus.WaitingForEvent;                workflowResult.Subscriptions.Add(new EventSubscription                {                    WorkflowId = workflow.Id,                    StepId = pointer.StepId,                    ExecutionPointerId = pointer.Id,                    EventName = pointer.EventName,                    EventKey = pointer.EventKey,                    SubscribeAsOf = result.EventAsOf,                    SubscriptionData = result.SubscriptionData                });            }            if (result.Proceed)            {                pointer.Active = false;                pointer.EndTime = _datetimeProvider.UtcNow;                pointer.Status = PointerStatus.Complete;                                                foreach (var outcomeTarget in step.Outcomes.Where(x => x.Matches(result, workflow.Data)))                {                                        workflow.ExecutionPointers.Add(_pointerFactory.BuildNextPointer(def, pointer, outcomeTarget));                }                var pendingSubsequents = workflow.ExecutionPointers                    .FindByStatus(PointerStatus.PendingPredecessor)                    .Where(x => x.PredecessorId == pointer.Id);                foreach (var subsequent in pendingSubsequents)                {                    subsequent.Status = PointerStatus.Pending;                    subsequent.Active = true;                }                _eventPublisher.PublishNotification(new StepCompleted                {                    EventTimeUtc = _datetimeProvider.UtcNow,                    Reference = workflow.Reference,                    ExecutionPointerId = pointer.Id,                    StepId = step.Id,                    WorkflowInstanceId = workflow.Id,                    WorkflowDefinitionId = workflow.WorkflowDefinitionId,                    Version = workflow.Version                });            }            else            {                foreach (var branch in result.BranchValues)                {                    foreach (var childDefId in step.Children)                    {                           workflow.ExecutionPointers.Add(_pointerFactory.BuildChildPointer(def, pointer, childDefId, branch));                                            }                }            }        }        public void HandleStepException(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer pointer, WorkflowStep step, Exception exception)        {            _eventPublisher.PublishNotification(new WorkflowError            {                EventTimeUtc = _datetimeProvider.UtcNow,                Reference = workflow.Reference,                WorkflowInstanceId = workflow.Id,                WorkflowDefinitionId = workflow.WorkflowDefinitionId,                Version = workflow.Version,                ExecutionPointerId = pointer.Id,                StepId = step.Id,                Message = exception.Message            });            pointer.Status = PointerStatus.Failed;                        var queue = new Queue<ExecutionPointer>();            queue.Enqueue(pointer);            while (queue.Count > 0)            {                var exceptionPointer = queue.Dequeue();                var exceptionStep = def.Steps.FindById(exceptionPointer.StepId);                var shouldCompensate = ShouldCompensate(workflow, def, exceptionPointer);                var errorOption = (exceptionStep.ErrorBehavior ?? (shouldCompensate ? WorkflowErrorHandling.Compensate : def.DefaultErrorBehavior));                foreach (var handler in _errorHandlers.Where(x => x.Type == errorOption))                {                    handler.Handle(workflow, def, exceptionPointer, exceptionStep, exception, queue);                }            }        }                private bool ShouldCompensate(WorkflowInstance workflow, WorkflowDefinition def, ExecutionPointer currentPointer)        {            var scope = new Stack<string>(currentPointer.Scope);            scope.Push(currentPointer.Id);            while (scope.Count > 0)            {                var pointerId = scope.Pop();                var pointer = workflow.ExecutionPointers.FindById(pointerId);                var step = def.Steps.FindById(pointer.StepId);                if ((step.CompensationStepId.HasValue) || (step.RevertChildrenAfterCompensation))                    return true;            }            return false;        }    }}

AppFeatureProvider 检查

AppConsts检查

对比Web.Core 下Controllers文件夹下内容

特别是FileController

时区设置

CoreModule.cs

 public class CoreModule : AbpModule    {        public override void PreInitialize()        {            Clock.Provider = ClockProviders.Utc;}            }            

注意2.2升级导致的linq计算方式出错

https://learn.microsoft.com/zh-cn/ef/core/what-is-new/ef-core-3.x/breaking-changes#linq-queries-are-no-longer-evaluated-on-the-client
image

  1. 对比Application.Authorization文件夹下文件
  2. 对比Application.Organizations文件夹下文件

检查Json转换库

System.Text.Json

JsonDocument.Parse(await response.Content.ReadAsStringAsync())var result = jsonDoc.RootElement;var errorCode = result.GetString("errcode");OAuthTokenResponse tokenstokens.Response.RootElement.GetString("code")

Swagger错误处理

                services.AddSwaggerGen(options =>                {                    options.SwaggerDoc("v1",new Microsoft.OpenApi.Models.OpenApiInfo() { });                    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme                    {                        Name = "Authorization",                        Type = SecuritySchemeType.ApiKey,                        Scheme = "Bearer",                        BearerFormat = "JWT",                        In = ParameterLocation.Header,                        Description = "JWT token Bearer"                    });                    //Resolve conflicting schemaIds - yue.fei 20190723.                    //options.CustomSchemaIds(x => x.FullName);                    options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());                    IncludeXmlComments(options);                });                  private void IncludeXmlComments(SwaggerGenOptions options)        {            var xmlFiles = System.IO.Directory.GetFiles(AppContext.BaseDirectory, "*.xml");            foreach (var file in xmlFiles)            {                               options.IncludeXmlComments(file);            }        }
❌