memcached是一款优秀的分布式key-value缓存框架。使用,集成简单方便。
但是对于如何优化数据库查询中,具体的使用方法网上相关的资源非常少。这里我给出自己摸索的经验方法与大家共享一下。
这里主要讲 如何给所有的表添加一种统一的使用缓存的办法,以及使用缓存使用中会为出现的问题,及统一的解决策略。
(以下是代码是基于python+Django,但其原理不限于此,但python语言的灵活强大在这里也可以得到很好的体现,试想要是在C++上实现这一切,就不是那么简单的一些事情)
首先我们假设创建了一个表User, 有id,name,password等荐,其中id为primary key,name为key.
1,基本的memcached使用方法
那么最简单的使用memcached的方法即为
cache.get('user_id_123')
cache.set('user_id_123', user )
2,何种查询使用memcached
这里只针对返回单条数据的查询做memcached,如果是返回集合的做cache,对于何时失效,如何失效的策略非常复杂,其开销也非常大,所以目前还没寻找到一个好的方法。所以这里只针对特定key的查询进行cache.
比如user这张表,可以定义按id进行查询,也可以按name进行查询。 又如有Item这张表,其构成为id,character_id,type,number. id为primary key, (character_id,type)为key.那么可以按id进行查询,也可以按(character_id,type)进行查询。像(character_id,type)这样的,我们可以称做侯选key。这种查询更接近于我们数据库的使用情况,做缓存的意义更大一些,也更加复杂,我们主要就是对其进行讨论。
后面我再详细说如何以统一的接口来定义侯选key.
3,如何为查询生成key
def makeKey( classA,*args,**kwargs):
endword = ""
keys = kwargs.keys()
keys.sort()
for key in keys:
endword += "_"+key
endword += "%s"%(kwargs[key])
return classA.__name__+endword
如MakeKey( User,name='123')返回 User_name_123
如MakeKey( Item,character_id=1,type=3)或( Item,type=3,character_id=1)都返回Item_character_id_1_type_3
4,统一的get方法
def getFromDBWithId( className,*args, **kwargs ):
key = makeKey(className,*args, **kwargs )
value = cache.get(key )
if( not value ):
try:
value = className.objects.get(*args,**kwargs )
except:
value = None
cache.set( key, value )
return value
5,何时添加缓存
get不命中时
insert时
6,何时缓存失效
在任何对缓存进行操作时,缓存失效:delete, update
7,update的注意事项
首先这里会有一个前提假设,即数据库的主健id不会发生变化。
即不能user = User(id=123)
user.id = 124
user.save()
一般我们在使用数据库时,都不会修改主健ID。对于侯选健我们一般也不会这样做。
如果侯选健不发生变化,那么update时的操作非常简单:cache.set(key,newValue),重新赋值即可。
如果侯选健发生变化,那上面的办法就行不通了。
试想user = getFromDBWithId(User,name='xxx')
user.name ='ddd'
user.save()
如果用上面的方法,在查询 getFromDBWithId(User,name='xxx')时还会返回数据,而不是返回空
但即侯选健发生了变化,但主健id不会发生变化,我们也可以通过以下的方法来解决上面这个问题:
数据内容仅由id为key的来进行存储,侯选key存储的是与id的一个双向索引。
比如user = User( name='xxx'),其实在内存中有三部分构成:
1,cache.set('User_id_111',user)
2,cache.set('User_id_to_name_111','xxx')
3,cache.set('User_name_to_id_xxx',111)
这样我们再来看上面的情况
user = getFromDBWithId(User,name='xxx')
user.name ='ddd'
user.save()
在save时,我们这样处理,就不会有问题了:
1,oldName = cache.get('User_id_to_name_%s'%user.id) # (oldName='xxx')
2,cache.invalide('User_name_to_id_%s'%oldName)
3,cache.set('User_id_to_name_%s'user.id,user.name)
4,cache.set('User_name_to_id_%s'%user.name,user.id)
5,cache.set('User_id_%s'%user.id,user)
8,统一加入缓存
上面我们给出了在User表上加入缓存的方法,当然我们不希望每个表都需要这样写一遍,高举DRY( do not reapeat yourself)的程序员肯定能想到更高明的办法。
切面编程?如果只需要写一个save()函数,然后注册的类,在更新表项时,都会被其改写,你只需要在外面配置哪些表需要加入cache,而完全不用从内部对其进行任务改写,这样会不会听上去很酷?
python切面编程的库有很多,我这里使用的aspects
1,这里我们需要每个表下定义一个CACHE_KEY(即我们的侯选key)
如
class User( models.Model ):
name = models.CharField( max_length=20 )
password = models.CharField( max_length=32 )
CACHE_KEY = ["name"]
我们来定义我们想重用的delete
def newDelete(self):
className = self.__class__.__name__
key = className
for subKey in self.__class__.CACHE_KEY :
key += "_%s_"%subKey+str(getattr( self,subKey ))
cache.delete(key) #选移除缓存,再从数据库中移出
retval = yield aspects.proceed(self)
yield aspects.return_stop(retval)
定义新的Save(如果侯选key内容会变,可以参照第7点进行改动)
def newSave(self):
etval = yield aspects.proceed(self) #数据库执行update
className = self.__class__.__name__
key = className
for subKey in self.__class__.CACHE_KEY :
key += "_%s_"%subKey+str(getattr( self,subKey ))
cache.set(key,self)
yield aspects.return_stop(retval)
搜索所有的表项,添加上hook
import aspects
allClasses = dir()
def registerHook():
global allClasses
for elem in allClasses:
if( globals().has_key(elem) ):
if( "<class 'django.db.models.base.ModelBase'>" == str(type(globals()[elem]))):
aspects.with_wrap(newSave, getattr(globals()[elem],"save"))
aspects.with_wrap(newDelete, getattr(globals()[elem],"delete"))
(当然你也可以手动定义好,给固定的类加上cache )
9,加起来不到100行代码,你已经给你所有的数据库表项添加上了缓存。现在你可以去敲你老板的门了:我刚给数据库讲了一个笑话,它现在冷静了很多