缓存设计原则:
我们的项目采用多级缓存的架构
Redis缓存有集中管理缓存的特点,是常见NoSql数据库组件
热点数据存到JVM本地缓存中
所有数据最后都会在nginx服务器上做反向代理,nginx服务器也可以开启proxy cache缓存
nginx定制lua脚本做nginx内存缓存
Redis是一个NoSql 基于Key-valule数据库的中间件,是易失的
Redis缓存的几种形式:
**优点:**架构简单,方便,高性能
**缺点:**缓存中使用,重启后会丢失,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务+受CPU处理能力限制,CPU性能有瓶颈
Sentinel(哨兵)是用于监控redis集群中Master状态的工具,是Redis 的高可用性解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。
sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求
sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。其结构如下:
Redis支持主从同步机制,redis2作为redis1的slave从机,同步复制master的内容,当其中一个数据库宕机,应用服务器是很难直接通过找地址来切换成redis2,这时就用到了redis sentinal 哨兵机制。sentinal与redis1和redis2建立长连接,与主机连接是心跳机制,miaosha.jar无需知道redis1,redis2主从关系,只需ask
redis sentinal,之后sentinal就response
回应redis1为master,redis2为slave
一旦发生redis1坏掉或者发生网络异常,心跳机制就会破坏掉,sentinal更改redis2为master,redis1为slave,变换主从关系,然后发送change给应用服务器,然后miaosha.jar就向redis2进行get、set操作(或者redis读写分离,在master上set,slave上get)——redis 哨兵机制
总结一下:
Sentinal作用:
一般情况下,使用主从模式加Sentinal监控就可以满足基本需求了,但是当数据量过大一个主机放不下的时候,就需要对数据进行分区,将key按照一定的规则进行计算,并将key对应的value分配到指定的Redis实例上,这样的模式简称Redis集群。
cluster集群配置有多个slave用来读,master用来写,各种redis服务器彼此知道相互关系。
cluster好处:
首先,在Redis Cluster中每个节点都存有集群中所有节点的信息。它们之间通过互相ping-pong判断节点是否可以连接。如果有一半以上的节点去ping一个节点的时候没有回应,集群就认为这个节点宕机。
当主节点被集群公认为fail状态,那么它的从节点就会发起竞选,如果存在多个从节点,数据越新的节点越有可能发起竞选。集群中其他主节点返回响应信息。
当竞选从节点收到过半主节点同意,便会成为新的主节点。此时会以最新的Epoch通过PONG消息广播,让Redis Cluster的其他节点尽快的更新集群信息。当原主节点恢复加入后会降级为从节点。
当集群中某节点中的所有从实例宕机时,Redis Cluster会将其他节点的非唯一从实例进行副本迁移,成为此节点的从实例。
这样集群中每个主节点至少有一个slave,使得Cluster 具有高可用。集群中只需要保持 2*master+1 个节点,就可以保持任一节点宕机时,故障转移后继续高可用。
Redis Cluster保证基本可用的特性,在达到一定条件时才会认定为fail:
1、某个主节点和所有从节点全部挂掉,则集群进入fail状态。
2、如果集群超过半数以上主节点挂掉,无论是否有从节点,集群进入fail状态。
3、如果集群任意主节点挂掉,且当前主节点没有从节点,集群进入fail状态。
在商品详情页添加redis缓存
ItemController:
//商品详情页浏览
@RequestMapping(value = "/get",method = RequestMethod.GET) //浏览时服务端用GET请求
@ResponseBody
public CommonReturnType getItem(@RequestParam(name = "id") Integer id) {
//根据商品的id到redis内获取
ItemModel 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);
}
ItemVO itemVO = this.convertVOFromModel(itemModel);
return CommonReturnType.create(itemVO);
}
ItemModel、PromoModel要实现序列化
查看Redis写入的数据,发现会出现如下乱码:
修改config文件夹下的RedisConfig
@Component
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//首先解决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 JodaDateTimeJsonSerializer());
simpleModule.addDeserializer(DateTime.class,new JodaDateTimeJsonDeserializer());
objectMapper.registerModule(simpleModule);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
新建serializer文件夹,做日期DateTime的序列化\反序列化
JodaDateTimeJsonSerializer.java
:
public class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> {
@Override
public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
}
}
JodaDateTimeJsonDeserializer.java
public class JodaDateTimeJsonDeserializer extends JsonDeserializer<DateTime> {
@Override
public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String dateString = jsonParser.readValueAs(String.class);
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
return DateTime.parse(dateString,formatter);
}
}
连接到和redis server于数据库 server共享的server上面
top -H
ifconfig
./redis-cli -h 172.31.49.157
select 10
get item_6
采用Jmeter压测工具发现,Average耗时为250多ms,Tps 2000/s 采用top -H查看发现redisserver占用的cpu只有2%,没有到达瓶颈
本地数据热点缓存的解决方案类似于hashmap,key是item_id,value装的是itemModel。而且还要解决高并发问题,我们想到有Concurrenthashmap,为什么不用呢?
Segment+HashEntry+ReentrantLock
实现的,在1.8后采用Node+CAS+Synchronized
实现,get操作没有加锁,put锁加上后,会对读锁性能有影响Google公司推出了一款Guava cache组件,本质上也是一种可并发的hashmap,特点有:
下面首先在pom文件中加入依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>18.0version>
dependency>
新建cacheService接口,实现读和写两种操作
CacheService.java
//封装本地缓存操作类
public interface CacheService {
//存方法
void setCommonCache(String key,Object value);
//取方法
Object getFromCommonCache(String key);
}
CacheServiceImpl实现类
@Service
public class CacheServiceImpl implements CacheService {
private Cache<String,Object> commonCache = null;
@PostConstruct //保证Spring加载Bean优先执行这个init方法
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) {
//存在返回,不存在返回null
return commonCache.getIfPresent(key);
}
}
ItemController类中实现的原理就是:先查询本地缓存->Redis缓存->数据库
//商品详情页浏览
@RequestMapping(value = "/get",method = RequestMethod.GET) //浏览时服务端用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 = this.convertVOFromModel(itemModel);
return CommonReturnType.create(itemVO);
}
线程数1000,ramp-up时间:5s,循环次数:60
Average time 150ms,对应的Tps:3500/s,对应的redis几乎没有任何压力,
缓存机制从redis缓存加载到了jvm缓存之后,减少了多段的网络开销,并且完成了对应的内存访问输出结果,性能提升明显,但是数据更新之后缓存失效,还有JVM容量大小的限制;
启用nginx缓存的条件:
首先连接到nginx反向代理的服务器
修改conf文件nginx.conf
#声明一个cache缓存节点的内容
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;
//做一个二级目录,先将对应的url做一次hash,取最后一位做一个文件目录的索引;
//在取一位做第二级目录的索引来完成对应的操作,文件内容分散到多个目录,减少寻址的消耗
//在nginx内存当中,开了100m大小的空间用来存储keys_zone中的所有的key
//文件存取7天,文件系统组多存取10个G
location / {
proxy_cache tmp_cache;
proxy_cache_key &uri;
proxy_cache_valid 200 206 304 302 7d;//只有后端返回的状态码是这些,对应的cache操作才会生效,缓存周期7天
}
sbin/nginx -s reload重启服务器
nginx的缓存本质上缓存读取的内容还是本地的文件,并没有把对应的文件缓存在nginx内存中,所以不如nginx反向代理存的内容更高效。不是很理想
协程又叫微线程,最近几年在Lua脚本中得以广泛应用。协程,区别于子程序的层级调用,执行过程中,在子程序内部可中断,然后转而执行其他子程序,在适当的时候再返回来接着执行。
举个 生产者/消费者的 协程例子:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
整个代码关键点在于n = yield r
和r = c.send(n)
这两处。
生产者先执行循环n=n+1,运行到 r = c.send(n)
这句:将n通过send()传递给consumer,此时n = yield r
接受send的传递值,n=1,往下执行 r = '200 ok'
,再执行到n=yield r
时候,yield返回r,切换到produce函数,输出打印 retur 的r值。
运行结果如下:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
总结一下:
运行机制:
nginx每个工作进程创建一个lua虚拟机
工作进程内的所有协程共享同一个vm
每一个外部请求都是由一个lua协程处理,之间数据隔离;
lua代码调用io等异步接口时,协程被挂起,上下文数据保持不变;
自动保存,不阻塞工作进程
io异步操作完成后还原协程上下文,代码继续执行
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0, //读取请求头,例如get还是post,cookie中有哪些方法
NGX_HTTP_SERVER_REWRITE_PHASE, //执行rewrite - rewrite_handler,uri与location匹配前,修改uri的阶段,用于重定向
NGX_HTTP_FIND_CONFIG_PHASE, //根据uri替换location
NGX_HTTP_REWRITE_PHASE, //根据替换结果继续执行rewrite - rewrite_handler,上一阶段找到location块后再修改uri
NGX_HTTP_POST_REWRITE_PHASE, //执行rewrite后处理,防止重写URL后导致的死循环
NGX_HTTP_PREACCESS_PHASE, //认证预处理 请求限制,连接限制 -limit_conn_handler -limit_req_handler
NGX_HTTP_ACCESS_PHASE, //认证处理 - auth_basic_handler,access_handler,让HTTP模块判断是否允许这个请求进入Nginx服务器
NGX_HTTP_POST_ACCESS_PHASE, //认证后处理, 认证不通过, 丢包, 向用户发送拒绝服务的错误码,用来响应上一阶段的拒绝
NGX_HTTP_TRY_FILES_PHASE, //尝试try标签,为访问静态文件资源而设置
NGX_HTTP_CONTENT_PHASE, //内容处理 - static_handler 处理HTTP请求内容的阶段
NGX_HTTP_LOG_PHASE //日志处理 - log_handler 处理完请求后的日志记录阶段
} ngx_http_phases;
Nginx提供了许多再执行lua脚本的挂载方案,用的最多的几个nginx lua插载点
content_by_lua展示:
OpenResty由Nginx核心加很多第三方模块组成,默认集成了Lua开发环境,使得Nginx可以作为一个Web Server使用
借助于Nginx的事件驱动模型和非阻塞IO(epoll多路复用机制),可以实现高性能的Web应用程序
OpenResty提供了大量组件如Mysq、Redis、Memcached等等,使得在Nginx上开发Web应用更方便更简单。
下面进行OpenResty实践操作
新建helloworld.lua脚本:
ngx.exec("/item/get?id=6");
修改nginx.conf
location /helloworld {
content_by_lua_file ../lua/helloworld.lua;
}
重新reload,可以发现和访问正常url一样
新建itemsharedic.lua脚本:
function get_from_cache(key)
local cache_ngx = ngx.shared.my_cache
local value = cache_ngx:get(key)
return value
end
function set_to_cache(key,value,exptime)
if not exptime then
exptime = 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/get?id="..id)
item_model = resp.body
set_to_cache("item_"..id,item_model,1*60)
end
ngx.say(item_model)
修改nginx.conf
lua_shared_dict my_cache 128m;
location /luaitem/get {
default_type "application/json";
content_by_lua_file ../lua/itemsharedic.lua;
}
我们打算做这种架构,nginx通过读redis slave的内容,来兼顾内容的更新问题,redis自身有master/slave的主从机制。
若nginx可以连接到redis上,进行只读不写,若redis内没有对应的数据,那就回源到miaoshaserver上面,然后对应的miaoshaserver也判断一下redis内有没有对应的数据,
若没有,回源mysql读取,读取之后放入redis中 ,那下次h5对应的ajax请求就可以直接在redis上做一个读的操作,nginx不用管数据的更新机制,下游服务器可以填充redis,nginx只需要实时的感知redis内数据的变化,在对redis添加一个redis slave,redis slave通过redis master做一个主从同步,更新对应的脏数据。
新建itemredis.lua脚本
local args = ngx.req.get_uri_args()
local id = args["id"]
local redis = require "resty.redis"
local cache = redis:new()
local ok,err = cache:connect("172.26.241.149",6379)
local item_model = cache:get("item_"..id)
if item_model == ngx.null or item_model == nil then
local resp = ngx.location.capture("/item/get?id="..id)
item_model = resp.body
end
ngx.say(item_model)
修改nginx.conf,重启nginx
浏览网页成功
—————————————————————————————————
【本课程已整理完毕】
01_电商秒杀商品回顾
02_云端部署
03_分布式扩展
04_查询性能优化技术之多级缓存
05_查询性能优化技术之页面静态化
06_交易性能优化技术之缓存库存
07_交易性能优化技术之事务型消息
08_流量削峰技术
09_防刷限流技术
10_课程总结
—————————————————————————————————