44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景

目录

  • 前言
  • 瞬间缓存热点问题
  • 基于nginx+lua+storm的热点缓存的流量分发策略自动降级解决方案
    • 方案思路
      • storm实时计算热点
      • storm发送热点到nginx
      • nginx的分发策略降级
      • storm热点取消逻辑
  • 实战
    • 热点商品计算
    • 流量分发nginx
    • 服务应用nginx
    • 负载均衡流量分发策略
    • 热点缓存消失逻辑
  • 测试

前言

项目地址:eshop-study
切换到相应分支:
44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第1张图片

瞬间缓存热点问题

  1. 上一章我们解决的缓存冷启动问题: 热数据 -> 热数据的统计 -> redis中缓存的预热 -> 避免新系统刚上线,或者是redis崩溃数据丢失后重启,redis中没有数据,redis冷启动 -> 大量流量直接到数据库; redis启动前,必须确保其中是有部分热数据的缓存的

  2. 本章我们要解决瞬间的缓存热点
    44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第2张图片

基于nginx+lua+storm的热点缓存的流量分发策略自动降级解决方案

方案思路

44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第3张图片

storm实时计算热点

  1. 在storm中,实时的计算出瞬间出现的热点
  2. 有很多种算法,简单的算法,某个storm task,上面算出了1万个商品的访问次数,商品id和访问次数存到``LRUMap
  3. 频率高一些,每隔5秒,去遍历一次LRUMap,将其中的访问次数进行排序,统计出往后排的95%的商品访问次数的平均值
  4. 比如商品id列表: [1000,999,888,777,666,50,60,80,100,120],95%的商品即1000后面的9个商品id,访问次数的平均值是100,然后,从最前面开始,往后遍历,去找有没有瞬间出现的热点数据,这里就只是判断第一个;
  5. 设定一个热点商品访问次数阈值,比如95%的平均值(100)的10倍,如果LRUMap中第一个商品id的访问次数超过这个值,我们就认为是瞬间出现的热点数据
  6. 当遍历,发现说第一个商品的访问次数,小于平均值的10倍,让该商品id热点消失,break循环

storm发送热点到nginx

  1. storm通过算法计算出热点商品id列表后,会直接发送http请求到nginx上,nginx上用lua脚本去处理这个请求
  2. storm会将热点本身对应的productId,发送到流量分发nginx上面去,放在本地缓存中;
  3. storm会将热点对应的完整的商品详情缓存数据,发送到所有的应用nginx服务器上去,直接放在本地缓存中

nginx的分发策略降级

  1. 流量分发nginx,添加逻辑:每次访问一个商品详情页的时候,如果发现它是个热点,那么立即做流量分发策略的降级
  2. 之前的普通商品请求 hash策略:同一个productId的访问都同一台应用nginx服务器上,降级成对这个热点商品,流量分发采取负载均衡发送到所有的后端应用nginx服务器上去
  3. 瞬间将热点缓存数据的访问,从hash分发全部到一台nginx,变成了,负载均衡发送到多台nginx上去
  4. 避免说大量的流量全部集中到一台机器,50万的访问量到一台nginx,5台应用nginx,每台就可以承载10万的访问量

storm热点取消逻辑

  1. storm还需要保存下来上次识别出来的热点list

  2. 下次去识别的时候,这次的热点list跟上次的热点list做一下diff,看看可能有的商品已经不是热点了

  3. 热点的取消的逻辑,发送http请求到流量分发的nginx上去,取消掉对应的热点数据,从nginx本地缓存中,删除

实战

热点商品计算

  1. 在storm拓扑中加入热点缓存实时自动识别和感知的代码逻辑
  2. 在storm拓扑中加入nginx反向推送缓存热点与缓存数据的代码逻辑
  3. 在流量分发+后端应用双层nginx中加入接收热点缓存数据的接口

流量分发nginx

服务机器: [eshop-cache03: 192.168.0.108]

  1. vi /usr/hello/hello.conf
    44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第4张图片
location /hot {
        default_type 'text/html';
        # lua_code_cache off;
        content_by_lua_file /usr/hello/lua/hot.lua;
    }
  1. vi /usr/hello/lua/hot.lua
    在这里插入图片描述
local uri_args = ngx.req.get_uri_args()
local product_id = uri_args["productId"]

local cache_ngx = ngx.shared.my_cache

local hot_product_cache_key = "hot_product_"..product_id
-- 缓存1小时
cache_ngx:set(hot_product_cache_key, "true", 60 * 60)
  1. 重启nginx: /usr/servers/nginx/sbin/nginx -s reload

服务应用nginx

服务机器: [eshop-cache01: 192.168.0.106][eshop-cache02: 192.168.0.107]

  1. 同上面小节分发nginx [eshop-cache03: 192.168.0.108]的第一步;
  2. 同上面小节分发nginx [eshop-cache03: 192.168.0.108]的第二步,内容更换为下面的配置;
local uri_args = ngx.req.get_uri_args()
local product_id = uri_args["productId"]
local product_info = uri_args["productInfo"]

local product_cache_key = "product_info_"..product_id

local cache_ngx = ngx.shared.my_cache
-- 缓存1小时
cache_ngx:set(product_cache_key,product_info,60 * 60)
  1. 重启nginx: /usr/servers/nginx/sbin/nginx -s reload

负载均衡流量分发策略

在nginx+lua中实现热点缓存自动降级为负载均衡流量分发策略的逻辑
服务机器: [eshop-cache03: 192.168.0.108]

  1. vi /usr/hello/lua/distribute.lua

之前是对商品id取模,然后将请求分发到两台应用nginx机器上

local uri_args = ngx.req.get_uri_args()
local productId = uri_args["productId"]
local shopId = uri_args["shopId"]

local hosts = {"192.168.0.106", "192.168.0.107"}
local backend = ""

-- 和上面分发层nginx设置的key值相同
local hot_product_key = "hot_product_"..productId

local cache_ngx = ngx.shared.my_cache
local hot_product_flag = cache_ngx:get(hot_product_key)

-- 判断是否走原来hash分发还是降级为负载均衡流量分发策略
if hot_product_flag == "true" then
  math.randomseed(tostring(os.time()):reverse():sub(1, 7))
-- 现在有两台应用nginx机器,随机获取12
  local index = math.random(1, 2)  
  backend = "http://"..hosts[index]
else
  local hash = ngx.crc32_long(productId)
  local index = (hash % 2) + 1
  backend = "http://"..hosts[index]
end

local requestPath = uri_args["requestPath"]
requestPath = "/"..requestPath.."?productId="..productId.."&shopId="..shopId

local http = require("resty.http")
local httpc = http.new()

local resp, err = httpc:request_uri(backend,{
  method = "GET",
  path = requestPath,
  keepalive=false
})

if not resp then
  ngx.say("request error: ", err)
  return
end

ngx.say(resp.body)

httpc:close()
  1. 注意nginx配置开启缓存:vi /usr/servers/nginx/conf/nginx.conf
    44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第5张图片
lua_shared_dict my_cache 128m;
  1. 重启nginx: /usr/servers/nginx/sbin/nginx -s reload

热点缓存消失逻辑

在storm拓扑中加入热点缓存消失的实时自动识别和感知的代码逻辑

  1. 代码逻辑: ProductCountBolt.java
// 6. 实时感知热点数据的消失
if (lastTimeHotProductIdList.size() > 0) {
	for (Long productId : lastTimeHotProductIdList) {
		if (!hotProductIdList.contains(productId)) {
			// 说明上次计算的热点商品id已经不是热点
			// 发送该商品id的http请求到流量分发的nginx中,取消热点缓存标识
			String cancelHotProductIdUrl = "http://192.168.0.108/cancel_hot?productId=" + productId;
			HttpClientUtils.sendGetRequest(cancelHotProductIdUrl);
		}
	}
}

if (hotProductIdList.size() > 0) {
	lastTimeHotProductIdList.clear();
	lastTimeHotProductIdList.addAll(hotProductIdList);
}
  1. 分发层nginx [eshop-cache03: 192.168.0.108]修改: vi /usr/hello/hello.conf
location /cancel_hot {
    default_type 'text/html';
    # lua_code_cache off;
    content_by_lua_file /usr/hello/lua/cancel_hot.lua;
}
  1. 分发层nginx [eshop-cache03: 192.168.0.108]添加lua脚本: vi /usr/hello/lua/cancel_hot.lua
local uri_args = ngx.req.get_uri_args()
local product_id = uri_args["productId"]

local cache_ngx = ngx.shared.my_cache

local hot_product_cache_key = "hot_product_"..product_id
- 缓存1分钟
cache_ngx:set(hot_product_cache_key, "false", 60 * 60)
  1. 重启nginx: /usr/servers/nginx/sbin/nginx -s reload

测试

将热点缓存自动降级解决方案的代码运行后观察效果以及调试和修复bug

  1. 测试过程参考43. 缓存冷启动问题解决方案:基于storm实时热点统计的分布式并行缓存预热;
  2. 访问商品详情的http请求:按照95%平均值计算排序好后后面5个的平局值,这里访问6个商品不同id的请求;
http://192.168.0.108/product?requestPath=product&productId=1&shopId=1
http://192.168.0.108/product?requestPath=product&productId=2&shopId=1
http://192.168.0.108/product?requestPath=product&productId=3&shopId=1
http://192.168.0.108/product?requestPath=product&productId=4&shopId=1
http://192.168.0.108/product?requestPath=product&productId=5&shopId=1
http://192.168.0.108/product?requestPath=product&productId=6&shopId=1
  1. 不断访问productid = 6 的http请求,使其成为一个商品热点:发现日志已经打印逻辑正确
    44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第6张图片
  2. 此时再访问productid = 6 的http请求,发现已经实现自动降级为负载均衡流量分发策略的逻辑,随机访问eshop-cache01eshop-cache02应用nginx;
    44. 热点缓存问题:秒杀时的超热商品可能导致系统全盘崩溃的场景_第7张图片

你可能感兴趣的:(缓存高可用微服务实战)