熬了一個通宵,終於把7千萬個Key刪完了!

因爲有一條業務線不理想,高層決定下架業務。對於咱們技術團隊而言,其對應的全部服務器資源和其餘相關資源都要釋放。redis

釋放了 8 臺應用服務器;1 臺 ES 服務器;刪除分佈式定時任務中心相關的業務任務;備份並刪除 MySQL 數據庫;刪除 Redis 中相關的業務緩存數據。spring

CTO 指名點姓讓我帶頭衝鋒,才扣了我績效……好吧,衝~數據庫

其餘都還好,很少時就解決了。惟獨這刪除 Redis 中的數據,害得我又熬了一個通宵,真是折煞我也!緩存

難點分析服務器

共用 Redis 服務集羣運維

因爲這條業務線的數據在 Redis 大概在 3G 左右,徹底不必單獨建一個 Redis 服務集羣,本着能節約就節約的態度,當初就決定和其餘項目共享一個集羣(這個集羣配置:16 個節點,128G 內存,還算豪華吧~)分佈式

集羣配置以下:

ced13826458e4b1caf4548403b11201c

在這種共用集羣的狀況下,致使沒法簡單粗暴的釋放。所以只能選擇刪除 Key 的方式。ide

Key 命名不規範spring-boot

要刪除 Key,首先就要精準的定位出哪些 Key 須要刪除,若是勿刪 Key,會影響到其餘服務正常運轉!性能

若是 Key 自己設置了過時時間,但有些數據需是持久化的。然而那該死的項目經理一直催項目進度,致使開發人員在開發過程當中不少地方都沒有設計到位。

好比 Redis Key 散落在項目代碼的每一個角落;好比命名不是很規範。

真不知道是怎麼 Review 代碼!哦,想必是沒有時間 Review,那該死的項目經理……

我隨便截個支付服務中的 Key 命名:

65af026e27d1470890d35bb9c0afecc9

怎麼樣?是否是以爲咱們開發人員寫的代碼很 Low!別笑,在實際工做中,還有比這更 Low 的!但願你別遇到,否則真的很痛苦~

解決思路

通過以上的分析,咱們簡單概括以下:

  • 咱們真正關心的是那些未設置過時時間的 Key。

  • 不能誤刪除 Key,不然下個月績效也沒了。

  • 因爲 Key 的命名及使用及其不規範,致使 Key 的定位難度很大。

看來,經過 Scan 命令掃描匹配 Key 的方式行不通了。只能經過人肉搜索了。

幸而 Idea 的搜索大法好,這個項目中使用的是 spring-boot-starter-data-redis。

所以我經過搜索 RedisTemplate 和 StringRedisTemplate 定位全部操做 Redis 的代碼。

具體步驟以下:

  • 經過這些代碼統計出 Key 的前綴並錄入到文本中。

  • 經過 Python 腳本把載入文中中的的 Key 並在後面加上「*」通配符。

  • 經過 Python 腳本經過 Scan 命令掃描出這些 Key。

  • 爲了便於檢查,咱們並無直接使用 Del 命令刪除 Key,在刪除 Key 以前,先經過 debug object key 的方式獲得其序列化的長度,再執行刪除並返回序列化長度。這樣,咱們就能夠統計出全部 Key 的序列化長度來獲得咱們釋放的空間大小。

關鍵代碼以下:

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)

經過以上方式,可以統計出究竟釋放了多少內存了。因爲這個集羣是有特麼接近 7 千萬個 Key:

 

5a4c51abc1594caa8327e38c2da3876a

所以,等到了次日天亮,我睡眼朦朧的看了一下,終於刪除完畢了,時間 07:13,早高峯即未來臨……

知恥然後勇

歷來沒有經歷過因業務下線而清除資源的經驗。此次事情真心讓我以爲細微之處見真功夫的道理。

若是一開始咱們就可以遵循開發規範來使用和設計 Redis Key,也不至於浪費這麼多時間。

爲了讓 Key 的命名和使用更加規範,以及從此避免再次遇到這種狀況,下午睡醒以後,我就在 Redis 公共組件庫裏面添加了一個配置和自定義了 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 對字符串序列化新增前綴
 * @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;
    }
}

使用效果:爲了不再次發生這種工做低效而又不得不作的事情,咱們在開發規範中規定,新項目中 Redis 的使用必須設置此配置,前綴就設置爲:項目編號。

另外,一個模塊中的 Key 必須統必定義在二方庫的 RedisKeyConstant 類中。

配置以下:

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前綴設置的key Serializer
    redisTemplate.setKeySerializer(new PrefixStringKeySerializer());
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return redisTemplate;
}

 

經過以上方式,咱們至少能夠從項目維度來區分出 Key,避免了多個項目之間共用同一個集羣時而致使重複 Key 的問題。

 

從項目維度對 Key 進行了劃分。更方便管理和運維。若是對於 Key 的管理粒度要求更細,咱們甚至能夠細化到具體業務維度。

咱們在測試環境進行了壓測,增長 Key 前綴對 Redis 性能幾乎沒有影響。性能方面能接受。

總結

經過本次事情,我發現對於大多數開發者而言,差距其實不在於智力,而是在於態度。

好比此次事件暴露出來的問題:你們都知道要遵循開發規範,然而到了真正「打仗」的時候,負責這個項目的開發者卻沒有幾我的能始終如一的作好這些細微之事。

另外,Reviewer 的工做實際上是極其重要的,他就像那「紀檢委」,若是「紀檢委」都放水睜一隻眼閉一隻眼,那麻煩可就大了!千里之提,毀於平常的點滴鬆懈啊!

通過此次事件以後,若是上天再給一次這樣的機會,我必定會對項目經理說:接着奏樂,接着舞!

相關文章
相關標籤/搜索