天天看點

Redis--大部分人不知道的緩存擊穿與緩存設定順序的操蛋事

一、關于緩存設定順序:

錯誤操作1:更新DB,同時寫入cache 
eg:程序A寫了cache,此時程序B打斷了A,又寫cache,并寫了DB,再次輪到程序A繼續寫DB,
此時會導緻,cache中儲存的是B寫入的資料,而DB中儲存了A寫入的資料,最終資料不一緻,而且
這個cache一直都是髒資料,如果此時不斷有程序來讀取,都是存在的cache髒資料;同理,如果先
寫DB,在寫cache,一樣存在可能被打斷,最終導緻cache是髒資料的問題

錯誤操作2,先删除cache,再更新DB,高并發時可能出現的問題:
eg:程序A先删除了cache,此時程序B打斷A,則從DB中讀取舊資料,并設定到了cache,再回來
程序A更新DB,那麼從這裡開始,接下去所有的讀請求,都是舊cache,而且一直都是髒資料

正确的做法應該是:
1、讀:先從DB讀取之後,再寫到cache中
2、更新:先更新 DB 中的資料,再删除 cache (必須是删除,而不是更新cache)
但是,這樣一樣不能保證不出錯
eg:A程序讀DB,B程序打斷A,進行DB的更新,删除cache,再回來A程序寫入到cache,一樣
cache中是舊資料,而且一直是髒資料,但是,讀資料庫操作很快,寫資料庫操作比較慢,讓一個慢
的操作打斷快的相對機率比較低,是以采用這種方式,至于這裡為什麼是删除cache,而不是更新
cache,那是因為,如果A程序更新DB,此時B程序更新DB,同時更新cache,A程序再回來更新
cache,将會導緻cache中的是髒資料           
def worker_read_type1(write_flag, user_workid):
        ''先從cache中讀,擷取不到,再從DB讀取之後,再寫到cache中''
        num = 0
        err_num = 0
        while True:
                redis_key = 't_users:'+str(user_workid)
                user_name = redis_db.get(redis_key)
                if not user_name:
                        sql = "select user_workid, user_name from t_users where user_workid={user_workid} limit 1".format(user_workid=user_workid)
                        data = mysql_extract_db.query_one_dict(sql=sql)
                        user_name = data.get('user_name', '')
                        redis_db.set(redis_key, user_name)
                num += 1
                if len(write_flag):
                        if user_name != write_flag[0]:
                                err_num += 1
                                print '出現不一緻--user_name:{}---write_flag:{}, errpercent: err_num/num={}'.format(user_name, write_flag[0], str(float(err_num*100)/num)+'%')
                time.sleep(0.01)

    def worker_update_type1(write_flag, user_workid, user_name):
        "'先更新DB,然後更新cache'''
        while True:
                sql = "update t_users set user_name='{user_name}' where user_workid={user_workid} ".format(user_name=user_name, user_workid=user_workid)
                res = mysql_extract_db.execute_commit(sql=sql)
                write_flag[0] = user_name  #寫入資料庫後的值
                if res:
                        redis_key = 't_users:'+str(user_workid)
                        redis_db.set(redis_key, user_name)  #再更新cache
                time.sleep(0.01)

def worker_update_type2(write_flag, user_workid, user_name):
        ''
        先更新cache,再更新DB
        ''
        while True:
                redis_key = 't_users:'+str(user_workid)
                redis_db.set(redis_key, user_name)  #更新緩存
                sql = "update t_users set user_name='{user_name}' where user_workid={user_workid} ".format(user_name=user_name, user_workid=user_workid)
                res = mysql_extract_db.execute_commit(sql=sql)
                write_flag[0] = user_name  #寫入資料庫後的值
                time.sleep(0.01)

def worker_update_type3(write_flag, user_workid, user_name):
        ''
        先删除cache,再更新DB
        ''
        while True:
                redis_key = 't_users:'+str(user_workid)
                redis_db.delete(redis_key)  #删除緩存
                sql = "update t_users set user_name='{user_name}' where user_workid={user_workid} ".format(user_name=user_name, user_workid=user_workid)
                res = mysql_extract_db.execute_commit(sql=sql)
                write_flag[0] = user_name  #寫入資料庫後的值
                time.sleep(0.01)

def worker_update_type4(write_flag, user_workid, user_name):
        ''
        先更新DB,再删除cache
        ''
        while True:
                begin = time.time()
                sql = "update t_users set user_name='{user_name}' where user_workid={user_workid} ".format(user_name=user_name, user_workid=user_workid)
                res = mysql_extract_db.execute_commit(sql=sql)
                write_flag[0] = user_name  #寫入資料庫後的值
                if res:
                        redis_key = 't_users:'+str(user_workid)
                        redis_db.delete(redis_key)  #删除緩存
                time.sleep(0.01)

def test_check_run(read_nump=1, wri_nump=2, readfunc=None, wrifunc=None):
        "運作測試"
        write_flag = Manager().list()
        write_flag.append('1')
        for i in range(0, wri_nump):
                    p_write = Process(target=wrifunc, args=(write_flag, 2633,'RobotZhu'+str(random.randrange(1, 10000000))))
                p_write.start()
        for i in range(0, read_nump):
                p_read = Process(target=readfunc, args=(write_flag, 2633, ))
                p_read.start()
        print 'p is running'
        while True:
                pass            

#下面運作測試看看,一般來說,系統的讀請求遠遠大于寫請求,這裡100個程序讀,2個程序寫
test_check_run(read_nump=100, wri_nump=2, readfunc=worker_read_type1, wrifunc=worker_update_type1)  #先更新DB,再更新cache,多程序寫有問題
出現不一緻--user_name:RobotZhu2038562---write_flag:RobotZhu669457, errpercent: err_num/num=11.4285714286%  出現資料不一緻的情況機率是11.5%左右
test_check_run(read_nump=100, wri_nump=2, readfunc=worker_read_type1, wrifunc=worker_update_type2)  #先更新cache,再更新DB,多程序寫有問題
出現不一緻--user_name:RobotZhu4607997---write_flag:RobotZhu8633737, errpercent: err_num/num=53.8461538462% 出現資料不一緻的情況機率是50%左右
test_check_run(read_nump=100, wri_nump=2, readfunc=worker_read_type1, wrifunc=worker_update_type3)  #先删除cache,再更新DB,讀程序打斷寫程序時有嚴重問題
出現不一緻--user_name:RobotZhu2034159---write_flag:RobotZhu4882794, errpercent: err_num/num=23.9436619718%  不一緻機率20%多
test_check_run(read_nump=100, wri_nump=2, readfunc=worker_read_type1, wrifunc=worker_update_type4)  #先更新DB,再删除cache,寫程序打斷讀程序是有問題
出現不一緻--user_name:RobotZhu1536990---write_flag:RobotZhu1536990, errpercent: err_num/num=7.69230769231%  資料不一緻機率7%左右,是以這個比較好           

二、緩存“擊穿”處理:

解決辦法:
1、當擷取資料發現為空時,說明cache過期了,此時不馬上連接配接DB,而是類似redis中的SETNX語
法,設定一個tempkey=1,如果這個tempkey存在,則設定失敗,不存在則設定成功, 設定成功,則
進行DB讀取資料,寫入cache,否則延時30s,再次重試讀cache,可能就有資料了。為什麼這麼
做?因為多程序并發的時候,第一個發現cache失效了,設定了tempkey,進行DB讀資料,其他程序
則因為無法設定tempkey而等待一會,再讀資料。           
代碼示例:
def get_data(key=None):
  value = redis.get(key)
  if not value:
      #緩存失效
      if 1==redis.setnx(key+'tempkey', 1, 60):   #設定一個臨時key,如果被其他程序設定過了,則設定失敗,也就不會連接配接db
          value = db.query('select name from test')
          redis.set(key, value)
          redis.delete(key+'tempkey')
     else:
         time.sleep(10)
         get_data(key)  #遞歸重試,或許已經可以直接從cache中擷取了
 else:
     return value           

繼續閱讀