缓存知识

权限系统是管理类系统中必不可少的一个模块,一个好的缓存设计更是权限系统的重中之重,今天来聊下如何更好设计权限系统的缓存。
单节点缓存
权限校验属于使用频率超高的操作,如果每次都去请求db的话,不仅会给db带来压力,也会导致用户响应过慢,造成很不好的用户体验,因此把权限相关数据放到缓存中是很有必要的,伪代码如下:
private static final FUNCTION_CACHE_KEY = "function_cache_key";
public List<Function> loadFunctions() {
// 优先从缓存中取
List<Function> functions = cacheService.get(FUNCTION_CACHE_KEY);
if(functions != null){
return functions;
}
// 缓存中没有,从数据库中取,并放入缓存
functions = functionDao.loadFunctions();
cacheService.put(FUNCTION_CACHE_KEY, functions);
return functions;
}
推荐使用ehcache作为缓存组件,ehcache是一个纯Java的进程内缓存框架,支持数据持久化到磁盘,并且支持多种缓存策略,对于权限数据这种大数据量的缓存可以说是非常合适。
集群缓存
ehcache属于进程级缓存,对集群支持不是很友好,虽然可以通过一些方案实现分布式缓存,但总感觉没有直接用memcached或redis来的痛快,但直接用memcached或redis的话,会经过一次网络调用,而且对于权限缓存这样内存比较大的数据,性能没有ehcache这种进程级缓存好。那有没有一直方案可以兼顾ehcache的性能优势和redis的分布式优势呢?
可以通过ehcache和redis共用的方式来解决这个问题,大致思路是用ehcache做主缓存,缓存更新通过MQ在集群间进行通信,而redis做为二级缓存使用。
具体方案如下:
更新数据
把数据同时放入ehcache和redis中,同时通过MQ通知其它节点更新自身的缓存,更新的数据从redis里面拉取
删除数据
删除ehcache和redis中数据,同时通过MQ通知其它节点删除自身的数据
其实对于权限缓存,一般情况下更新操作并不频繁,通过MQ做变更通知,redis做二级缓存,这样就可以在集群环境下仍旧使用ehcache的高效存储了
用时间戳保证级联缓存的一致性
在设计缓存的时候,并不是所有的缓存都是从数据库取的,有的缓存是从其它缓存从取的,这样可以减少使用时的计算时间
数据库 --> 缓存a --> 缓存b
有上面的依赖关系可以看出,缓存a发生变更时,缓存b如果不重新从缓存a中重新加载,就会造成缓存脏数据。
最直观的方案是刷新a缓存时,同步刷新b缓存,但从上述依赖关系可以看到,b依赖a,a并不依赖b,b缓存对于a应该是不可见的,所以从逻辑上来说不符合依赖的规则。
而且上面只是二级关联,如果是四级,五级的话,上层缓存的变更带动了太多下级缓存的变更,需要耗费很多时间,因此如果能用延迟刷新或许是更好的方案。
用时间戳或许是个不错的办法,上述例子中,可以给缓存a增加一个时间戳,每次a缓存变更,同步更新时间戳。获取b的时候只需要校验下a的时间戳是否变更,变更了就重新加载b缓存,否则直接返回b。
伪代码如下:
// 权限信息缓存key
private static final FUNCTION_CACHE_KEY = "function_cache_key";
// 权限信息缓存时间戳
private static final FUNCTION_TIME_STAMP = "function_time_stamp";
// 权限信息缓存旧的时间戳
private static final FUNCTION_OLD_TIME_STAMP = "function_old_time_stamp";
// 用户权限信息缓存key
private static final USER_FUNCTION_CACHE_KEY = "uer_function_cache_key";

// 加载所有的权限信息
public List<Function> loadFunctions() {
// 优先从缓存中取
List<Function> functions = cacheService.get(FUNCTION_CACHE_KEY);
if(functions != null){
return functions;
}
// 缓存中没有,从数据库中取,并放入缓存
functions = functionDao.loadFunctions();
cacheService.put(FUNCTION_CACHE_KEY, functions);
// 同步更新时间戳
String timeStamp = String.valueOf(System.currentTimeMillis());
cacheService.put(FUNCTION_TIME_STAMP, timeStamp);
return functions;
}

// 根据用户id加载用户的权限信息
public List<Function> loadUserFunctions(Long userId) {
List<Function> functions = loadFunctions();
// 加载缓存中用户权限信息
List<Function> userFunctions = cacheService.get(USER_FUNCTION_CACHE_KEY + userId);
String newTimeStamp= cacheService.get(FUNCTION_TIME_STAMP);
String oldTimeStamp= cacheService.get(FUNCTION_OLD_TIME_STAMP);
// 如果缓存中没有用户权限信息,或者时间戳不相等,重新从权限信息里面加载用户权限信息
if(userFunctions == null || newTimeStamp != oldTimeStamp){
userFunctions = getUserFunctions(functions, userId);
// 把用户权限信息放入缓存
cacheService.put(USER_FUNCTION_CACHE_KEY + userId, functions);
// 把当前时间戳放入缓存
cacheService.put(FUNCTION_OLD_TIME_STAMP, newTimeStamp);
return userFunctions;
}
return userFunctions;
}
需要说明的是,上述代码只是作为示例,真正开发时用户的权限信息一般有更好的处理方式,并不一定是上面示例中每个用户都单独放一份缓存。
因为上面缓存只是二级级联,如果级数更多,同样可以用时间戳来进行延迟加载
数据库 --> 缓存a --> 缓存b --> 缓存c --> 缓存d
获取缓存d时,可以校验 缓存a时间戳 + 缓存b时间戳 + 缓存c时间戳,abc任何一个时间戳发生变化,缓存d都需要重新加载,思路和上面的差不多,这里就不多赘述了。
guava 的妙用
对于权限校验中使用频率高,但校验逻辑又不常变化的地方可以再加一层缓存。
例如一般都权限系统都有对外的接口,可以直接匿名访问,校验代码如下
// ant风格 url 匹配器
private AntPathMatcher matcher = new AntPathMatcher();
// 可以访问的匿名url集合,通常采用ant风格,例如 /open/api/**
// 匿名url通常写在配置文件中,并且在bean初始化时加载到该集合中
private Set<String> anonymousUrlPatterns = new HashSet<String>();

// 判断url是否能匿名访问
public boolean couldAnonymous(String url) {
for (String patternUrl : anonymousUrlPatterns) {
if (matcher.match(patternUrl, url)) {
isMatch = true;
break;
}
}
return isMatch;
}
可以看到,每一次url访问都会校验,可以通过加一层缓存来优化性能
用分布式缓存感觉有点大材小用,ehcache又有点太重量级,ConcurrentHashMap又不支持缓存策略,思来想去guava貌似是最好的选择,改造完后的代码如下:
// ant风格 url 匹配器
private AntPathMatcher matcher = new AntPathMatcher();
// 可以访问的匿名url集合,通常采用ant风格,例如 /open/api/**
// 匿名url通常写在配置文件中,并且在bean初始化时加载到该集合中
private Set<String> anonymousUrlPatterns = new HashSet<String>();

// 匿名url访问权限缓存
private static Cache<String, Boolean> anonymousUrlCache = CacheBuilder.newBuilder()
.maximumSize(5000)
.initialCapacity(1000)
.expireAfterAccess(1, TimeUnit.DAYS) // 设置cache中的的对象多久没有被访问后过期
.build();

// 判断url 是否能匿名访问
public boolean couldAnonymous(String url) {
// 先从缓存中取,有的话直接返回
Boolean couldAnonymousAccess = anonymousUrlCache.getIfPresent(url);
if (couldAnonymousAccess != null) {
return couldAnonymousAccess;
}
boolean isMatch = false;
for (String patternUrl : anonymousUrlPatterns) {
if (matcher.match(patternUrl, url)) {
isMatch = true;
break;
}
}
// 匹配结果放入缓存
anonymousUrlCache.put(url, isMatch);
return isMatch;
}
localStorage 缓存
localStorage 是 HTML5支持的新特性,可以把一些数据缓存放在客户端,减轻服务器的压力,例如可以把菜单数据放到客户端,菜单数据是否过期通过时间戳来判断,伪代码如下:
var timestamp = localStorage.getItem("timestamp" + userId);
// 请求后台获取菜单接口,带上时间戳参数 timestamp
// 后台校验时间戳是否变更,如果变更,返回新的菜单数据和新的时间戳,否则不需要返回菜单数据,仍旧返回旧的时间戳即可
// 后台接口返回数据格式 result = {menus:{},timestamp:""}
var newTimestamp = result.timestamp;
// 时间戳变更,把新的菜单数据和新的时间戳 放入 localStorage
if (newTimestamp != timestamp) {
localStorage.setItem("menus" + userId, JSON.stringify(result.menus));
localStorage.setItem("timestamp" + userId, newTimestamp);
}
有人担心把缓存放在localStorage中如果被修改会造成安全问题,其实这个担心是没必要的,因为权限校验是在服务器端做的,localStorage中的缓存只做展示使用,因此修改localStorage时没有任何意义的。
总结
在不同的情况下,上述场景分别用了ehcache,redis,guava,localStorage做缓存,更加说明了没有最好的技术,只有最适合的技术。通过引入时间戳这种版本号的机制,解决了缓存更新问题。最终的目的只有一个,保证缓存数据一致性的同时,把性能做的极致,用户体验做到最好。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容