系统现状:
由于系统目前提醒是在公共模块,对于提醒的数目实时计算。对于db的压力较大。因此采用J2cache的两级缓存。关于两级缓存的说明
但是当部分业务更新时需要将缓存删除,导致系统在某个时间点出现db负载过大。
对应业务系统出现大量慢sql以及由于超时无法完成的请求
出现了系统雪崩的场景。
两级缓存实现分析之缓存设置 ===》当大量请求提醒的业务sql过来导致系统无法及时的响应。而提醒sql又出现超时是的无法真正的缓存下来。使的sql越积越多,db负载过高。直到有成功将缓存写入或者db被击穿。
对策:
- 改写sql,尽量提高sql效率
- 在缓存处加分布式锁,避免同时回源
第一个策略对于系统并没有得到很好的缓解。主要是由于该发生提醒缓存失效使用的用户较多,用户频繁请求数据,导致结果无法在有效的h时间内缓存,出现大量慢sql请求直至系统超时。
那么我们要做的就是在缓存上增加blockingcache的功能。
那么何为blockingCache呢?需要哪些功能呢?
- 对于系统同一个缓存在多个请求过来时能hold住其他请求,单个请求执行业务
- 防止出现死锁,在系统挂掉的情况下锁能够自动释放
- 可定义的超时时长
- 其他请求在缓存中设置过值之后自动苏醒,返回缓存结果
- 支持可重入
- 其他配置需求等等
由上述可知,我们在缓存get时需要处理的事情如下:
- 先取缓存,如果有结果直接返回
- 如果没有缓存,直接加锁
- 如果加锁成功直接返回null ===》需要去执行业务逻辑
- 如果加锁不成功或者不可重入 则线程需要等待
- 线程每隔指定时间sleep结束(默认为300ms)最多等待60s,每次需要检查是否缓存中结果如果有结果直接返回
- 还需要检查是否可以加锁成功(如果加锁成功返回null)
查看原先的get的代码如下
public Object get(Object key) throws CacheException { if (null == key) return null; Object obj = null; try { byte[] b = redisCacheProxy.hget(region2, getKeyName(key)); if (b != null) obj = SerializationUtils.deserialize(b); } catch (Exception e) { log.error("Error occured when get data from redis2 cache", e); if (e instanceof IOException || e instanceof NullPointerException) evict(key); } return obj; }
改造后代码如下
public Object get(Object key) throws CacheException { if (null == key) return null; Object obj = null; try { byte[] keyName = getKeyName(key); byte[] b = redisCacheProxy.hget(region2, keyName); if (b != null) { obj = SerializationUtils.deserialize(b); } else if (redisCacheProxy.isBlock()) { byte[] lockKey = getLockKey(key); boolean locked = getLock(lockKey); if (locked || canReentrant(key)) { return null; } else { int timeLeft = redisCacheProxy.getTimeOutMillis(); while (timeLeft > 0) { Thread.sleep(redisCacheProxy.getTimeWaitMillis()); timeLeft -= redisCacheProxy.getTimeWaitMillis(); b = redisCacheProxy.hget(region2, keyName); if (b != null) { obj = SerializationUtils.deserialize(b); break; } else { //如果拿不到再尝试一次获取lock,防止出现部分情况一直没有put导致等待时间过长。后续要改造成可重入 if (getLock(lockKey)) { return null; } } //超时是应该抛异常呢还是直接返回null? 目前返回null } } } } catch (Exception e) { log.error("Error occured when get data from redis2 cache", e); if (e instanceof IOException || e instanceof NullPointerException) evict(key); } return obj; } private boolean canReentrant(Object key) { //对于缓存来说要求不精确,使用线程id即可 try { String value = redisCacheProxy.get(getLockKeyString(key)); if (value != null) { long oriThreadId = Long.parseLong(value); return oriThreadId == Thread.currentThread().getId(); } } catch (Exception ex) { log.error(ex.getMessage(), ex); } return false; } private boolean getLock(byte[] lockKey) { return getLock(lockKey, String.valueOf(Thread.currentThread().getId()).getBytes()); } private boolean getLock(byte[] lockKey, byte[] keyName) { return "OK".equals(redisCacheProxy.set(lockKey, keyName, NX, PX, redisCacheProxy.getTimeLockMillis())); } private void releaseLock(byte[] lockKey) { redisCacheProxy.del(lockKey); } private String getLockKeyString(Object key) { return String.format(lockPattern, region, key.hashCode() % redisCacheProxy.getStripes()); } private byte[] getLockKey(Object key) { String keyName = getLockKeyString(key); return keyName.getBytes(); }
可以参考
https://git.oschina.net/ld/J2Cache/pulls/48
https://git.oschina.net/ld/J2Cache/pulls/47
http://git.oschina.net/ld/J2Cache/commit/928bde1a60b9884f5dff95257a954c61aa2bb367