熬了一個(gè)通宵,終于把7千萬(wàn)個(gè)Key刪完了
鏈接:https://juejin.im/post/6854573215726075917
由于有一條業(yè)務(wù)線不理想,高層決定下架業(yè)務(wù)。對(duì)于我們技術(shù)團(tuán)隊(duì)而言,其對(duì)應(yīng)的所有服務(wù)器資源和其他相關(guān)資源都要釋放。
釋放了 8 臺(tái)應(yīng)用服務(wù)器;1 臺(tái) ES 服務(wù)器;刪除分布式定時(shí)任務(wù)中心相關(guān)的業(yè)務(wù)任務(wù);備份并刪除 MySQL 數(shù)據(jù)庫(kù);刪除 Redis 中相關(guān)的業(yè)務(wù)緩存數(shù)據(jù)。
CTO 指名點(diǎn)姓讓我?guī)ь^沖鋒,才扣了我績(jī)效……好吧,沖~
其他都還好,不多時(shí)就解決了。唯獨(dú)這刪除 Redis 中的數(shù)據(jù),害得我又熬了一個(gè)通宵,真是折煞我也!
難點(diǎn)分析
共用 Redis 服務(wù)集群
由于這條業(yè)務(wù)線的數(shù)據(jù)在 Redis 大概在 3G 左右,完全沒(méi)必要單獨(dú)建一個(gè) Redis 服務(wù)集群,本著能節(jié)約就節(jié)約的態(tài)度,當(dāng)初就決定和其他項(xiàng)目共享一個(gè)集群(這個(gè)集群配置:16 個(gè)節(jié)點(diǎn),128G 內(nèi)存,還算豪華吧~)
集群配置如下:

在這種共用集群的情況下,導(dǎo)致無(wú)法簡(jiǎn)單粗暴的釋放。因此只能選擇刪除 Key 的方式。
Key 命名不規(guī)范
要?jiǎng)h除 Key,首先就要精準(zhǔn)的定位出哪些 Key 需要?jiǎng)h除,如果勿刪 Key,會(huì)影響到其他服務(wù)正常運(yùn)轉(zhuǎn)!
如果 Key 本身設(shè)置了過(guò)期時(shí)間,但有些數(shù)據(jù)需是持久化的。然而那該死的項(xiàng)目經(jīng)理一直催項(xiàng)目進(jìn)度,導(dǎo)致開(kāi)發(fā)人員在開(kāi)發(fā)過(guò)程中很多地方都沒(méi)有設(shè)計(jì)到位。
比如 Redis Key 散落在項(xiàng)目代碼的每個(gè)角落;比如命名不是很規(guī)范。
真不知道是怎么 Review 代碼!哦,想必是沒(méi)有時(shí)間 Review,那該死的項(xiàng)目經(jīng)理……
我隨便截個(gè)支付服務(wù)中的 Key 命名:

怎么樣?是不是覺(jué)得我們開(kāi)發(fā)人員寫(xiě)的代碼很 Low!別笑,在實(shí)際工作中,還有比這更 Low 的!希望你別遇到,不然真的很痛苦~
解決思路
經(jīng)過(guò)以上的分析,我們簡(jiǎn)單歸納如下:
我們真正關(guān)心的是那些未設(shè)置過(guò)期時(shí)間的 Key。
不能誤刪除 Key,否則下個(gè)月績(jī)效也沒(méi)了。
由于 Key 的命名及使用及其不規(guī)范,導(dǎo)致 Key 的定位難度很大。
通過(guò)這些代碼統(tǒng)計(jì)出 Key 的前綴并錄入到文本中。
通過(guò) Python 腳本把載入文中中的的 Key 并在后面加上“*”通配符。
通過(guò) Python 腳本通過(guò) Scan 命令掃描出這些 Key。
為了便于檢查,我們并沒(méi)有直接使用 Del 命令刪除 Key,在刪除 Key 之前,先通過(guò) debug object key 的方式得到其序列化的長(zhǎng)度,再執(zhí)行刪除并返回序列化長(zhǎng)度。這樣,我們就可以統(tǒng)計(jì)出所有 Key 的序列化長(zhǎng)度來(lái)得到我們釋放的空間大小。
關(guān)鍵代碼如下:
def get_key(rdbConn,start):
try:
keys_list = rdbConn.scan(start,count=2000)
return keys_list
except Exception,e:
print e
''' Redis DEBUG OBJECT command got key info '''
def get_key_info(rdbConn,keyName):
try:
rpiple = rdbConn.pipeline()
rpiple.type(keyName)
rpiple.debug_object(keyName)
rpiple.ttl(keyName)
key_info_list = rpiple.execute()
return key_info_list
except Exception,e:
print "INFO : ",e
def redis_key_static(key_info_list):
keyType = key_info_list[0]
keySize = key_info_list[1]['serializedlength']
keyTtl = key_info_list[2]
key_size_static(keyType,keySize,keyTtl)
通過(guò)以上方式,能夠統(tǒng)計(jì)出究竟釋放了多少內(nèi)存了。由于這個(gè)集群是有特么接近 7 千萬(wàn)個(gè) Key:

知恥而后勇
從來(lái)沒(méi)有經(jīng)歷過(guò)因業(yè)務(wù)下線而清除資源的經(jīng)驗(yàn)。這次事情真心讓我覺(jué)得細(xì)微之處見(jiàn)真功夫的道理。
如果一開(kāi)始我們就能夠遵循開(kāi)發(fā)規(guī)范來(lái)使用和設(shè)計(jì) Redis Key,也不至于浪費(fèi)這么多時(shí)間。
為了讓 Key 的命名和使用更加規(guī)范,以及今后避免再次遇到這種情況,下午睡醒之后,我就在 Redis 公共組件庫(kù)里面添加了一個(gè)配置和自定義了 Key 序列化。
代碼如下:
@ConfigurationProperties(prefix = "spring.redis.prefix")
public class RedisKeyPrefixProperties {
private Boolean enable = Boolean.TRUE;
private String key;
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
/**
* @desc 對(duì)字符串序列化新增前綴
* @author create by liming sun on 2020-07-21 14:09:51
*/
public class PrefixStringKeySerializer extends StringRedisSerializer {
private Charset charset = StandardCharsets.UTF_8;
private RedisKeyPrefixProperties prefix;
public PrefixStringKeySerializer(RedisKeyPrefixProperties prefix) {
super();
this.prefix = prefix;
}
@Override
public String deserialize(@Nullable byte[] bytes) {
String saveKey = new String(bytes, charset);
if (prefix.getEnable() != null && prefix.getEnable()) {
String prefixKey = spliceKey(prefix.getKey());
int indexOf = saveKey.indexOf(prefixKey);
if (indexOf > 0) {
saveKey = saveKey.substring(indexOf);
}
}
return (saveKey.getBytes() == null ? null : saveKey);
}
@Override
public byte[] serialize(@Nullable String key) {
if (prefix.getEnable() != null && prefix.getEnable()) {
key = spliceKey(prefix.getKey()) + key;
}
return (key == null ? null : key.getBytes(charset));
}
private String spliceKey(String prefixKey) {
if (StringUtils.isNotBlank(prefixKey) && !prefixKey.endsWith(":")) {
prefixKey = prefixKey + "::";
}
return prefixKey;
}
}
使用效果:為了避免再次發(fā)生這種工作低效而又不得不做的事情,我們?cè)陂_(kāi)發(fā)規(guī)范中規(guī)定,新項(xiàng)目中 Redis 的使用必須設(shè)置此配置,前綴就設(shè)置為:項(xiàng)目編號(hào)。
另外,一個(gè)模塊中的 Key 必須統(tǒng)一定義在二方庫(kù)的 RedisKeyConstant 類(lèi)中。
配置如下:
spring:
redis:
prefix:
enable: true
key: E00P01
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 支持key前綴設(shè)置的key Serializer
redisTemplate.setKeySerializer(new PrefixStringKeySerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
總結(jié)
通過(guò)本次事情,我發(fā)現(xiàn)對(duì)于大多數(shù)開(kāi)發(fā)者而言,差距其實(shí)不在于智力,而是在于態(tài)度。
比如這次事件暴露出來(lái)的問(wèn)題:大家都知道要遵循開(kāi)發(fā)規(guī)范,然而到了真正“打仗”的時(shí)候,負(fù)責(zé)這個(gè)項(xiàng)目的開(kāi)發(fā)者卻沒(méi)有幾個(gè)人能始終如一的做好這些細(xì)微之事。
另外,Reviewer 的工作其實(shí)是極其重要的,他就像那“紀(jì)檢委”,如果“紀(jì)檢委”都放水睜一只眼閉一只眼,那麻煩可就大了!千里之提,毀于日常的點(diǎn)滴松懈??!
經(jīng)過(guò)這次事件之后,如果上天再給一次這樣的機(jī)會(huì),我一定會(huì)對(duì)項(xiàng)目經(jīng)理說(shuō):接著奏樂(lè),接著舞!
-END-
PS:歡迎在留言區(qū)留下你的觀點(diǎn),一起討論提高。如果今天的文章讓你有新的啟發(fā),歡迎轉(zhuǎn)發(fā)分享給更多人。 Java后端編程交流群已成立 公眾號(hào)運(yùn)營(yíng)至今,離不開(kāi)小伙伴們的支持。為了給小伙伴們提供一個(gè)互相交流的平臺(tái),特地開(kāi)通了官方交流群。掃描下方二維碼備注 進(jìn)群 或者關(guān)注公眾號(hào) Java后端編程 后獲取進(jìn)群通道。 —————END————— 推薦閱讀:
MyBatis動(dòng)態(tài)SQL,寫(xiě)SQL更爽 面試官:String長(zhǎng)度有限制嗎?是多少? 阿里首推的 SpringBoot + Vue 全棧項(xiàng)目 憑啥不能用uuid做MySQL的主鍵??? SpringBoot+vue.js搭建圖書(shū)管理系統(tǒng) MyBatis-Plus常用API全套教程,看完沒(méi)有不懂的
最近面試BAT,整理一份面試資料《Java面試BAT通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。 獲取方式:關(guān)注公眾號(hào)并回復(fù) java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。 明天見(jiàn)(??ω??)??
