情况是这样的,线上系统出问题了。有一个弱密码的判断功能,我做了缓存,结果用户修改了密码后,没有及时修改缓存,导致重复报弱密码,要求修改密码后才能执行下一步操作。
卡BUG了。
怎么办呢?首先,因为系统性能问题,我做了redis缓存,缓存时长是可配置的,但配置了10天;原则上,如果旅客修改了密码,会触发事件清空缓存的,但修改密码的接口比较多,代码没有覆盖到;现在唯一能抢救的,就是先把缓存时长配置到最低(几乎无缓存),然后想法清理掉原先的缓存值。
生产服务器是隔离的,我要去查看和操作redis,有点麻烦。幸好有一个运维接口,获得了redis所有的key值。但是,取出来的key值很多,有27M的大小,我需要通过匹配关系,取出需要删除的key值。于是写了一个cmd控制台小程序,把需要的值批量取出来,然后在生成curl脚本,回调服务器清空相关的缓存。
操作步骤如下:
1、通过运维接口,获得应用下redis的所有key值(但有27M大小,需要筛选)
2、临时编写一个cmd控制台小程序,筛选出要清理的key值,并批量生成curl脚本
3、在服务器,通过curl脚本调用接口的方式,清空对应的redis缓存。
相关代码:
一、获得应用下redis的所有key值
定义操作的枚举
import com.alibaba.fastjson.JSONObject;
import com.hna.hicloud.uni.user.common.util.JsonUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@AllArgsConstructor
@Getter
public enum RedisDevManageActionEnum {
GET_VALUE("get-value", "通过key获得值"),
GET_KEY("get-key", "获得keys"),
DELETE_ITEM("delete-item","删除"),
DELETE_BATCH("delete-batch","批量删除"),
UNKNOWN("unknown", "未知"),
;
private final String code;
private final String desc;
public static RedisDevManageActionEnum getItem(String pKey, RedisDevManageActionEnum defaultValue) {
if (pKey == null || pKey.isEmpty()) {
return defaultValue;
}
for (RedisDevManageActionEnum item : RedisDevManageActionEnum.values()) {
if (pKey.equals(item.getCode())) {
return item;
}
}
return defaultValue;
}
protected static String toJson() {
List<JSONObject> dataList = new ArrayList<>();
JSONObject item = null;
for (RedisDevManageActionEnum em : RedisDevManageActionEnum.values()) {
item = new JSONObject();
dataList.add(item);
item.put("id", em.getCode());
item.put("name", em.getDesc());
}
return JsonUtil.objToJsonStr(dataList);
}
public static void main(String[] args) {
String str = toJson();
System.out.println(str);
}
}
定义redis操作的相关方法
@Override
public RestResponse redisDevManage(String appId, RedisDevManageReqDTO req) {
String key = null;
String data = null;
RedisDevManageActionEnum actionEnum = RedisDevManageActionEnum.getItem(req.getAction(), RedisDevManageActionEnum.UNKNOWN);
switch (actionEnum) {
case GET_KEY:
// 例如,匹配所有以 “user:” 开头的键。user:*
// "*product*"
// 比如匹配以 “category-” 开头,后面跟着任意一个字符,再接着是 “-item” 结尾的键。category-?-item
// “*”:代表任意数量的任意字符。
// “?”:代表单个任意字符。
key = req.getKeyName();
if (StrUniUtil.strIsTrimEmpty(key)) {
key = "*";
}
return RestResponse.ok(redisTemplate.keys(key));
case GET_VALUE:
key = req.getKeyName();
data = redisTemplate.opsForValue().get(key);
if (StrUniUtil.strIsNotEmpty(data)) {
return RestResponse.ok(data);
} else {
return RestResponse.ok();
}
case DELETE_ITEM:
key = req.getKeyName();
CheckUtil.assertStrTrimNotEmpty(key, ErrorCode.ILLEGAL_ARGUMENT, "keyName不能为空");
return RestResponse.ok(redisTemplate.delete(key));
case DELETE_BATCH:
List<String> keyArray = StrUniUtil.strSplitRnToArray(req.getKeyName(), true, true, false, "");
boolean deleteItemResult = false;
JSONObject batchDeleteResult = new JSONObject();
for (String keyEach : keyArray) {
key = keyEach;
deleteItemResult = redisTemplate.delete(key);
batchDeleteResult.put(key, deleteItemResult);
}
return RestResponse.ok(batchDeleteResult);
case UNKNOWN:
default:
return RestResponse.ok(actionEnum);
}
}
获得的数据样例如下:
{
"code":0,
"data":[
"app::1179208949",
"channelAppInfo::user",
"user:center:userid:CZ:99:20250614214935",
"uc:HU::query:member:pwd:status:123456435",
"user:center:userid:MU:99:20250614215103",
"spring:session:expirations:1750210140000",
"uc:utils:country:list::79b5e4e45df93f47fu",
"spring:session:expirations:1750321200000",
"app::wx123",
"spring:session:sessions:9af1a112-a192-410d-8bf0-4451ab3496f3",
"channelAppInfo::user111",
"spring:session:expirations:1750058160000",
"spring:session:sessions:3544cc02-37dc-449e-8dd6-867ee1f14630",
"sysConfig::KAFKA_IS_PUSH",
"uni-risk-sit:uni-risk:email_blacklist",
"spring:session:sessions:d7d74fe3-702c-4d2d-ada6-3bd683b33f7f",
"app::web2111" ],
"requestId":"j0vchk6zedAhuT0614215155P99",
"traceId":""
}
上面只是简单样例,为了能提取
uc:HU::query:member:pwd:status:为前缀的key值,并生成在sh下能执行的curl脚本,编写cmd代码如下:
import cn.hutool.core.io.FileUtil;
public class ToolCommonOutFileTwo {
protected static void doOne() {
String fileName = "C:\\temps\\1.log";
String outFileName = "C:\\temps\\cidItem.log";
String outCurlFileName = "C:\\temps\\curl.sh";
String text = FileUtil.readUtf8String(fileName);
int idx = -1;
String cid = null;
int addNum = 0;
StringBuilder cidSb = new StringBuilder();
String curlText = "";
int curlIdx = 0;
while (true) {
idx = text.indexOf("\"uc:HU::query:member:pwd:status:");
if (idx == -1) {
break;
}
text = text.substring(idx + ("\"uc:HU::query:member:pwd:status:").length());
idx = text.indexOf("\",");
if (idx == -1) {
break;
}
cid = text.substring(0, idx);
++addNum;
cidSb.append(cid + '\n');
if (addNum >= 1000) {
FileUtil.appendUtf8String(cidSb.toString(), outFileName);
curlText = "curl -H'Content-Type:application/json' -H'ip:127.0.0.1' -X POST --data '{\"input\":\"" + cidSb.toString().replace("" + '\n', "\\n") + "\",\"action\":\"batch-delete\"}' http://my.app.com/ctrl/clearRedis";
++curlIdx;
FileUtil.appendUtf8String(curlText + '\n' + '\n', outCurlFileName);
FileUtil.appendUtf8String("echo " + curlIdx + '\n' + '\n', outCurlFileName);
cidSb = new StringBuilder();
addNum = 0;
System.out.println(curlIdx);
}
}
if (addNum > 0) {
FileUtil.appendUtf8String(cidSb.toString(), outFileName);
curlText = "curl -H'Content-Type:application/json' -H'ip:127.0.0.1' -X POST --data '{\"input\":\"" + cidSb.toString().replace("" + '\n', "\\n") + "\",\"action\":\"batch-delete\"}' http://my.app.com/ctrl/clearRedis";
++curlIdx;
FileUtil.appendUtf8String(curlText + '\n' + '\n', outCurlFileName);
FileUtil.appendUtf8String("echo " + curlIdx + '\n' + '\n', outCurlFileName);
cidSb = new StringBuilder();
addNum = 0;
System.out.println(curlIdx);
}
System.out.println("end");
}
public static void main(String[] args) {
doOne();
}
}
几点:
1、临时工具,代码里面固定写好参数
2、如果每条记录都写文件流,执行效率慢,改成1000条一批次写入
3、echo输出,在sh执行的时候,可以知道执行到什么位置
最后,输出效果如下:
生成的curl脚本执行效果:
总结:
1、通过接口获得redis的key值,应该有更好的方法,可以表达式获得指定前缀的,但之前没有研究,因而这次需要取出来的值做二次处理
2、cmd的程序,因为有27M的文件,内容比较多,只能考虑输出到文件,在二次处理。但如果逐条输出,性能比较慢,所以用1000条一批次的方案。