缓存安全以及优化

缓存安全以及优化

商城项目中的一段代码(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:

image-20210128104928738

启动时端口冲突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。。。

(未完待续~)

posted @ 2021-01-28 14:49  ilusymon  阅读(112)  评论(0编辑  收藏  举报