内容分类
容量规化
架构设计
数据库设计
缓存设计
框架选型
数据迁移方案
性能压测
监控报警
领域模型
回滚方案
高并发
分库分表
优化策略
负载均衡
软件负载
nginx:它自身的高可用是用lvs去保证。
下单需要登录 > 需要Session > 分布式Session的会话问题 >
解决方案:
1、ip哈希进行路由,某个人的ip只会路由到一台服务器,从而避免了分布式session的问题。【但是某台单点故障了呢?或者是某台容量上限了呢?】
2、web服务器的tomcat之间的session相互同步(tomcat内含广播机制)问题【耗内存,耗宽带】
3、session放入到redis中即可【引入缓存技术,同时需要维护redis的高可用】
硬件负载
数据库优化
1、换数据库 mysql oracle tidb redis
读多写少(访问商品时) 》 redis搜索 》 es内存搜索(全量同步和增量同步)
2、分库分表(读写分离)
jdbc应用层:shardingSphere 、 tddl 【java应用层-》请求哪台数据库。性能较好,毕竟少了一次通讯;但是依赖于jar包,依赖于java语言】
proxy代理层:mycat、mysql-proxy 【中间代理层-》请求哪台数据库。与语言无关】
3、内部本身优化(字段、视图、索引、参数调优等)
应用程序优化
拆分服务
目前都是通过增加机器来解决的。但是有的应用程序模块不需要这么多的机器,造成了机器的浪费。但是有个应用程序这些增加的机器又不够,所以得把应用程序拆开,拆分为服务后独立部署,最大效率的利用资源。
会员部门新增需求上线成功,商品部门修改的bug上线失败,需要进行回滚。
【1个服务变成了多个服务之后,服务之间的依赖调用就会增多。】
【使用RPC框架远程异步(mq,异步,解耦,削峰)调用,如何保证分布式事务(系统之间的分布式事务和不同种类数据库之间的分布式事务),如何保证幂等性?????????????????】
【如果引入了mq,如何保证消息重复,消息丢失,消息积压】
接口幂等性解决方案:
1、全局唯一ID,数据库
2、去重表,redis
3、状态机,0,1
那么数据积累如何解决呢??????????????
商品模块的难点(压测商品详情)
1、表的设计
1、从上往下分析:
商品分类:category 【手机】
商品品牌:brand 【苹果】
商品规格:product 【SPU】 iphone14
物品属性:attr
---------直至找到SKU【可以定位到库存的实际物品】----------------------------
2、页面静态化
技术:FreeMarker 提前准备好模板,然后动态生产html,然后放到CDN服务器上,美名其曰“静态页面比动态页面加载快”。
适用于:商品量较少的网站;
缺陷:
1、小米的商品只有1000个,那么它只需要几个模板,动态生成的静态页面就是1000个,可适用于小型网站。一个模板改了,那么用其生成的页面都得改。
2、它是根据服务器进行生成的,那么不同的服务器之间如何进行同步呢?【SCP命令??】
3、磁盘IO+网络IO优化(引入redis)
redis它是基于内存的数据库,基本不涉及磁盘的iO;
商品详情页:网络IO【通信3306】+磁盘IO【mysql做了持久化】,但是它有读多写少的特点;所以可以将其放入到缓存里边去。
4、数据一致性解决方案
1、最终一致性解决方案
给缓存设置失效时间;
2、实时一致性解决方案
1、高并发下的线程安全问题;
并发情况下,为什么还是走那么多次数据库,而不是走一次数据库,然后走缓存呢?
原因:在并发过程中,在查询数据库,还没有设置缓存的时候,有很多其余的线程继续执行该方法,导致查询数据库。所以要加锁。
关于加锁:
1、单实例加锁没问题;
2、多实例加锁:要加分布式锁;
2、缓存问题
缓存穿透(查询永不存在的数据)
指的是查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也没有此条记录,我们没有将这次查询的null写入到缓存,这将 导致这个不存在的数据每次请求都要到存储层去查询,这就使得缓存失去了意义。
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃。
解决:null结果缓存,并加入短暂过期时间。(如果一直对空结果进行缓存,那么如果数据库中有数据,缓存中会一直获取到数据为null,所以不会加 载到新的不为null的数据,所以要设置过期时间。)
缓存雪崩(集体失效)
指的是我们在设置缓存key时候,采用了相同的过期时间,导致缓存在某一个时间同时失效,
请求全部转发到db,db瞬时压力过重崩溃。
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每个缓存的重复率就会降低,很难引发集体失效的事件。
缓存击穿(过期的热点数据)
指的是对于一些设置了过期时间的key,这些key过于热点,在某一时刻失效后,同时有大并发来请求数据,此时要请求数据库,造成数据库崩溃。
解决:加锁,大量并发只让一个人去请求,其他人等待,查询到以后释放锁,其他人获取锁。先查询缓存,获取数据,就不会去请求数据库了。
3、压缩的问题(减少内存-以数据进行压缩,以最小的内存做最多的事情);
redis解决的是磁盘io,所以网络io还是存在的。所以网速和数据传输数据量还是影响很大的。
5、缓存通过什么样的方式可以避免网络IO
本地缓存:hashMap,有两个问题?
1、线程安全问题:用concurrentHashMap,也会有一些问题;
比如数据会一直增加,但是内存是有容量大小的。对缓存做一个过期时间,
那么引入guava缓存
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22</version>
</dependency>
@Component
public class GuavaCacheUtil {
private Cache<String, SkuInfoEntity> localCache = null;
@PostConstruct
private void init(){
localCache = CacheBuilder.newBuilder().
initialCapacity(10) //初始容量
.maximumSize(500) //最大容量
.expireAfterWrite(50, TimeUnit.MILLISECONDS) //过期时间
.build();
}
public void setLocalCache(String key,SkuInfoEntity obj){
localCache.put(key,obj);
}
public SkuInfoEntity get(String key){
return localCache.getIfPresent(key);
}
}
优先级
1、先查询本地缓存;【没有磁盘IO和网络IO】
2、查询redis;【有网络IO】
3、查询数据库;【有网络IO和磁盘IO】
public PmsProductParam getProductInfo4(Long id){
PmsProductParam productInfo = null;
productInfo = cache.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id);
if(null != productInfo){
return productInfo;
}
productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id,PmsProductParam.class);
if(productInfo != null){
cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id,productInfo); //设置本地缓存;
return productInfo;
}
RLock lock = redisson.getLock(lockPath + id);
try{
if(lock.tryLock(0,10,TimeUnit.SECONDS)){ //等待时间,持续时间
productInfo = portalProductDao.getProductInfo(id);
if(null == productInfo){
return null;
}
checkFlash(id,productInfo);
redisOpsUtil.set(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id,productInfo,3600,TimeUtil.SECONDS);
cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id,productInfo); //设置本地缓存;
log.info("set cache productId");
}else{
productInfo = redisOpsUtil.get(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id,PmsProductParam.class);
if(productInfo != null){
cache.setLocalCache(RedisKeyPrefixConst.PRODUCT_DETAIL_CACHE + id,productInfo); //设置本地缓存;
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(lock.isLocked()){
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
布隆过滤器(原理+代码)
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
原理:提前将所有的key值放入到过滤器中,如果新来的id没有击中key,那么就判断该id是非法数据,直接返回,不用经过数据库和缓存。
@Slf4j
@Configuration
public class BloomFilterConfig implements InitializingBean{
@Autowired
private PmsProductService productService;
@Autowired
private RedisTemplate template;
@Bean
public BloomFilterHelper<String> initBloomFilterHelper() {
return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8)
.putString(from, Charsets.UTF_8), 1000000, 0.01);
}
/**
* 布隆过滤器bean注入
* @return
*/
@Bean
public BloomRedisService bloomRedisService(){
BloomRedisService bloomRedisService = new BloomRedisService();
bloomRedisService.setBloomFilterHelper(initBloomFilterHelper());
bloomRedisService.setRedisTemplate(template);
return bloomRedisService;
}
//重点!!!!!!!!!!!!!!!!!!!!!!!!!
@Override
public void afterPropertiesSet() throws Exception {
List<Long> list = productService.getAllProductId();
log.info("加载产品到布隆过滤器当中,size:{}",list.size());
if(!CollectionUtils.isEmpty(list)){
list.stream().forEach(item->{
//LocalBloomFilter.put(item);
bloomRedisService().addByBloomFilter(RedisKeyPrefixConst.PRODUCT_REDIS_BLOOM_FILTER,item+"");
});
}
}
}
@Slf4j
public class BloomFilterInterceptor implements HandlerInterceptor {
@Autowired
private BloomRedisService bloomRedisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String currentUrl = request.getRequestURI();
PathMatcher matcher = new AntPathMatcher();
//解析出pathvariable
Map<String, String> pathVariable = matcher.extractUriTemplateVariables("/pms/productInfo/{id}", currentUrl);
//布隆过滤器存储在redis中
if(bloomRedisService.includeByBloomFilter(RedisKeyPrefixConst.PRODUCT_REDIS_BLOOM_FILTER,pathVariable.get("id"))){
return true;
}
/**
* 存储在本地jvm布隆过滤器中
*/
/*if(LocalBloomFilter.match(pathVariable.get("id"))){
return true;
}*/
/*
* 不在本地布隆过滤器当中,直接返回验证失败
* 设置响应头
*/
response.setHeader("Content-Type","application/json");
response.setCharacterEncoding("UTF-8");
String result = new ObjectMapper().writeValueAsString(CommonResult.validateFailed("产品不存在!"));
response.getWriter().print(result);
return false;
}
}
如果:后边以后增量数据,怎么办??通过消息中间件,去解决。
6、缓存的终极方案
小流量架构图:https://www.processon.com/view/link/5e5774dae4b0cb56daac5a80
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R3PyqSSh-1668502850638)(F:\面试准备\项目准备\小流量架构图.png)]
大型网站的架构:https://www.processon.com/view/link/5e57735ce4b0362765065cf3
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vioRJuim-1668502850639)(F:\面试准备\项目准备\大型网站架构图.png)]
1、OpenResty框架
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。
OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如
MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
http://openresty.org/cn/
项目一共需要3个nginx,一个用来做流量分发,其余两个用来做静态文件的缓存。
流量分发的nginx,会发送http请求到后端的应用层nginx上去,所以要先引入lua http lib包
wget https://raw.githubusercontent.com/pintsized/lua‐resty‐http/master/lib/resty/http_headers.lua
wget https://raw.githubusercontent.com/pintsized/lua‐resty‐http/master/lib/resty/http.lua
最近网络不稳定,也可以从:https://github.com/bungle/lua-resty-template 去下载这两个lua脚本。
nginx.conf
2、三层缓存存储分配
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zw9MwhXs-1668502850640)(F:\面试准备\项目准备\三层缓存商品分配.png)]
2-8原则即可。2成商品是最热的,其余商品放到redis中。
lua+nginx:数据量小,访问量很高;
JVM本地缓存:数据量很大,访问量比较高;
redis:数据量相对来说比较大,访问量相对不高;
LRU算法:最热的数据进入缓存;
LRU-K算法:最近最热的数据放入到缓存,连续访问三次以上。
【电商互联网架构演化:https://note.youdao.com/ynoteshare/index.html?id=63d31b96ca60b59374650cec2e6f4b3e&type=note&_time=1668501407455】