缓存安全以及优化
缓存安全以及优化
商城项目中的一段代码(V1):
public SkuInfo getSkuInfoById(Long skuId) {
SkuInfo skuInfo = null;
// 查询缓存
skuInfo = (SkuInfo) redisTemplate.opsForValue().get("sku:" + skuId + ":info");
if (skuInfo == null) {
// 查询数据库
skuInfo = getSkuInfoFromDb(skuId);
// 同步缓存
redisTemplate.opsForValue().set("sku:" + skuId + ":info",skuInfo);
}
return skuInfo;
}
以上代码有许多缓存漏洞,如:
A 缓存穿透
通过缓存中不存在的key,绕过缓存直接攻击db
B 缓存击穿
缓存中的一个热点key,在高并发时失效,导致一瞬间高并发打入db的过程
C 缓存雪崩
缓存中的一批热点key,在高并发时同时失效,导致一瞬间高并发打入db的过程
如何防止因为缓存的漏洞导致的db崩溃
两种分布式锁:
A 基于redis的原始功能 set nx 的分布式锁
B 基于框架分布式锁,如redisson
压力测试工具Appache24:
启动时端口冲突443,查找并删除相关进程
netstat -ano|findstr 443
测试命令:
ab -c 并发数 -n 总请求数 url地址
缓存的分布式锁优化
key的设计,决定着缓存的可读性
缓存key的设计规则:
Object:id:field
eg:
User:1:age
User:1:password
User:1:username
Sku:22:info
上锁类型:
sku:Lock 全部商品上锁
sku:category3Id:lock 同一分类的商品上锁
sku:id:lock 单个商品上锁
sku:id:userId:lock 同一个账号的单个商品上锁
通常情况下单个商品上锁即可
V2版本:
public SkuInfo getSkuInfoById(Long skuId) {
SkuInfo skuInfo = null;
// 查询缓存
skuInfo = (SkuInfo) redisTemplate.opsForValue().get("sku:" + skuId + ":info");
if (skuInfo == null) {
Boolean OK = redisTemplate.opsForValue().setIfAbsent("sku:" + skuId + ":lock", 1, 3, TimeUnit.SECONDS);
if (OK) {// OK为true,说明成功拿到了锁
// 查询数据库
skuInfo = getSkuInfoFromDb(skuId);
if (skuInfo != null) {
// 同步缓存
redisTemplate.opsForValue().set("sku:" + skuId + ":info", skuInfo);
}
// 释放锁
redisTemplate.delete("sku:" + skuId + ":lock");
} else {
//没有查询到缓存数据,并且没有获得分布式锁,自旋
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此处务必加入return,否则回产生两个线程
return getSkuInfoById(skuId);
}
}
return skuInfo;
}
以上代码依然有逻辑漏洞
假如有两个线程A、B依次访问(A先行),A执行到15行之前,A的锁过期,此时B会拿到锁进入7行之后,A便会把B的锁删除,以此类推,之后的CDEFG等等线程进入后,前一个线程便会删除后一个线程的锁
V3版本:
public SkuInfo getSkuInfoById(Long skuId) {
SkuInfo skuInfo = null;
// 查询缓存
skuInfo = (SkuInfo) redisTemplate.opsForValue().get("sku:" + skuId + ":info");
if (skuInfo == null) {
String lockTag = UUID.randomUUID().toString();
Boolean OK = redisTemplate.opsForValue().setIfAbsent("sku:" + skuId + ":lock", lockTag, 3, TimeUnit.SECONDS);
if (OK) {// OK为true,说明成功拿到了锁
// 查询数据库
skuInfo = getSkuInfoFromDb(skuId);
if (skuInfo != null) {
// 同步缓存
redisTemplate.opsForValue().set("sku:" + skuId + ":info", skuInfo);
}
String delTag = (String) redisTemplate.opsForValue().get("sku:" + skuId + ":info");
if (delTag.equals(lockTag)) {
// 释放锁
redisTemplate.delete("sku:" + skuId + ":lock");
}
} else {
//没有查询到缓存数据,并且没有获得分布式锁,自旋
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此处务必加入return,否则回产生两个线程
return getSkuInfoById(skuId);
}
}
return skuInfo;
}
在释放锁的时候检测锁的值(16行)
为防止黑客恶意使用不存在的id查询数据库,这时可以在没有在db查询出数据的时候随意设置一个缓存值,并设置过期时间
此时依旧不是完美的——当程序运行到15行已经查询出锁的值时,锁刚好过期了,这时与V2版本的问题也是一样的
如果在判断时,或者del命令在网络传输过程中,锁过期,还会出现删除其他线程锁的情况
本质问题就是在查和删缓存的时候不统一,这时便要祭出lua大法了:
V4版本:
public SkuInfo getSkuInfoById(Long skuId) {
SkuInfo skuInfo = null;
// 查询缓存
skuInfo = (SkuInfo) redisTemplate.opsForValue().get("sku:" + skuId + ":info");
if (skuInfo == null) {
String lockTag = UUID.randomUUID().toString();
Boolean OK = redisTemplate.opsForValue().setIfAbsent("sku:" + skuId + ":lock", lockTag, 3, TimeUnit.SECONDS);
if (OK) {// OK为true,说明成功拿到了锁
// 查询数据库
skuInfo = getSkuInfoFromDb(skuId);
if (skuInfo != null) {
// 同步缓存
redisTemplate.opsForValue().set("sku:" + skuId + ":info", skuInfo);
} else {
redisTemplate.opsForValue().set("sku:" + skuId + ":info", null, 5, TimeUnit.MINUTES);
}
// String delTag = (String) redisTemplate.opsForValue().get("sku:" + skuId + ":info");
// if (delTag.equals(lockTag)) {
// // 释放锁
// redisTemplate.delete("sku:" + skuId + ":lock");
// }
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";// 脚本查询是否存在,存在则删除,否则返回0
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("sku:" + skuId + ":lock"), lockTag);// 执行
} else {
//没有查询到缓存数据,并且没有获得分布式锁,自旋
try {
Thread.sleep(3000);// 假设让用户等待一会儿
} catch (InterruptedException e) {
e.printStackTrace();
}
//此处务必加入return,否则回产生两个线程
return getSkuInfoById(skuId);
}
}
return skuInfo;
}
这时看似代码很完美,其实还可以用AOP。。。
(未完待续~)