其实在大部分场景中都需要使用缓存,比如购物网站上商品详情页的信息。如果不使用缓存,用户每发起一起请求,都需要到服务端数据库查询,数据库的每一次操作都会涉及到磁盘IO,这样不仅增加服务端的压力,同时也降得了响应速度。而缓存存在于内存中,对缓存的操作比对磁盘的操作要快得多。但也不是什么数据都可以存入缓存中。首先内存大小有限,不可能将所有数据都存入内存中。其次对于那些更新比较频繁的数据,如果存入缓存中,就需要经常更新缓存,这有可能得不偿失。
所以缓存中存储的数据应该是一些比较热点(即经常访问)同时又不经常修改的数据。因为缓存的设计是为了提高数据处理速度,提高响应速度,所以也应该将缓存推到离用户最近的地方。
在使用缓存之前整个系统的结构如下图所示:
服务器端使用的是SpringBoot2.0.5版本。
这里以查询商品详情信息为例。服务器端关于查询商品详情的部分代码如下:
/**
* @author: Charviki
* @create: 2019-09-03 14:47
**/
@RestController
@RequestMapping("/item")
public class ItemController{
@Autowired
private ItemService itemService;
@GetMapping(value = "/getItemById")
public ItemModel getItemById(@RequestParam("id") Integer itemId){
// 直接从数据库查询
ItemModel itemModel = itemService.getItemById(itemId);
return itemModel;
}
}
首先,我们可以在首次向查询商品数据之后,将数据存入redis中,下一次再查询商品数据时先从Redis缓存中取,如果缓存中没有,再从数据库中查询。修改controller,代码如下:
/**
* @author: Charviki
* @create: 2019-09-03 14:47
**/
@RestController
@RequestMapping("/item")
public class ItemController{
@Autowired
private ItemService itemService;
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/getItemById")
public ItemModel getItemById(@RequestParam("id") Integer itemId){
// 生成key,item+id
String key = "item_" + itemId;
// 先到redis查询
ItemModel itemModel = (ItemModel) redisTemplate.opsForValue().get(key);
// 判断redis中是否存在相应的商品信息
if (itemModel == null){
// 不存在,查询数据库
itemModel = itemService.getItemById(itemId);
// 将商品模型数据存入redis,并设置过期时间
redisTemplate.opsForValue().set(key,itemModel,10, TimeUnit.MINUTES);
}
return itemModel;
}
}
我们还可以通过将缓存推向tomcat的位置,减少每次都需要访问redis的网络开销。缓存放到tomcat的位置,即在程序中将一些热点数据缓存起来,放入jvm堆中,实现了本地缓存。之后用户每次获取数据时先在本地中查询,如果本地没有,再向下一级缓存redis中查询。
引入依赖:
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>18.0version>
dependency>
定义本地缓存操作接口:
**
* 本地缓存操作类
* @author: Charviki
* @create: 2019-09-22 9:56
**/
public interface CacheService {
/**
* 存入缓存
* @param key
* @param value
*/
void setCommonCache(String key,Object value);
/**
* 根据key取出缓存
* @param key
* @return
*/
Object getCommonCache(String key);
}
实现该接口:
/**
* @author: Charviki
* @create: 2019-09-22 9:57
**/
@Service
public class CacheServiceImpl implements CacheService {
private Cache<String,Object> commonCache = null;
@PostConstruct
public void init(){
commonCache = CacheBuilder.newBuilder()
// 设置缓存容器初始容量大小为10
.initialCapacity(10)
// 设置缓存容器最多存储100个key,超过100个之后会根据LRU的策略移除缓存项
.maximumSize(100)
// 设置缓冲过期时间
.expireAfterWrite(60, TimeUnit.SECONDS).build();
}
@Override
public void setCommonCache(String key, Object value) {
commonCache.put(key,value);
}
@Override
public Object getCommonCache(String key) {
return commonCache.getIfPresent(key);
}
}
修改controller代码:
/**
* @author: Charviki
* @create: 2019-09-03 14:47
**/
@RestController
@RequestMapping("/item")
public class ItemController{
@Autowired
private ItemService itemService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private CacheService cacheService;
@GetMapping(value = "/getItemById")
public ItemModel getItemById(@RequestParam("id") Integer itemId){
// 生成key,item+id
String key = "item_" + itemId;
// 先根据key到本地缓存查找
ItemModel itemModel = (ItemModel) cacheService.getCommonCache(key);
// 判断本地缓存是否存在该商品信息
if (itemModel == null){
// 不存在,根据key到redis查询
itemModel = (ItemModel) redisTemplate.opsForValue().get(key);
// 判断redis中是否存在相应的商品信息
if (itemModel == null){
// 不存在,查询数据库
itemModel = itemService.getItemById(itemId);
// 将商品模型数据存入redis
redisTemplate.opsForValue().set(key,itemModel,10, TimeUnit.MINUTES);
}
// 将商品模型数据存入本地缓存
cacheService.setCommonCache(key,itemModel);
}
return itemModel;
}
}
我们再把缓存往上推,推到nginx的位置。我们能否在nginx上实现对商品的缓存呢?答案是肯定的。nginx自身有一套缓存机制,该缓存机制是将数据中存放在磁盘中,将数据的key(数据在磁盘中的地址)存放到内存中。
实现nginx自身的缓存机制,需要对nginx的配置文件nginx.conf作如下配置:
# 这里忽略了无关的配置,只需要在相应的位置下增加配置即可
http{
# 申明一个cache缓存节点的内容
# levels=1:2 缓存文件可存储到二级目录
# keys_zone=tmp_cache:100m 内存中用于存储该keys_zone的内存大小
# inactive=7d 缓存过期时间
# max_size=10g 该文件系统最大存储的文件总量,超过后会采取LRU策略清除
# 新增以下配置
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;
server {
# 这里使用监听根路径访问为例
location / {
# 新增以下配置
# 缓存空间名
proxy_cache tmp_cache;
# 将uri作为缓存的key
proxy_cache_key $uri;
# 返回状态码为以下状态码时对数据进行缓存 最后一个7d指缓存过期时间
proxy_cache_valid 200 300 302 304 7d;
}
}
}
nginx自身的缓存机制虽然将key值放入内存中,提供了查找速度,但是实际数据存放在磁盘中,在获取数据时会涉及磁盘IO,使用该缓存机制甚至无法提供响应速度,不推荐使用。
OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。我们可以在nginx上使用lua脚本实现数据的缓存。
修改nginx配置文件,删除上面的配置,新增以下配置:
lua_shared_dict my_cache 128m;
在openresty目录下新建lua目录,在lua目录下新增itemsharedic.lua脚本:
-- 从lua缓存中获取
function get_from_cache(key)
local cache_ngx = ngx.shared.my_cache
local value = cache_ngx:get(key)
return value
end
-- 将数据存入lua缓存
function set_to_cache(key,value,exptime)
if not expitime then
expitime = 0
end
local cache_ngx = ngx.shared.my_cache
local succ,err,forcible = cache_ngx:set(key,value,exptime)
return succ
end
local args = ngx.req.get_uri_args()
local id = args["id"]
local item_model = get_from_cache("item_"..id)
if item_model == nil then
local resp = ngx.location.capture("/item/getItemById?id="..id)
item_model = resp.body
set_to_cache("item_"..id,item_model,1*60)
end
ngx.say(item_model)
使用lua脚本实现的缓存数据都存储在内存中,操作速度比nginx自身的缓存机制要快得多。
使用lua脚本在nginx上实现了热点数据的缓存,这样虽然减轻了服务端的压力,但是却把压力都增加到了nginx上。同时,将数据缓存放到nginx的lua缓存中,一旦数据修改,对于lua缓存的更新是比较困难的。在openresty上同时集成了连接redis的插件,我们可以使用lua脚本,将数据获取直接转到redis中查询,当redis中获取不到时,再从tomcat中获取。这样缓存在redis中也方便管理。
修改nginx配置文件,删除上面的配置,新增以下配置:
# 这里的路径根据实际情况设置
location /luaitem/get {
# 返回的数据类型
default_type "application/json";
# lua脚本
content_by_lua_file ../lua/itemredis.lua;
}
在lua目录下新增itemredis.lua脚本:
local args = ngx.req.get_uri_args()
local id = args["id"]
local redis = require "resty.redis"
local cache = redis:new()
# 连接redis
local ok,err = cache:connect("172.18.88.10",6379)
# redis的第几个库
ok,err = cache:select(10)
local item_model = cache:get("item_"..id)
if item_model == ngx.null or item_model == nil then
local resp = ngx.location.capture("/item/getItemById?id="..id)
item_model = resp.body
end
ngx.say(item_model)
这里推荐使用的是lua+redis实现nginx端的缓存,还有java本地缓存和redis端缓存。实现后系统结构图如下: