项目运行过程中往往为了提升项目对数据加载效率,一般都会增加缓存,但缓存如何加载效率最高?如何加载对后端服务造成的压力最小?我们需要设计一套完善的缓存架构体系。
1 多级缓存架构分析
用户请求到达后端服务,先经过代理层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缓存。
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
效果如下:
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
上添加缓存开启注解:
2.4.1 推广产品缓存加载
在com.gupaoedu.vip.mall.goods.service.impl.SkuServiceImpl
添加@Cacheable
注解,代码如下:
完整代码如下:
/***
* 根据推广产品分类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缓存数据如下:
我们可以发现上面存储的数据是二进制数据,我们很难阅读,而且占空间极大,我们可以使用FastJSON将每次存入到Redis中的数据转成JSON字符串,此时我们需要把参考资料
中的RedisConfig.java
拷贝到工程中,其他工程也有可能需要,我们可以拷贝到mall-service-dependency
工程的com.gupaoedu.vip.mall.config
包下。
此时清理再执行加载缓存后,效果如下:
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
了。
其他地方肯定会调用这几个方法用于实现缓存更新,我们可以在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
按照上面分析的架构,可以每次在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
效果如下:
4 Nginx代理缓存
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代理缓存热点数据应用
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缓存:
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,表示清除缓存,如果出现如下效果表示清理成功: