电商项目缓存问题的解决方案(初步)

内容分类

容量规化

架构设计

数据库设计

缓存设计

框架选型

数据迁移方案

性能压测

监控报警

领域模型

回滚方案

高并发

分库分表

优化策略

负载均衡

软件负载

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】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值