项目运行过程中往往为了提升项目对数据加载效率,一般都会增加缓存,但缓存如何加载效率最高?如何加载对后端服务造成的压力最小?我们需要设计一套完善的缓存架构体系。

1 多级缓存架构分析

redis如何设计多级key redis多级缓存_缓存

用户请求到达后端服务,先经过代理层nginx,nginx将请求路由到后端tomcat服务,tomcat去数据库中取数据,这是一个非常普通的流程,但在大并发场景下,需要做优化,而缓存是最有效的手段之一。缓存优化有,执行过程如下:

1:请求到达Nginx,Nginx抗压能力极强。
2:Tomcat抗压能力很弱,如果直接将所有请求路由给Tomcat,Tomcat压力会非常大,很有可能宕机。我们可以在Nginx这里设置2道缓存,第1道是Redis缓存,第2道是Nginx缓存。
3:先加载Redis缓存,如果Redis没有缓存,则加载Nginx缓存,Nginx如果没有缓存,则将请求路由到Tomcat。
4:Tomcat发布的程序会加载数据,加载完成后需要做缓存的,及时将数据存入Redis缓存,再响应数据给用户。
5:用户下次查询的时候,查询Redis缓存或Nginx缓存。
6:后面用户请求的时候,就可以直接从Nginx缓存拿数据了,这样就可以实现后端Tomcat发布的服务被调用的次数大幅减少,负载大幅下降。

上面这套缓存架构被多个大厂应用,除了可以有效提高加载速度、降低后端服务负载之外,还可以防止缓存雪崩(如果redis缓存失效,可以查询Nginx缓存),为服务稳定健康打下了坚实的基础,这也就是鼎鼎有名的多级缓存架构体系。

2 推广商品高效加载

首页很多商品优先推荐展示,这些其实都是推广商品,并非真正意义上的热门商品,首页展示这些商品数据需要加载效率极高,并且商城首页访问频率也是极高,我们需要对首页数据做缓存处理,我们首先想到的就是Redis缓存。

redis如何设计多级key redis多级缓存_Nginx_02

2.1 表机构分析

推广商品并非只在首页出现,有可能在列表页、分类搜索页多个地方出现,因此可以设计一张表用于存放不同位置展示不同商品的表,推广产品推荐表如下:

CREATE TABLE `ad_items` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `type` int(3) DEFAULT NULL COMMENT '分类,1首页推广,2列表页推广',
  `sku_id` varchar(60) DEFAULT NULL COMMENT '展示的产品(对应Sku)',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 推广商品异步加载

1)Bean创建

goods-api中创建com.gupaoedu.vip.mall.goods.model.AdItems

@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "ad_items")
public class AdItems {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer type;
    private String skuId;
    private Integer sort;
}

2)Mapper

创建com.gupaoedu.vip.mall.goods.mapper.AdItemsMapper

public interface AdItemsMapper extends BaseMapper<AdItems> {
}

3)Service

接口:修改com.gupaoedu.vip.mall.goods.service.SkuService,增加如下方法:

public interface SkuService extends IService<Sku> {

    /***
     * 根据推广产品分类ID查询Sku列表
     * @param id
     * @return
     */
    List<Sku> typeSkuItems(Integer id);
}

实现类:修改com.gupaoedu.vip.mall.goods.service.impl.SkuServiceImpl增加实现方法:

@Service
public class SkuServiceImpl extends ServiceImpl<SkuMapper,Sku> implements SkuService {

    @Autowired
    private AdItemsMapper adItemsMapper;

    @Autowired
    private SkuMapper skuMapper;

    /***
     * 根据推广产品分类ID查询Sku列表
     * @param id
     * @return
     */
    @Override
    public List<Sku> typeSkuItems(Integer id) {
        //查询所有分类下的推广
        QueryWrapper<AdItems> adItemsQueryWrapper=new QueryWrapper<AdItems>();
        adItemsQueryWrapper.eq("type",id);
        List<AdItems> adItems = adItemsMapper.selectList(adItemsQueryWrapper);

        //获取所有SkuId
        List<String> skuIds = adItems.stream().map(adItem -> adItem.getSkuId()).collect(Collectors.toList());
        //批量查询Sku
        List<Sku> skus = skuMapper.selectBatchIds(skuIds);
        return skus;
    }
}

4)Controller

修改com.gupaoedu.vip.mall.goods.controller.SkuController增加方法:

@RestController
@RequestMapping(value = "/sku")
@CrossOrigin
public class SkuController {

    @Autowired
    private SkuService skuService;

    /****
     * 指定分类下的推广产品列表
     */
    @GetMapping(value = "/aditems/type/{id}")
    public List<Sku> typeItems(@PathVariable(value = "id")Integer id){
        //查询
        List<Sku> adSkuItems = skuService.typeSkuItems(id);
        return adSkuItems;
    }
}

用Postman测试http://localhost:8081/sku/aditems/type/1效果如下:

redis如何设计多级key redis多级缓存_缓存_03

2.3 缓存常用注解

@EnableCaching:

开关性注解,在项目启动类或某个配置类上使用此注解后,则表示允许使用注解的方式进行缓存操作。

@Cacheable:

可用于类或方法上;在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值。不再执行目标方法;无则执行目标方法,并将方法的返回值作为value,并以键值对的形式存入缓存。

@CacheEvict:

可用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话)。

@CachePut:

可用于类或方法上;在执行完目标方法后,并将方法的返回值作为value,并以键值对的形式存入缓存中。

@Caching:

此注解即可作为@Cacheable、@CacheEvict、@CachePut三种注解中的的任何一种或几种来使用。

@CacheConfig:

可以用于配置@Cacheable、@CacheEvict、@CachePut这三个注解的一些公共属性,例如cacheNames、keyGenerator。
2.4 推广产品缓存操作

1)配置缓存链接

修改bootstrap.yml,增加配置Redis缓存链接,如下:

#Redis配置
  redis:
    host: 192.168.100.130
    port: 6379

2)开启缓存

com.gupaoedu.vip.mall.MallGoodsServiceApplication上添加缓存开启注解:

redis如何设计多级key redis多级缓存_缓存_04

2.4.1 推广产品缓存加载

com.gupaoedu.vip.mall.goods.service.impl.SkuServiceImpl添加@Cacheable注解,代码如下:

redis如何设计多级key redis多级缓存_Nginx_05

完整代码如下:

/***
 * 根据推广产品分类ID查询Sku列表
 * cacheNames = "ad-items-skus":命名空间
 * key ="#id":入参id作为缓存的key,使用的是SpEL表达式
 */
@Cacheable(cacheNames = "ad-items-skus",key ="#id")
@Override
public List<Sku> typeSkuItems(Integer id) {
    //查询所有分类下的推广
    QueryWrapper<AdItems> adItemsQueryWrapper=new QueryWrapper<AdItems>();
    adItemsQueryWrapper.eq("type",id);
    List<AdItems> adItems = adItemsMapper.selectList(adItemsQueryWrapper);

    //获取所有SkuId
    List<String> skuIds = adItems.stream().map(adItem -> adItem.getSkuId()).collect(Collectors.toList());
    //批量查询Sku
    List<Sku> skus = skuMapper.selectBatchIds(skuIds);
    return skus;
}

请求http://localhost:8081/sku/aditems/type/1此时Redis缓存数据如下:

redis如何设计多级key redis多级缓存_Nginx_06

我们可以发现上面存储的数据是二进制数据,我们很难阅读,而且占空间极大,我们可以使用FastJSON将每次存入到Redis中的数据转成JSON字符串,此时我们需要把参考资料中的RedisConfig.java拷贝到工程中,其他工程也有可能需要,我们可以拷贝到mall-service-dependency工程的com.gupaoedu.vip.mall.config包下。

此时清理再执行加载缓存后,效果如下:

redis如何设计多级key redis多级缓存_Nginx_07

2.4.2 推广产品缓存清理

1)Service

接口:添加清理缓存方法com.gupaoedu.vip.mall.goods.service.SkuService#delTypeSkuItems

/***
 * 清理分类ID下的推广产品
 * @param id
 */
void delTypeSkuItems(Integer id);

实现类:添加实现方法com.gupaoedu.vip.mall.goods.service.impl.SkuServiceImpl#delTypeSkuItems

/****
 * 清理缓存
 * @param id
 */
@CacheEvict(cacheNames = "ad-items-skus",key ="#id")
@Override
public void delTypeSkuItems(Integer id) {
}

2)Controller

添加删除缓存方法com.gupaoedu.vip.mall.goods.controller.SkuController#deleteTypeItems

/****
 * 删除指定分类下的推广产品列表
 */
@DeleteMapping(value = "/aditems/type")
public RespResult deleteTypeItems(@RequestParam(value = "id") Integer id){
    //清理缓存
    skuService.delTypeSkuItems(id);
    return RespResult.ok();
}
2.4.3 注解缓存操作优化

使用@CacheConfig优化注解,可以将cacheNames挪到类上,每个方法上就不用重复写cacheNames了。

redis如何设计多级key redis多级缓存_redis如何设计多级key_08

其他地方肯定会调用这几个方法用于实现缓存更新,我们可以在goods-api中添加feigin接口。

mall-api中引入common工具包和feign依赖包:

<!--工具包-->
<dependency>
    <groupId>com.gupaoedu.vip.mall</groupId>
    <artifactId>mall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

goods-api中创建com.gupaoedu.vip.mall.goods.feign.SkuFeign,代码如下:

@FeignClient(value = "mall-goods")
public interface SkuFeign {

    /****
     * 指定分类下的推广产品列表
     */
    @GetMapping(value = "/sku/aditems/type")
    public List<Sku> typeItems(@RequestParam(value = "id") Integer id);

    /****
     * 删除指定分类下的推广产品列表
     */
    @DeleteMapping(value = "/sku/aditems/type/{id}")
    public RespResult deleteTypeItems(@PathVariable(value = "id")Integer id);

    /****
     * 修改指定分类下的推广产品列表
     */
    @PutMapping(value = "/sku/aditems/type/{id}")
    public RespResult updateTypeItems(@PathVariable(value = "id")Integer id);
}

3 多级缓存-Lua+Redis

redis如何设计多级key redis多级缓存_redis如何设计多级key_09

按照上面分析的架构,可以每次在Nginx的时候使用Lua脚本查询Redis,如果Redis有数据,则将数据存入到Nginx缓存,再将数据响应给用户,此时我们需要实现使用Lua将数据从Redis中加载出来。

我们在/usr/local/openresty/nginx/lua中创建文件aditem.lua,脚本如下:

--数据响应类型JSON
ngx.header.content_type="application/json;charset=utf8"
--Redis库依赖
local redis = require("resty.redis");
local cjson = require("cjson");

--获取id参数(type)
local id = ngx.req.get_uri_args()["id"];
--key组装
local key = "ad-items-skus::"..id
--创建链接对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--设置服务器链接信息
red:connect("192.168.100.130", 6379)
--查询指定key的数据
local result=red:get(key);

--关闭Redis链接
red:close()

if result==nil or result==null or result==ngx.null then
	return true
else
	--输出数据
	ngx.say(result)
end

修改nginx.conf添加如下配置:(最后记得将content_by_lua_file改成rewrite_by_lua_file)

#推广产品查询
location /sku/aditems/type {
    content_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua;
}

访问http://www.gpshopvip.com/sku/aditems/type?id=1效果如下:

redis如何设计多级key redis多级缓存_缓存_10

4 Nginx代理缓存

redis如何设计多级key redis多级缓存_redis如何设计多级key_11

proxy_cache 是用于 proxy 模式的缓存功能,proxy_cache 在 Nginx 配置的 http 段、server 段中分别写入不同的配置。http 段中的配置用于定义 proxy_cache 空间,server 段中的配置用于调用 http 段中的定义,启用对server 的缓存功能。

使用:

1、定义缓存空间
2、在指定地方使用定义的缓存

4.1 Nginx代理缓存学习

1)开启Proxy_Cache缓存:

我们需要在nginx.conf中配置才能开启缓存:

proxy_cache_path /usr/local/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;

参数说明:

【proxy_cache_path】指定缓存存储的路径,缓存存储在/usr/local/openresty/nginx/cache目录

【levels=1:2】设置一个两级目录层次结构存储缓存,在单个目录中包含大量文件会降低文件访问速度,因此我们建议对大多数部署使用两级目录层次结构。如果 levels 未包含该参数,Nginx 会将所有文件放在同一目录中。

【keys_zone=proxy_cache:10m】设置共享内存区域,用于存储缓存键和元数据,例如使用计时器。拥有内存中的密钥副本,Nginx 可以快速确定请求是否是一个 HIT 或 MISS 不必转到磁盘,从而大大加快了检查速度。1 MB 区域可以存储大约 8,000 个密钥的数据,因此示例中配置的 10 MB 区域可以存储大约 80,000 个密钥的数据。

【max_size=1g】设置缓存大小的上限。它是可选的; 不指定值允许缓存增长以使用所有可用磁盘空间。当缓存大小达到限制时,一个称为缓存管理器的进程将删除最近最少使用的缓存,将大小恢复到限制之下的文件。

【inactive=60m】指定项目在未被访问的情况下可以保留在缓存中的时间长度。在此示例中,缓存管理器进程会自动从缓存中删除 60 分钟未请求的文件,无论其是否已过期。默认值为 10 分钟(10m)。非活动内容与过期内容不同。Nginx 不会自动删除缓存 header 定义为已过期内容(例如 Cache-Control:max-age=120)。过期(陈旧)内容仅在指定时间内未被访问时被删除。访问过期内容时,Nginx 会从原始服务器刷新它并重置 inactive 计时器。

【use_temp_path=off】表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了避免在更新缓存时,磁盘之间互相复制响应数据,我们一般关闭该功能。

2)Proxy_Cache属性:

proxy_cache:设置是否开启对后端响应的缓存,如果开启的话,参数值就是zone的名称,比如:proxy_cache。

proxy_cache_valid:针对不同的response code设定不同的缓存时间,如果不设置code,默认为200,301,302,也可以用any指定所有code。

proxy_cache_min_uses:指定在多少次请求之后才缓存响应内容,这里表示将缓存内容写入到磁盘。

proxy_cache_lock:默认不开启,开启的话则每次只能有一个请求更新相同的缓存,其他请求要么等待缓存有数据要么限时等待锁释放;nginx 1.1.12才开始有。
配套着proxy_cache_lock_timeout一起使用。

proxy_cache_key:缓存文件的唯一key,可以根据它实现对缓存文件的清理操作。

4.2 Nginx代理缓存热点数据应用

redis如何设计多级key redis多级缓存_数据_12

1)开启代理缓存

修改nginx.conf,添加如下配置:

proxy_cache_path /usr/local/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;

修改nginx.conf,添加如下配置:

#门户发布
server {
    listen       80;
    server_name  www.gpshopvip.com;

    #推广产品查询
    location /sku/aditems/type {
        #先找Nginx缓存
        rewrite_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua;
        #启用缓存openresty_cache
        proxy_cache proxy_cache;
        #针对指定请求缓存
        #proxy_cache_methods GET;
        #设置指定请求会缓存
        proxy_cache_valid 200 304 60s;
        #最少请求1次才会缓存
        proxy_cache_min_uses 1;
        #如果并发请求,只有第1个请求会去服务器获取数据
        #proxy_cache_lock on;
        #唯一的key
        proxy_cache_key $host$uri$is_args$args;
        #动态代理
        proxy_pass http://192.168.100.1:8081;
    }


    #其他所有请求
    location / {
        root   /usr/local/gupao/web/static/frant;
    }
}

重启nginx或者重新加载配置文件nginx -s reload,再次测试,可以发现下面个规律:

1:先查找Redis缓存
2:Redis缓存没数据,直接找Nginx缓存
3:Nginx缓存没数据,则找真实服务器

我们还可以发现cache目录下多了目录和一个文件,这就是Nginx缓存:

redis如何设计多级key redis多级缓存_数据_13

4.3 Cache_Purge代理缓存清理

很多时候我们如果不想等待缓存的过期,想要主动清除缓存,可以采用第三方的缓存清除模块清除缓存 nginx_ngx_cache_purge。安装nginx的时候,需要添加purge模块,purge模块我们已经下载了,在/usr/local/gupao目录下,添加该模块--add-module=/usr/local/gupao/ngx_cache_purge-2.3/,这一个步骤我们在安装OpenRestry的时候已经实现了。

安装好了后,我们配置一个清理缓存的地址:http://192.168.100.130/purge/sku/aditems/type?id=1

#清理缓存
location ~ /purge(/.*) {
    #清理缓存
    proxy_cache_purge proxy_cache $host$1$is_args$args;
}

此时访问http://www.gpshopvip.com/purge/sku/aditems/type?id=1,表示清除缓存,如果出现如下效果表示清理成功:

redis如何设计多级key redis多级缓存_Nginx_14