缓存设计
对于缓存设计,有几个原则:
用快速存取设备,例如内存
将缓存推到离用户最近的地方
脏缓存清理,也就是数据库变化后,缓存内的数据也要同步更新
多级缓存
redis缓存
对于redis,不同太多介绍了。这里介绍一下单机版和sentinal哨兵模式和集群cluster模式
对于哨兵模式,就是用一个sentinal节点管理redis,sentinal和主从redis有长连接,并发送心跳,sentinal可以决定master和slave,应用程序只需要询问sentinal就可以知道哪一台是master,然后连接相应的master即可。
cluster模式,就是多台redis的情况下,可以根据cluster来决定哪些是主分片,哪些是从分片,所有分片都有连接,应用程序只要连接到任意一台机器,就可以获取所有数据。
三种模式都被jedis集成了。
这里编码主要针对单机版,在日常应用的时候,我们往往会以uuid作为key,实体类作为value来存redis,对于默认存进去的数据格式,java会把实体类以java的方式序列化并存进去,这样redis里面显示的就是一堆乱码,因此我们可以扩展一下redis的序列化方式,让他以我们自己的方式存进去:
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//解决redis中key的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
//解决value的序列化方式
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
但是这样还有一个问题就是,DateTime格式没法序列化,因此再进行扩展一下:
/**
* joda的DateTime格式序列化
*/
public class DateTimeJsonSerializer extends JsonSerializer<DateTime> {
@Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.toString("yyyy-MM-dd HH:mm:ss"));
}
}
/**
* joda的DateTime格式反序列化
*/
public class DateTimeJsonDeserializer extends JsonDeserializer<DateTime> {
@Override
public DateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String dateString = p.readValueAs(String.class);
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
return DateTime.parse(dateString , formatter);
}
}
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//解决redis中key的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
//解决value的序列化方式
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(DateTime.class , new DateTimeJsonSerializer());
simpleModule.addDeserializer(DateTime.class , new DateTimeJsonDeserializer());
//这个的意思是在json前加实体类名以及序列化方式
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.registerModule(simpleModule);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
如此,再用RedisTemplate,redis的数据结构就很清晰明了了。
接下来就可以进行压测了。这里由于服务器带宽没有那么大,所以吞吐量上不去,就不测了。
本地热点缓存
由于使用redis,肯定会有网络io的消耗,所以在redis基础之上我们还需要增加本地热点缓存。
对于本地热点缓存,有几个特点:存热点数据,脏读非常不敏感,内存可控。因为本地缓存是跟jvm共用内存的,所以我们不可能把所有的数据都缓存到本地。
由于上面几个特性,也就决定了本地数据生命周期很短,这样才能控制脏读。
要是用本地缓存,首先认识一下Guava cache:他本质上是一个可并发的hashmap,可以控制keyvalue的大小和超时时间,可以配置lru策略(内存不足时,最近、最少访问的key优先被淘汰),线程安全。
接下来编码:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
/**
* 封装本地缓存操作类
*/
public interface CacheService {
//存方法
void setCommonCache(String key , Object value);
//取方法
Object getFromCommonCache(String key);
}
@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 getFromCommonCache(String key) {
return commonCache.getIfPresent(key);
}
}
实现:
//商品详情页浏览
@RequestMapping(value = "/get",method = {RequestMethod.GET})
@ResponseBody
public CommonReturnType getItem(@RequestParam(name = "id")Integer id){
ItemModel itemModel = null;
//先取本地缓存
itemModel = (ItemModel) cacheService.getFromCommonCache("item_" + id);
if (itemModel == null){
//根据商品的id到redis内获取
itemModel = (ItemModel) redisTemplate.opsForValue().get("item_"+id);
//若redis内不存在对应的itemModel,则访问下游service
if(itemModel == null){
itemModel = itemService.getItemById(id);
//设置itemModel到redis内
redisTemplate.opsForValue().set("item_"+id,itemModel);
redisTemplate.expire("item_"+id,10, TimeUnit.MINUTES);
}
//填充本地缓存
cacheService.setCommonCache("item_" + id , itemModel);
}
ItemVO itemVO = convertVOFromModel(itemModel);
return CommonReturnType.create(itemVO);
}
压测结果暂时不贴了,缺点也显而易见,当数据库改变时这里变不了,因此失效时间就非常重要。
nginx proxy cache缓存
对于本地热点缓存来说,虽然相对redis性能上好了很多,但是请求从nginx到服务还是有性能消耗,所以进一步优化,我们可以把热点缓存放在nginx上,不经过服务,那么性能就更好了。
当然nginx也是有要求的,nginx必须可以作为反向代理,才可以用proxy cache(废话)。实际上nginx是依靠文件系统存索引级的文件,也就是把一个请求当做一个文件存到本地的,当下一次请求进来的时候看本地有没有这样的文件来决定是否启用缓存。依靠内存缓存文件地址,也就是说虽然数据是以文件的形式存储,但是地址key是存在内存中的。
设置一下:
重启后压测可以看出,这样的性能并不好,这是因为数据虽然在nginx中,但是是以文件的形式存的,虽然没有访问远程连接,但是读取文件消耗的性能更大。所以一般不用这种方法,接下来:
nginx lua缓存
lua协程机制:这个在之前已经说过,好处就是在编写代码的时候不用考虑线程,以同步的方式编写。
nginx协程机制:使用lua作为插件模块完成并使用携程进行开发。
nginx lua插载点:nginx给我们留了很多lua插载点,可以在nginx不同的生命周期中通过lua实现功能。
OpenResty:将nginx nginxlua打包起来,并提供了nginx lua的例如redis等的库文件的封装。
关于lua编程:
function foo(a)
print("foo 函数输出" , a)
return coroutine.yield(2*a)
end
co = coroutine.create(function(a,b)
print("no.1",a,b)
local r = foo(a+1)
print("no.2",r)
local r,s=coroutine.yield(a+b,a-b)
print("no.3",r,s)
return b,"end"
end)
print("main",coroutine.resume(co,1,10))
print("===")
print("main",coroutine.resume(co,"r"))
print("===")
print("main",coroutine.resume(co,"x","y"))
print("===")
print("main",coroutine.resume(co,"x","y"))
print("===")
运行结果:
结合结果容易看出处理顺序了,这里不多赘述。
nginx协程:nginx的每一个Worker进程都是在epoll或者kqueue这种事件模型智商,封装成协程。之前说过nginx是运行在select epoll上的,当socket句柄接收到http请求的时候,epoll对应的socket就会被唤醒,然后获得一个http的request请求,这是nginx会new出一个协程,来处理http请求的完整生命周期。 在这个过程中一旦遇到block(例如协程要把stream反向代理给服务器,等待服务器做返回的操作,这就是block了),协程主动放弃自己的执行权限,并且跟后端反向代理的连接注册到epoll事件中,等待事件被唤醒,被唤醒后会new一个新协程做接下来的操作。看起来就像是几个协程串在一起共同完成一个操作。所以每个请求都会有一个携程处理。即使ngx_lua需要运行Lua,相对C有一定的开销,但依然能保证高并发能力。
nginx协程机制:nginx每个工作进程创建一个lua虚拟机。工作进程中的所有协程共享同一个虚拟机。每个外部请求都由一个lua协程处理,之间数据隔离。lua代码调用io等异步接口时,协程被挂起,上下文数据保持不变。自动保存,不阻塞工作进程。io异步操作完成后还原协程上下文,代码继续执行。
nginx处理阶段(从上往下执行):
NGX_HTTP_POST_READ_PHASE=0 //读取请求头
NGX_HTTP_SERVER_REWRITE_PHASE //执行rewrite → rewrite_handler
NGX_HTTP_FIND_CONFIG_PHASE //根据uri替换location
NGX_HTTP_REWRITE_PHASE //根据替换结果继续执行rewrite → rewrite_handler
NGX_HTTP_POST_REWRITE_PHASE //执行rewrite后处理
NGX_HTTP_PREACCESS_PHASE //认证预处理 请求限制,连接限制 → limit_conn_handler,limit_req_handler
NGX_HTTP_ACCESS_PHASE //认证处理 → auth_basic_handler,access_handler
NGX_HTTP_POST_ACCESS_PHASE //认证后处理,认证不通过,丢包
NGX_HTTP_TRY_FILES_PHASE //尝试try标签
NGX_HTTP_CONTENT_PHASE //内容处理 → static_handler
NGX_HTTP_LOG_PHASE //日志处理 → log_handler
nginx挂载点:
主要用到的有:
init_by_lua:系统启动时调用
init_worker_by_lua:worker进程启动时调用
set_by_lua:nginx变量用复杂lua return
rewrite_by_lua:重写url规则
access_by_lua:权限验证阶段
content_by_lua:内容输出节点