系统现状:
由于系统目前提醒是在公共模块,对于提醒的数目实时计算。对于db的压力较大。因此采用J2cache的两级缓存。关于两级缓存的说明
但是当部分业务更新时需要将缓存删除,导致系统在某个时间点出现db负载过大。
135920_z4OL_871390.png对应业务系统出现大量慢sql以及由于超时无法完成的请求
135927_BvHp_871390.png出现了系统雪崩的场景。
两级缓存实现分析之缓存设置 ===》当大量请求提醒的业务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
网友评论