Java性能优化打造亿级流量秒杀系统:4.查询性能优化:多机缓存
4.查询性能优化:多机缓存
一、学习目标:
上一节通过服务器的水平扩展,将应用部署在多台机器上来处理请求极大地提升了TPS,但这多台数据库仍然使用同一台机器的数据库。
接下来将使用一些查询优化的技术,来完成商品详情页的查询优化解决方案。
- 掌握多级缓存的定义
- 掌握 redis缓存,本地缓存
- 掌握热点nginx lux缓存
二、缓存设计原则概览
- 缓存要使用快速存取设备,内存
- 将缓存推到离用户最近的地方,减少网络延时
- 脏缓存清理,数据库发生变更后,缓存中的旧数据就是
脏缓存
,如何清理,清理策略
** 多级缓存**
- redis缓存
- 热点数据,内存本地缓存
- nginx proxy cache缓存
- nginx lua缓存
三、Redis集中式缓存介绍
安装、原理、session管理、token管理见前文。
key-value格式的数据库。
memorycatch 是完全的内存数据库。
而redis可以将数据刷新到磁盘,容许一定数量的丢失。一般将其当做易失性的存储。
Why集中式缓存?
应用服务器水平拓展后都连接这同一台redis服务器。
1.单机版
故障瓶颈,容量上限
2.sentinal哨兵模式
redis2做redis1的从属备份redis,redis上的改动都被同步至redis2上备份。
但党redis1挂掉的时候,理应应用服务器理应自动向redis2请求服务,但是由于并发环境太复杂,应用服务器很难感知到redis2挂掉了。因此产生了sentinel哨兵模式。
在第一次使用时,应用服务器首先ask sentinel,该使用哪台redis,sentinel回复告知,应用服务器再去向redis请求。
sentinel工具与redis建立心跳机制。任何导致心跳断开的情况,sentinel都认为是redis1的故障。则发送命令,令redis2变更为master,redis1变更为slave,并通知应用服务器change(如下图)。
3.集群cluster模式
哨兵模式的缺点也很明显,同一时间只有一台redis对外提供服务。
在cluster之前,使用分片与读写分离:
但在新增redis5的时候,需要各种繁杂的数据迁移。可扩容性还是差。
因此出现了cluster集群模式:
redis2个读,2个写。并自动竞选出master和slave。并且每一个redis都知道所有的关系。并将这关系路由表发给应用服务器,应用服务器将这些分片信息维护在自己的内存当中。
当redis3出现故障时,自动通过帕克机制重新分配地位,哈希分片都会自动重新调整。
但应用服务器内还是错误的分片路由列表。当应用服务器按照错误列表找到了redis2请求某数据时,redis2发现服务器请求的key不属于自己调整之后的管理范畴,则向服务器返回reask请求,让服务器重新拉取最新的分片路由表。
以上三种模式均得到了 jedis
的实现。
四、Redis集中式缓存商品详情页
在controller层,将详情信息缓存起来,不去走下游service的调用了。减少对数据库的依赖。
但是被redis自己序列化后的乱码
更改了默认的键值的序列化方式,使其易读。
五、本地热点缓存
即JVM服务器的本地
- 热点数据
- 脏读非常不敏感,数据库一旦改动,就会出现很多脏缓存数据被读取而不知
可以通过MQ进行更新,但得不偿失。
- 内存可控,且珍贵
仅仅作为瞬时访问,因此缓存有效时间很短。
得支持并发读写。单纯的hashmap无法满足。还得以LRU等策略进行淘汰,按时间KEY自动失效等等。
实现:
Guava cache
- 可控只得大小和超时时间
- 可配置的lru策略
- 线程安全
@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);
}
}
提升1000TPS。
六、Nginx proxy catch
- nginx反向代理前置启用时,proxy catch才能用
- 依靠文件系统索引级的文件来完成缓存操作
- 依靠内存来 缓存文件地址
但由于其是将缓存存储在本地磁盘系统了,并不是保存在内存当中,因此QPS没有明显提升,甚至比jvm本地缓存的性能下降了。平均时延更长了。
七、Nginx Lua原理
- lua协程机制
线程空间站内的,依托于线程,根据用户模拟出来的
- nginx协程机制
无需考虑异步编程模式
- nginx lua插载点
- OpenResty
1.协程机制
- 依附于线程的内存模型,切换开销小
- 遇到阻塞及时归还执行权,代码同步
- 无需加锁
2.Nginx协程
- nginx的每一个worker进程都是在epoll或kqueue这种事件模型之上,封装成协程。
当
- 每一个请求都有一个协程进行处理
- 即使ngx_lua运行lua,相对c有一定的开销,但依旧能保证高并发能力。
3.Nginx协程机制
- nginx每个工作进程创建一个lua虚拟机。用来跑lua文件
- 工作进程内的所有协程共享同一个vm
- 每一个外部请求由一个lua协程处理,之间数据隔离
- lua代码调用io等异步接口时,协程被挂起,上下文数据保持不变
- 自动保存,不阻塞工作进程
- io异步操作完成后还原协程上下文,代码继续执行。即代码是同步型的编程,比较简单。
即在接收到HTTP请求后,分配一个lua协程去处理,但碰到比如反向代理需要等待后端服务器返回数据时,把socket句柄放置的epoll监听队列中,把自己挂起。继续执行其他协程,等到后端服务器返回时,epoll会监听到,然后协程唤醒自己再处理后面的response操作。
也正因此,各个协程之间的数据是隔离的。
nginx是一个worker就是一个线程,底下是很多的协程。完全基于协程的方式串行处理。
而java servlet在处理http请求时,是一个请求,分配一个线程去处理的。
4.Nginx处理阶段
5.Nginx lua挂载点
6.OpenResty
- 借助Nginx的事件驱动模型和非阻塞IO,可以实现高性能的web应用程序。
- 提供了大量组件如Mysql、redis、memcache等,使在nginx上开发web应用更方便简单。
(1) hello world
即在访问helloworld时,返回访问/item/get?id=6的结果。
(2) shared dic
共享内存字典,所有worker进程可见
类似于guaua catch一样的内存字典,且支持lru淘汰
但具有内存限制。
所以
(3) openresty redis支持
nginx从redis中读取肯定比从shared dic中要慢一点,
更新机制就在nginx处省掉了,让下游应用服务器去更新,nginx只读不写。
nginx可以实时感知redis中的数据,可以避免脏读。
因此,nginx可以只将热点数据存在自己的内存缓存中,非热点数据就读redis slave。
八、总结
越靠近上游,缓存占用的系统资源越昂贵,对应的更新机制越难,性能越高。
因此没有统一通用的缓存方案,需要根据实际业务场景,对脏读的容忍程度,热点数据的程度,来决定动态请求的缓存方案。
上一篇: 算法原理 朴素贝叶斯法
下一篇: prototype和__proto__