Redis--大部分人不知道的缓存击穿与缓存设置顺序的操蛋事

一、关于缓存设置顺序:

cache,在web设计中经常用,大家也都会知道设置cache可以提高系统的响应速度,在做这个cache的时候,不知道为什么,很多人的流程都是:读取数据时,先从cache中读取数据,获取不到再去DB中获取,再将数据设置到cache中;更新数据时,先更新DB,成功之后再更新cache。在这里看似简单并没有什么问题,但是实际上,DB和cache理应是一个事务操作,要么同时失败,要么同时成功。
所以,往往会有以下的错误:

在花溪等地区,都构建了全面的区域性战略布局,加强发展的系统性、市场前瞻性、产品创新能力,以专注、极致的服务理念,为客户提供成都做网站、网站建设 网站设计制作定制制作,公司网站建设,企业网站建设,高端网站设计,全网整合营销推广,成都外贸网站建设公司,花溪网站建设费用合理。

错误操作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%左右,所以这个比较好

二、缓存“击穿”处理:

一个设置了过期时间的cache,在它过期那一刻,大量的并发请求会直连DB,DB负载过重问题。

解决办法:
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

名称栏目:Redis--大部分人不知道的缓存击穿与缓存设置顺序的操蛋事
文章出自:http://scyanting.com/article/gepdps.html