ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

定时自动删除 Redis key

定时自动删除 Redis key

目录

1. 背景描述

2. 实现脚本

3. 自动执行

4. 另一种实现


1. 背景描述

使用 Redis 存储群发消息。当初匆忙上线,设计上有两个主要问题:一是在每个 userid 的 key 中存储消息体,从数据库的角度看,存在大量的数据冗余,占用大量存储空间。二是不设置 key 的过期时间,使得 Redis 像貔貅一样只进不出,不断膨胀。

由于对用户的群发消息量很大,使得 Redis 几天就内存使用率报警。开始时使用单实例 Redis,遇到报警就增加 maxmemory 配置。后来将单实例进行拆分,按照 userid 取模 4,拆分成 4 个 Redis 实例。但结果是治标不治本,依旧撑不了几天,架不住一个劲地狂发消息啊。不得已只能采用定期删除未读消息的方案来弥补设计缺陷。

2. 实现脚本

delmsg.sh 内容如下:

#!/bin/bash cd ~/delmsg/ # 已处理的最大 logid,首次为 0 max_logid=`cat max_logid.txt` # 从 mysql 导出三天前的数据,这部分可以删除。mod4 列用来标识 userid 的模数。 d=`date +'%Y-%m-%d'` mysql -u user1 -p123456 -h 10.10.10.1 -P3306 -D db1 -N -e " select logid, userid, msgorder, case when mod(userid,4)=0 then 'mod4_0' when mod(userid,4)=1 then 'mod4_1' when mod(userid,4)=2 then 'mod4_2' else 'mod4_3' end mod4 from t1 where yearMonth=year('$d')* 100 + month('$d') and createTime < date_sub('$d', interval 3 day) and logid > $max_logid order by logid" > yushu.txt # 如果导出文件为空则退出脚本 if [[ ! -s yushu.txt ]] then exit 1 fi ######## 处理 mod4_0(6379 端口的 redis 实例)######## # 生成 mod4_0 的 Redis 命令文件,每个文件不超过 50000 行,避免报 Redis 每秒命令数超限警告 grep mod4_0 yushu.txt | awk '{print "zremrangebyscore UnReadMessageEx_"$2 " " $3 " " $3}' | split -l 50000 -d -a 5 - yushu0_ # 由于可能发生自动主从 Redis 切换,需要从哨兵获取当前 6379 端口的 master ip master_ip=`/home/redis/redis-5.0.3/src/redis-cli -p 26379 info | grep 6379 | awk -F"[,=:]" '{print $7}'` # 删除消息。使用 --pipe 方式逐个处理 split 生成的命令文件,每个文件处理后 sleep 1 秒用于缓解负载。 ls -l yushu0_* | awk {'print "cat " $9 " | /home/redis/redis-5.0.3/src/redis-cli -h '$master' -p 6379 -a 123456 -n 1 --pipe; sleep 1;"'} > yushu0.sh chmod 755 yushu0.sh ./yushu0.sh ######## 处理 mod4_1(6380 端口的 Redis 实例)######## grep mod4_1 yushu.txt | awk '{print "zremrangebyscore UnReadMessageEx_"$2 " " $3 " " $3}' | split -l 50000 -d -a 5 - yushu1_ master_ip=`/home/redis/redis-5.0.3/src/redis-cli -p 26379 info | grep 6380 | awk -F"[,=:]" '{print $7}'` ls -l yushu1_* | awk {'print "cat " $9 " | /home/redis/redis-5.0.3/src/redis-cli -h '$master' -p 6380 -a 123456 -n 1 --pipe; sleep 1;"'} > yushu1.sh chmod 755 yushu1.sh ./yushu1.sh ######## 处理 mod4_2(6381 端口的 Redis 实例) ######## grep mod4_2 yushu.txt | awk '{print "zremrangebyscore UnReadMessageEx_"$2 " " $3 " " $3}' | split -l 50000 -d -a 5 - yushu2_ master_ip=`/home/redis/redis-5.0.3/src/redis-cli -p 26379 info | grep 6381 | awk -F"[,=:]" '{print $7}'` ls -l yushu2_* | awk {'print "cat " $9 " | /home/redis/redis-5.0.3/src/redis-cli -h '$master' -p 6381 -a 123456 -n 1 --pipe; sleep 1;"'} > yushu2.sh chmod 755 yushu2.sh ./yushu2.sh ######## 处理 mod4_3(6382 端口的 Redis 实例) ######## grep mod4_3 yushu.txt | awk '{print "zremrangebyscore UnReadMessageEx_"$2 " " $3 " " $3}' | split -l 50000 -d -a 5 - yushu3_ master_ip=`/home/redis/redis-5.0.3/src/redis-cli -p 26379 info | grep 6382 | awk -F"[,=:]" '{print $7}'` ls -l yushu3_* | awk {'print "cat " $9 " | /home/redis/redis-5.0.3/src/redis-cli -h '$master' -p 6382 -a 123456 -n 1 --pipe; sleep 1;"'} > yushu3.sh chmod 755 yushu3.sh ./yushu3.sh # 更新已处理的最大 logid tail -1 yushu.txt | awk '{print $1}' > max_logid.txt # 清理文件 rm -f yushu0_* yushu1_* yushu2_* yushu3_*

3. 自动执行

在 crontab 中增加项:

0 4 * * * delmsg.sh > delmsg.log 2>&1

4. 另一种实现

用一句查询生成 4 个文件,然后按端口映射关系,在对应的 Redis 主实例上并行执行删除。delete_redis_UnReadMessageEx.sh 脚本内容如下:

#!/bin/bash source ~/.bashrc cd ~ date +"%Y-%m-%d %H:%M:%S" # ============ 配置 ============ command_dir="/home/redis/redis-5.0.3/src" REDIS_PASSWORD=123456 SENTINEL_HOST=10.10.10.2 SENTINEL_PORT=30001 REDIS_DB=1 WORK_DIR="/home/mysql" # mod 对应的端口 declare -A port_map=( [0]=20007 [1]=20006 [2]=20015 [3]=20014 ) # 用环境变量传密码,避免出现在 ps 进程列表里 export REDISCLI_AUTH="$REDIS_PASSWORD" # ============ 第 1 步:从 MySQL 拉取待清理用户并分片 ============ # 先清理上次残留的 .tmp,避免 awk 的 >> 追加导致脏数据 rm -f ${WORK_DIR}/mod{0,1,2,3}_UnReadMessageEx.tmp mysql -uroot -p'H}+3v5mP9D6JuG' -h172.18.10.81 -P18251 -r -e " select concat('unlink UnReadMessageEx_', userid, '|', mod(userid,4)) from ( select userid, max(updateTime) updateTime from space.device_user group by userid ) t where t.updatetime < DATE_SUB(CURDATE(), INTERVAL 30 DAY); " -N 2>/dev/null \ | awk -F'|' ' $2 ~ /^[0-3]$/ { print $1 >> "/home/mysql/mod" $2 "_UnReadMessageEx.tmp" } END { for (i = 0; i <= 3; i++) { system("mv /home/mysql/mod" i "_UnReadMessageEx.tmp /home/mysql/mod" i "_UnReadMessageEx.txt 2>/dev/null") } } ' # ============ 第 2 步:并行处理 4 个分片 ============ pids=() for i in 0 1 2 3; do ( port=${port_map[$i]} log="${WORK_DIR}/mod${i}_UnReadMessageEx.log" file="${WORK_DIR}/mod${i}_UnReadMessageEx.txt" # 通过 Sentinel 动态发现该端口的 master master_ip=`$command_dir/redis-cli -h $SENTINEL_HOST -p $SENTINEL_PORT info 2>/dev/null \ | grep ":$port" | awk 'BEGIN{FS="[=:]"}{print $5}' | head -1` { echo "[$(date +'%F %T')] mod${i} -> ${master_ip}:${port}" if [ -z "$master_ip" ]; then echo "ERROR: 无法从 Sentinel 获取端口 ${port} 的 master IP" exit 1 fi if [ -f "$file" ] && [ -s "$file" ]; then result=$(cat "$file" | $command_dir/redis-cli -h "$master_ip" -p "$port" -n "$REDIS_DB" --pipe 2>/dev/null) echo "$result" if echo "$result" | grep -q "errors: 0"; then echo "Processed mod${i} -> ${master_ip}:${port}" else echo "WARN: mod${i} -> ${master_ip}:${port} 存在执行错误" fi $command_dir/redis-cli -h "$master_ip" -p "$port" info 2>/dev/null \ | egrep 'used_memory_human|maxmemory_human' else echo "Skip mod${i}: file empty or not exist" fi } > "$log" 2>&1 echo "mod${i} done (log: $log)" ) & pids+=($!) done wait "${pids[@]}" # ============ 第 3 步:汇总 ============ echo "----------------------------------------" echo "[$(date +'%F %T')] 所有分片处理完成,汇总如下:" for i in 0 1 2 3; do echo "===== mod${i} =====" cat "${WORK_DIR}/mod${i}_UnReadMessageEx.log" 2>/dev/null done echo "----------------------------------------"
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进