记录一次生产环境清理千万级redis数据

记录一次生产环境清理千万级redis数据

  • 一、场景
  • 二、批量生成模拟数据
    • 2.1、编写数据文件data.txt
      • 2.1.1、文件格式一
      • 2.1.2、文件格式二
    • 2.2、文件格式转码
    • 2.3、执行批量导入
      • 2.3.1、脚本生成模拟数据
      • 2.3.2、代码模拟数据
      • 2.3.3、执行导入
  • 三、批量删除
    • 3.1、使用keys *批量删除key
      • 3.1.1、单机部署测试
      • 3.1.2、集群部署测试
      • 3.1.3、通过脚本删除
    • 3.2、使用scan批量删除key
      • 3.2.1、单机部署测试
      • 3.2.2、集群部署测试
      • 3.2.3、通过脚本删除
    • 3.3、代码方式
  • 四、结论

一、场景

生产环境每天通过定时任务自动清理redis数据,由于使用的是其它项目的redis集群,其它项目把生产环境上的keys *命令禁用了,导致我们清理redis数据失败。

二、批量生成模拟数据

2.1、编写数据文件data.txt

2.1.1、文件格式一

*3
$3
set
$2
k1
$2
v1

*3
$3
set
$5
count
$6
123456

解释:

  • *3 表示有三个字符
  • $3 表示 set字符的长度为3,也就是我们的命令。
  • $5表示 key的长度为5,也就是count。
  • $6表示 key的长度为6,也就是123456。

不同的字符需要用换行隔开

2.1.2、文件格式二

简单直观,推荐使用。

set k1 v1
set k2 v2
set k3 v3

2.2、文件格式转码

## 文件格式转码,  会去掉行尾的^M符号
unix2dos  data.txt 

## unix2dos -k filename把一个文件从UNIX的断行符LF转为DOS的CRLF时
## 终端报错:-bash: unix2dos: command not found

## 解决:安装unix2dos、安装dos2unix(顺便安装)
yum install -y unix2dos dos2unix

2.3、执行批量导入

2.3.1、脚本生成模拟数据

#!/bin/sh

echo "构造数量:$1";
rm -f ./testdata.txt;
touch testdata.txt;

starttime=`date +'%Y-%m-%d %H:%M:%S'`;
echo "开始时间: $starttime";

for((i=0; i< $1 ;i++))
do
	total='set IMS:total:'${i}' '${i};
	last='set IMS:last:'${i}' '${i};
	echo  $total>> testdata.txt;
	echo  $last>> testdata.txt;
done

endtime=`date +'%Y-%m-%d %H:%M:%S'`;
echo "结束时间: $endtime";

start_seconds=$(date --date="$starttime" +%s);
end_seconds=$(date --date="$endtime" +%s);
echo "本次运行时间: "$((end_seconds-start_seconds))"s"

保存之后给脚本执行权限

chmod 777 dataBuild.sh

## 生成1w条数据
./dataBuild.sh 10000

2.3.2、代码模拟数据

public class MobileUtil {
    //中国移动
    public static final String[] CHINA_MOBILE = {
            "134", "135", "136", "137", "138", "139", "150", "151", "152", "157", "158", "159",
            "182", "183", "184", "187", "188", "178", "147", "172", "198"
    };
    //中国联通
    public static final String[] CHINA_UNICOM = {
            "130", "131", "132", "145", "155", "156", "166", "171", "175", "176", "185", "186", "166"
    };
    //中国电信
    public static final String[] CHINA_TELECOME = {
            "133", "149", "153", "173", "177", "180", "181", "189", "199"
    };

    /**
     * 生成手机号
     *
     * @param op 0 移动 1 联通 2 电信
     */
    public static String createMobile(int op) {
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        String mobile01;//手机号前三位
        int temp;
        switch (op) {
            case 0:
                mobile01 = CHINA_MOBILE[random.nextInt(CHINA_MOBILE.length)];
                break;
            case 1:
                mobile01 = CHINA_UNICOM[random.nextInt(CHINA_UNICOM.length)];
                break;
            case 2:
                mobile01 = CHINA_TELECOME[random.nextInt(CHINA_TELECOME.length)];
                break;
            default:
                mobile01 = "op标志位有误!";
                break;
        }
        if (mobile01.length() > 3) {
            return mobile01;
        }
        sb.append(mobile01);
        //生成手机号后8位
        for (int i = 0; i < 8; i++) {
            temp = random.nextInt(10);
            sb.append(temp);
        }
        return sb.toString();
    }

    public static String generateMobile() {
        Random random = new Random();
        int op = random.nextInt(3);//随机运营商标志位
        String mobile = createMobile(op);
        return mobile;
    }

    public static String generateSceneCode() {
        DecimalFormat decimalFormat = new DecimalFormat("0000");
        Random random = new Random();
        //生成1-600的整数
        int num = random.nextInt(600) + 1;
        String numFormat = decimalFormat.format(num);
        return numFormat;
    }

    public static String generateCount() {
        Random random = new Random();
        //生成1-50的整数
        int num = random.nextInt(50) + 1;
        return String.valueOf(num);
    }
}
public class Test01 {
    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            //生成手机号
            String mobile = MobileUtil.generateMobile();
            //生成场景码
            String sceneCode = MobileUtil.generateSceneCode();
            //生成随机total总数
            String count = MobileUtil.generateCount();
            list.add("set    IMS:total:" + sceneCode + ":" + mobile + "   " + count);
            list.add("set    IMS:last:" + sceneCode + ":" + mobile + "    " + System.currentTimeMillis());
        }
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter("C:\\Users\\Administrator\\Desktop\\data.txt");//创建文本文件
            for (int i = 0; i < list.size(); i++) {

                fileWriter.write(list.get(i) + "\r\n");//写入 \r\n换行

            }
            System.out.println("共" + list.size() + "条");
            fileWriter.flush();
            fileWriter.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

2.3.3、执行导入

## 使用pipe批量导入(分片集群必须要每个节点都要执行一次才可以导入成功)
cat data.txt | ./redis-cli -a redis\!\@\# -p 8001 -h 192.168.0.101 --pipe

## 报错
## MOVED 13861 192.168.0.101:8002
## MOVED 6169 192.168.0.101:8003
## Last reply received from server.

## 解决:使用 redis-cli -c 来启动redis集群模式 
cat data.txt | ./redis-cli -c -h 192.168.0.101 -p 8001 -a redis\!\@\#

通过 --pipe 来启动集群模式的,解决报错

  • 暴力型的
    你在集群模式下 --pipe 你会发现 key值的redis槽点在此节点上就写入成功,不在此节点就没有写入成功。可以把每个节点跑 --pipe 一次,那么每个节点就会写入自己的数据。
## 只需要在主节点上分别执行
cat data.txt | ./redis-cli -c -h 192.168.0.101 -p 8001 -a redis\!\@\#   --pipe
cat data.txt | ./redis-cli -c -h 192.168.0.102 -p 8001 -a redis\!\@\#   --pipe
cat data.txt | ./redis-cli -c -h 192.168.0.103 -p 8001 -a redis\!\@\#   --pipe
  • 细致型
    通过key 的哈希值 区分槽点,通过节点拿到槽点,这里不做演示。

三、批量删除

3.1、使用keys *批量删除key

xargs参数说明

  • -r: 当keys的数量为0时,就会报错,(error) ERR wrong number of arguments for ‘del’ command。
  • n1: 当集群情况keys的数量大于1时,不加会报错, (error) CROSSSLOT Keys in request don’t hash to the same slot。
  • -t: 加上-t会输出每次删除的内容,不加则不输出删除的内容,但还是会输出每次删除的key的数量。

3.1.1、单机部署测试

## 会有警告直接忽略 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
## 没有key会报(error) ERR wrong number of arguments for 'del' command
./redis-cli -h 127.0.0.1 -a redis\!\@\# keys 'IMS:last:*'|xargs -r./redis-cli -h 127.0.0.1 -a redis\!\@\# del

./redis-cli -h 127.0.0.1 -a redis\!\@\# keys 'IMS:total:*'|xargs -r ./redis-cli -h 127.0.0.1 -a redis\!\@\# del

3.1.2、集群部署测试

## 通过keys * 命令删除redis的key(对分片集群测试)
## 报错(error) CROSSSLOT Keys in request don't hash to the same slot
## 原因是因为哈希槽,在xargs参数后加上 -n1解决

## 每台节点上执行才可以删除
./redis-cli -c -h 192.168.0.101 -p 8001 -a redis\!\@\# keys  'IMS:last:*' |xargs -r -n1 ./redis-cli -c -h 192.168.0.101  -p 8001 -a redis\!\@\# del 

./redis-cli -c -h 192.168.0.101 -p 8001 -a redis\!\@\# keys 'IMS:total:*'|xargs -r -n1 ./redis-cli -c -h 192.168.0.101  -p 8001 -a redis\!\@\# del

3.1.3、通过脚本删除

# 核心命令
# 1.把KEYS结果写入文件
# 2.读取文件,执行DEL命令

# 用法示例:删除IMS:last:*这样的key数据
# ./deleteKeys.sh IMS:last:*
# ./deleteKeys.sh IMS:total:*

#!/bin/bash
redis_list=("192.168.0.101:8001" "192.168.0.102:8001" "192.168.0.103:8001")
redis_password='redis!@#'
redis_key_file='key.txt'
 
echo "Configuration info:"
echo "Redis cluster is $redis_list"
echo "Redis password is $redis_password"
echo "The key file is $redis_key_file"
rm -rf $redis_key_file
 
echo "-----------------------------------------------------------------------------------"
 
echo "Collecting the key list: "
for info in ${redis_list[@]}
do  
    echo "Running on :$info"  
    ip=`echo $info | cut -d \: -f 1`
    port=`echo $info | cut -d \: -f 2`
    ./redis-cli -h $ip -p $port -a $redis_password -c KEYS $1 2>/dev/null >> $redis_key_file
done 
echo "Key list collected."
echo "-----------------------------------------------------------------------------------"
 
echo "Deleting the key: "
for info in ${redis_list[@]}
do  
    echo "Running on :$info"  
    ip=`echo $info | cut -d \: -f 1`
    port=`echo $info | cut -d \: -f 2`
    cat $redis_key_file | xargs -t -n1 --verbose ./redis-cli -h $ip -p $port -a $redis_password -c del  
done 
echo "Deleted."

执行脚本

./deleteKeys.sh IMS:last:*
./deleteKeys.sh IMS:total:*

3.2、使用scan批量删除key

因为redis生产环境禁用了keys *命令,这里无法使用上面的方式,使用scan方式删除key

3.2.1、单机部署测试

## 先导入数据
cat data.txt | ./redis-cli -a redis\!\@\# --pipe

## xargs参数:-L 1000 返回条数(只有单机节点才生效)
## 指令表示xargs一次读取的行数,也就是每次删除key的数量,不要一次行读取太多数量key。
./redis-cli -p 6379 -a redis\!\@\# --scan --pattern "IMS:total:*" 2>/dev/null| xargs -r -L 100 ./redis-cli -p 6379 -a redis\!\@\# del 

./redis-cli -p 6379 -a redis\!\@\# --scan --pattern "IMS:last:*" 2>/dev/null | xargs -r -L 1000 ./redis-cli -p 6379 -a redis\!\@\# del 

3.2.2、集群部署测试

通过命令可以查看集群状态:

./redis-cli -a redis\!\@\# -p 8001 cluster nodes | grep master

删除

## 每个master节点都要执行一次分才可以删除
./redis-cli -h 192.168.0.101 -c -p 8001 -a redis\!\@\# --scan --pattern "IMS:total:0001:*" | xargs  -r -n1 ./redis-cli -h 192.168.0.101 -c -p 8001 -a redis\!\@\# del

./redis-cli -h 192.168.0.102 -c -p 8001 -a redis\!\@\# --scan --pattern "IMS:total:*" | xargs  -r -n1 -L 1000 ./redis-cli -h 192.168.0.102 -c -p 8002 -a redis\!\@\# del

./redis-cli -h 192.168.0.103 -c -p 8001 -a redis\!\@\# --scan --pattern "IMS:total:*" | xargs  -r -n1 -L 1000 ./redis-cli -h 192.168.0.103 -c -p 8003 -a redis\!\@\# del

3.2.3、通过脚本删除

#!/bin/bash

starttime=`date +'%Y-%m-%d %H:%M:%S'`;
echo "开始时间: $starttime";

redis_list=("192.168.0.101:8002" "192.168.0.101:8003" "192.168.0.103:8001")
redis_password='redis!@#'
redis_key_file='key.txt'
 
echo "Configuration info:"
echo "Redis cluster is $redis_list"
echo "Redis password is $redis_password"
echo "The key file is $redis_key_file"
rm -rf $redis_key_file
 
echo "-----------------------------------------------------------------------------------"
 
echo "Collecting the key list: "
for info in ${redis_list[@]}
do  
    echo "Running on :$info"  
    ip=`echo $info | cut -d \: -f 1`
    port=`echo $info | cut -d \: -f 2`
    ./redis-cli -h $ip -p $port -a $redis_password -c  --scan --pattern $1 2>/dev/null | xargs  -r -n1 -L 1000  >> $redis_key_file
done 
echo "Key list collected."
echo "-----------------------------------------------------------------------------------"
 
echo "Deleting the key: "
for info in ${redis_list[@]}
do  
    echo "Running on :$info"  
    ip=`echo $info | cut -d \: -f 1`
    port=`echo $info | cut -d \: -f 2`
    cat $redis_key_file | xargs -n1 -r --verbose ./redis-cli -h $ip -p $port -a $redis_password -c del  2>/dev/null
done 

endtime=`date +'%Y-%m-%d %H:%M:%S'`;
echo "结束时间: $endtime";

start_seconds=$(date --date="$starttime" +%s);
end_seconds=$(date --date="$endtime" +%s);
echo "本次运行时间: "$((end_seconds-start_seconds))"s"

执行脚本

## 6253 486s
./deleteScan.sh IMS:last:*
## 6457  518
./deleteScan.sh IMS:total:* 

3.3、代码方式

依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

配置文件

server:
  port: 9527
spring:
  redis:
    database: 0
    # redis密码 必须一致
    password: redis!@#
    timeout: 10000
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1
        min-idle: 0
    cluster:
      nodes:
        - 192.168.0.101:8001
        - 192.168.0.102:8001
        - 192.168.0.103:8001

配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(redisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(redisSerializer);
        //key haspmap序列化
        template.setHashKeySerializer(redisSerializer);
        return template;
    }
}

代码

@SpringBootTest
class RedisClusterCleanApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    private Integer totalKeyCount = 0;
    private Integer lastKeyCount = 0;

    @Test
    public void redisKeyCount() {

        try {
            redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
                Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("IMS:total:*")
                        .count(Integer.MAX_VALUE).build());
                while (cursor.hasNext()) {
                    totalKeyCount++;
                    cursor.next();
                }
                return null;
            });
        } catch (Throwable e) {
            e.printStackTrace();
        }

        try {
            redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
                Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("IMS:last:*")
                        .count(Integer.MAX_VALUE).build());
                while (cursor.hasNext()) {
                    lastKeyCount++;
                    cursor.next();
                }
                return null;
            });
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("total key的数量" + totalKeyCount);
        System.out.println("last key的数量" + lastKeyCount);
    }

    @Test
    public void redisKeyClean() {
        long start = System.currentTimeMillis();
        DecimalFormat decimalFormat = new DecimalFormat("0000");
        int count1 = 0;
        int count2 = 0;

        删除last
        for (int i = 0; i <= 600; i++) {
            long nowStart = System.currentTimeMillis();
            String sceneCode = decimalFormat.format(i);
            List<String> keyResultList = new ArrayList<>();
            try {
                redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
                    Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("IMS:last:" + sceneCode + ":*")
                            .count(Integer.MAX_VALUE).build());
                    while (cursor.hasNext()) {
                        keyResultList.add(new String(cursor.next()));
                    }
                    return null;
                });
            } catch (Throwable e) {
                e.printStackTrace();
            }

            //单个删除
            for (String key : keyResultList) {
                redisTemplate.delete(key);
            }

            //批量删除
            //redisTemplate.delete(keyResultList);

            count1 = count1 + keyResultList.size();

            long nowEnd = System.currentTimeMillis();
            System.out.println("删除[IMS:last:" + sceneCode + ":*], 数量:" + keyResultList.size() + " 耗时:" + (nowEnd - nowStart) + "ms");
        }

        //删除total
        for (int i = 0; i <= 600; i++) {
            long nowStart = System.currentTimeMillis();
            String sceneCode = decimalFormat.format(i);
            List<String> keyResultList = new ArrayList<>();
            try {
                redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
                    Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("IMS:total:" + sceneCode + ":*")
                            .count(Integer.MAX_VALUE).build());
                    while (cursor.hasNext()) {
                        keyResultList.add(new String(cursor.next()));
                    }
                    return null;
                });
            } catch (Throwable e) {
                e.printStackTrace();
            }

            //单个删除
            for (String key : keyResultList) {
                redisTemplate.delete(key);
            }

            //批量删除
            //redisTemplate.delete(keyResultList);

            count2 = count2 + keyResultList.size();

            long nowEnd = System.currentTimeMillis();
            System.out.println("删除[IMS:total:" + sceneCode + ":*], 数量:" + keyResultList.size() + " 耗时:" + (nowEnd - nowStart) + "ms");
        }

        long end = System.currentTimeMillis();
        System.out.println("删除" + (count1 + count2) + "个key, 总耗时:" + (end - start) / 1000 + "s");
    }
}

这里只是测试使用的单线程,在实际删除的时候使用多线程提升效率。

四、结论

通过测试,发现数据量过大之后通过脚本删除很慢,通过代码删除较快,最后通过修改代码,调用定时任务删除。

key的规则:项目名称:类型:场景码:手机号

以前的删除方式:通过项目名称:类型匹配,匹配到之后再进行删除

目前采用方式

  1. 查询所有的短信场景码,遍历场景码。
  2. 通过项目名称:类型:场景码匹配,然后通过线程池进行匹配和删除。
  3. 此定时任务只用一次,后期恢复原有删除方式。
  4. 在添加key时设置过期时间,防止此次情况的发生。

你可能感兴趣的:(Java,redis,数据库,缓存,分片集群,副本集)