0
点赞
收藏
分享

微信扫一扫

商城电商day07 商品详情页面优化

归零者245号 2022-03-31 阅读 168

day07 商品详情页面优化

一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。
重点要讲的是另外一个层面:尽量避免直接查询数据库。
解决办法就是:缓存

1.2 整合redis到工程

开始开发先说明redis key的命名规范,由于Redis不像数据库表那样有结构,其所有的数据全靠key进行索引,所以redis数据的可读性,全依靠key。
企业中最常用的方式就是:object🆔field
比如:sku:1314:info
user:1092:info
:表示根据windows的 /一个意思

二、分布式锁

2.1 本地锁的局限性

接下来启动8206 8216 8226 三个运行实例。
运行多个service-product实例:
server.port=8216
server.port=8226
**集群情况下又出问题了!!!
以上测试,可以发现:
本地锁只能锁住同一工程内的资源,在分布式系统里面都存在局限性。
此时需要分布式锁。。
**

2.2 分布式锁实现的解决方案

  1. 多个客户端同时获取锁(setnx)
  2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
  3. 其他客户端等待重试

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性

2.4 使用redisson 解决分布式锁

1 获得skuKey
2 先查询缓存 通过skuKey从redis获取SkuInfo
3 缓存没有 就要去查数据库
4,先获取锁 若果数据库也没有
5 就把空的数据放入 缓存
6,如果数据库有数据 就把数据放入redis缓存
7,try catch 有异常打印 finally 释放锁
8,如果没获取到锁 睡一会 在尝试
9,缓存有就加入缓存
10 为了防止缓存宕机:从数据库中获取数据 return getSkuInfoDB(skuId);

if (skuInfonull){//先查询缓存 通过skuKey从redis获取SkuInfo
if (res){//先获取锁
if (skuInfo
null){ //若果数据库也没有
}
}else{
// 其他线程等待 如果没获取到锁 睡一会 在尝试
Thread.sleep(1000);
return getSkuInfo(skuId);
}
}else{
return skuInfo;
}
return getSkuInfoDB(skuId);

// 使用redis’ 做分布式锁

package com.atguigu.gmall.product.service.impl;

import com.alibaba.nacos.client.utils.StringUtils;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.model.product.SkuImage;
import com.atguigu.gmall.model.product.SkuInfo;
import com.atguigu.gmall.product.mapper.SkuImageMapper;
import com.atguigu.gmall.product.mapper.SkuInfoMapper;
import com.atguigu.gmall.product.service.TestService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.swing.*;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {

    //都是源码的内容 用redis时使用的
    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SkuInfoMapper skuInfoMapper;

    @Autowired
    private SkuImageMapper skuImageMapper;




/*
    // 使用redis' 做分布式锁
    private SkuInfo getSkuInfoRedis(Long skuId) {
        SkuInfo skuInfo = null;
        try {
            // 缓存存储数据:key-value
            // 定义key sku:skuId:info
            String skuKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
            // 获取里面的数据? redis 有五种数据类型 那么我们存储商品详情 使用哪种数据类型?
            // 获取缓存数据
            skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
            // 如果从缓存中获取的数据是空
            if (skuInfo==null){
                // 直接获取数据库中的数据,可能会造成缓存击穿。所以在这个位置,应该添加锁。
                // 第一种:redis ,第二种:redisson
                // 定义锁的key sku:skuId:lock  set k1 v1 px 10000 nx
                String lockKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
                // 定义锁的值
                String uuid = UUID.randomUUID().toString().replace("-","");
                // 上锁
                Boolean isExist = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                if (isExist){
                    // 执行成功的话,则上锁。
                    System.out.println("获取到分布式锁!");
                    // 真正获取数据库中的数据 {数据库中到底有没有这个数据 = 防止缓存穿透}
                    skuInfo = getSkuInfoDB(skuId);
                    // 从数据库中获取的数据就是空
                    if (skuInfo==null){
                        // 为了避免缓存穿透 应该给空的对象放入缓存
                        SkuInfo skuInfo1 = new SkuInfo(); //对象的地址
                        redisTemplate.opsForValue().set(skuKey,skuInfo1,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                        return skuInfo1;
                    }
                    // 查询数据库的时候,有值
                    redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
                    // 解锁:使用lua 脚本解锁
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                    // 设置lua脚本返回的数据类型
                    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                    // 设置lua脚本返回类型为Long
                    redisScript.setResultType(Long.class);
                    redisScript.setScriptText(script);
                    // 删除key 所对应的 value
                    redisTemplate.execute(redisScript, Arrays.asList(lockKey),uuid);

                    return skuInfo;
                }else {
                    // 其他线程等待
                    Thread.sleep(1000);
                    return getSkuInfoRedis(skuId);
                }
            }else {

                return skuInfo;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 为了防止缓存宕机:从数据库中获取数据
        return getSkuInfoDB(skuId);
    }
*/

    @Override
    public void testLock() {

    }

    @Override
    public SkuInfo getSkuInfoDB(Long skuId) {

        SkuInfo skuInfo = skuInfoMapper.selectById(skuId);
        if (skuInfo!=null){
            QueryWrapper<SkuImage> skuImageQueryWrapper = new QueryWrapper<>();
            skuImageQueryWrapper.eq("sku_id",skuId);
            List<SkuImage> skuImageList = skuImageMapper.selectList(skuImageQueryWrapper);
            skuInfo.setSkuImageList(skuImageList);
        }

        return skuInfo;
    }

Redisson做分布式锁



    private SkuInfo getSkuInfoRedisson(Long skuId) {
        SkuInfo skuInfo = null;
        try {
            // 缓存存储数据:key-value
            // 定义key sku:skuId:info
            String skuKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
            // 获取里面的数据? redis 有五种数据类型 那么我们存储商品详情 使用哪种数据类型?
            // 获取缓存数据
            skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
            // 如果从缓存中获取的数据是空
            if (skuInfo==null){
                // 直接获取数据库中的数据,可能会造成缓存击穿。所以在这个位置,应该添加锁。
                // 第二种:redisson
                // 定义锁的key sku:skuId:lock  set k1 v1 px 10000 nx
                String lockKey = RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
                RLock lock = redissonClient.getLock(lockKey);
            /*
            第一种: lock.lock();
            第二种:  lock.lock(10,TimeUnit.SECONDS);
            第三种: lock.tryLock(100,10,TimeUnit.SECONDS);
             */
                // 尝试加锁
                boolean res = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                if (res){
                    try {
                        // 处理业务逻辑 获取数据库中的数据
                        // 真正获取数据库中的数据 {数据库中到底有没有这个数据 = 防止缓存穿透}
                        skuInfo = getSkuInfoDB(skuId);
                        // 从数据库中获取的数据就是空
                        if (skuInfo==null){
                            // 为了避免缓存穿透 应该给空的对象放入缓存
                            SkuInfo skuInfo1 = new SkuInfo(); //对象的地址
                            redisTemplate.opsForValue().set(skuKey,skuInfo1,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            return skuInfo1;
                        }
                        // 查询数据库的时候,有值 放入到redis里边
                        redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);

                        // 使用redis 用的是lua 脚本删除 ,但是现在用么? lock.unlock
                        return skuInfo;

                    }catch (Exception e){
                        e.printStackTrace();
                    }finally {
                        // 解锁:
                        lock.unlock();
                    }
                }else {
                    // 其他线程等待
                    Thread.sleep(1000);
                    return getSkuInfo(skuId);
                }
            }else {

                return skuInfo;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 为了防止缓存宕机:从数据库中获取数据
        return getSkuInfoDB(skuId);
    }

    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        // 使用框架redisson解决分布式锁!
        return getSkuInfoRedisson(skuId);

        // return getSkuInfoRedis(skuId);
    }
}


四、分布式锁 + AOP实现缓存

package com.atguigu.gmall.common.cache;
import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.constant.RedisConst;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @author mqx
 * 处理环绕通知
 * @date 2020-11-11 09:30:29
 */
@Component
@Aspect
public class GmallCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    //  切GmallCache注解
    //小辣椒异常处理注解
    @SneakyThrows
    @Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
    public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
        //  声明一个对象
        Object object = new Object();
        //  在环绕通知中处理业务逻辑 {实现分布式锁}
        //  获取到注解,注解使用在方法上!
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);

        //  获取到注解上的前缀
        String prefix = gmallCache.prefix(); // sku

        //  方法传入的参数
        Object[] args = joinPoint.getArgs();

        //  组成缓存的key 需要前缀+方法传入的参数
        String key = prefix+ Arrays.asList(args).toString();

        //  防止redis ,redisson 出现问题!
        try {
            //  从缓存中获取数据
            //  类似于skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
            object = cacheHit(key,signature);
            //  判断缓存中的数据是否为空!
            if (object==null){
                //  从数据库中获取数据,并放入缓存,防止缓存击穿必须上锁
                //  perfix = sku  index1 skuId = 32 , index2 skuId = 33
                //  public SkuInfo getSkuInfo(Long skuId)
                //  key+":lock"
                String lockKey = prefix + ":lock";
                //  准备上锁
                RLock lock = redissonClient.getLock(lockKey);
                boolean result = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
                //  上锁成功
                if (result){
                    try {
                        //  表示执行方法体 getSkuInfoDB(skuId);
                        object = joinPoint.proceed(joinPoint.getArgs());
                        //  判断object 是否为空
                        if (object==null){
                            //  防止缓存穿透
                            Object object1 = new Object();
                            redisTemplate.opsForValue().set(key, JSON.toJSONString(object1),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
                            //  返回数据
                            return object1;
                        }
                        //  放入缓存
                        redisTemplate.opsForValue().set(key, JSON.toJSONString(object),RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);

                        //  返回数据
                        return object;
                    } finally {
                        lock.unlock();
                    }
                }else{
                    //  上锁失败,睡眠自旋
                    Thread.sleep(1000);
                    return cacheAroundAdvice(joinPoint);
                    //  理想状态
                    //  return cacheHit(key, signature);
                }
            }else {
                return object;
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //  如果出现问题数据库兜底
        return joinPoint.proceed(joinPoint.getArgs());
    }
    /**
     *  表示从缓存中获取数据
     * @param key 缓存的key
     * @param signature 获取方法的返回值类型
     * @return
     */
    private Object cacheHit(String key, MethodSignature signature) {
        //  通过key 来获取缓存的数据
        String strJson = (String) redisTemplate.opsForValue().get(key);
        //  表示从缓存中获取到了数据
        if (!StringUtils.isEmpty(strJson)){
            //  字符串存储的数据是什么?   就是方法的返回值类型
            Class returnType = signature.getReturnType();
            //  将字符串变为当前的返回值类型
            return JSON.parseObject(strJson,returnType);
        }
        return null;
    }
}

RedisTemplate之opsForValue使用说明

1、set(K key, V value)

新增一个字符串类型的值,key是键,value是值。
适用于 不修改的地方缓存

redisTemplate.opsForValue().set("stringValue","bbb");  

适用于经常修改的地方
redisTemplate.opsForHash().put(“user”,“age”,18);

redisTemplate.opsForHash().put(“user”,“name”,“Lisa”);

观察以上用法,和直接命令行 hset user age 18 无异,相当于是对象是user,age是属性,18是属性值

但是另外一种写法就是另一番景象了

1.1 CompletableFuture介绍

1.2 创建异步对象

CompletableFuture 提供了四个静态方法来创建一个异步操作。

没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。

  • runAsync方法不支持返回值。
  • supplyAsync可以支持返回值。

1.3 计算完成时回调方法

1.3 计算完成时回调方法
当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
whenComplete可以处理正常或异常的计算结果,exceptionally处理异常情况。BiConsumer<? super T,? super Throwable>可以定义处理业务

whenComplete 和 whenCompleteAsync 的区别:
whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
方法不以Async结尾,意味着Action使用相同的线程执行,而Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

1.4 线程串行化与并行化方法

thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作
带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。

Function<? super T,? extends U>
T:上一个任务返回结果的类型
U:当前任务的返回值类型

1.5 多任务组合

allOf:等待所有任务完成
anyOf:只要有一个任务完成

1.6 优化商品详情页

day 08 二、首页商品分类实现

思路:
1,首页属于并发量比较高的访问页面,我看可以采取页面静态化方式实现,或者把数据放在缓存中实现
2,我们把生成的静态文件可以放在nginx访问或者放在web-index模块访问

2.2 封装数据接口

由于商品分类信息在service-product模块,我们在该模块封装数据,数据结构为父子层级
商品分类保存在base_category1、base_category2和base_category3表中,由于需要静态化页面,我们需要一次性加载所有数据,前面我们使用了一个视图base_category_view,所有我从视图里面获取数据,然后封装为父子层级
数据结构如下:json 数据结构

分析返回的数据格式 list
list集合里不是特定的对象 用JSONObject

[
  {
    "index": 1,
    "categoryChild": [ // 获取一级分类下面的所有集合
      {
        "categoryChild": [
          {
            "categoryName": "电子书", # 三级分类的name
            "categoryId": 1
          },
          {
            "categoryName": "网络原创", # 三级分类的name
            "categoryId": 2
          },
          ...
        ],
        "categoryName": "电子书刊", #二级分类的name 
        "categoryId": 1
      },
     ...
    ],
    "categoryName": "图书、音像、电子书刊", # 一级分类的name// 一级分类名称
    "categoryId": 1// 获取一级分类Id
  },

// 变量迭代
index++;

2.2.1 ManageService接口

public class JSONObject extends JSON implements Map

   /**
     * 获取全部分类信息
     * @return
     */
    List<JSONObject> getBaseCategoryList();

查询所有分级的标题并且返回list集合

 @Override
    @GmallCache(prefix = "category")
    public List<JSONObject> getBaseCategoryList() {

        //存储所有的分类信息
        ArrayList<JSONObject> jsonObjects = new ArrayList<>();

        // 声明获取所有分类数据集合
        List<BaseCategoryView> baseCategoryViewList = baseCategoryViewMapper.selectList(null);

        // 循环上面的集合并安一级分类Id 进行分组
        Map<Long, List<BaseCategoryView>> category1Map   = baseCategoryViewList.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory1Id));

        int index = 1;
        // 获取一级分类下所有数据  list集合里边是json对象所以用
        for (Map.Entry<Long, List<BaseCategoryView>> entry1 : category1Map.entrySet()) {

            Long category1Id = entry1.getKey();
            List<BaseCategoryView> category2List1 = entry1.getValue();//

            JSONObject category1 = new JSONObject();
            category1.put("index", index);
            category1.put("categoryId", category1Id);
            // 一级分类名称
            category1.put("categoryName", category2List1.get(0).getCategory1Name());
            // 变量迭代
            index++;

            // 循环获取二级分类数据
            Map<Long, List<BaseCategoryView>> category2Map = category2List1.stream().collect(Collectors.groupingBy(BaseCategoryView::getCategory2Id));
            // 声明二级分类对象集合
            List<JSONObject> category2Child = new ArrayList<>();
            // 循环遍历
            for (Map.Entry<Long, List<BaseCategoryView>> entry2 : category2Map.entrySet()) {
                // 获取二级分类Id
                Long category2Id = entry2.getKey();
                // 获取二级分类下的所有集合
                List<BaseCategoryView> category3List = entry2.getValue();
                // 声明二级分类对象
                JSONObject category2 = new JSONObject();

                category2.put("categoryId", category2Id);
                category2.put("categoryName", category3List.get(0).getCategory2Name());
                // 添加到二级分类集合
                category2Child.add(category2);

                List<JSONObject> category3Child = new ArrayList<>();

                // 循环三级分类数据
                category3List.stream().forEach(category3View -> {
                    JSONObject category3 = new JSONObject();
                    category3.put("categoryId", category3View.getCategory3Id());
                    category3.put("categoryName", category3View.getCategory3Name());

                    category3Child.add(category3);
                });

                // 将三级数据放入二级里面
                category2.put("categoryChild", category3Child);


            }

            // 将二级数据放入一级里面
            category1.put("categoryChild", category2Child);
            jsonObjects.add(category1);
        }

        return jsonObjects;
    }
举报

相关推荐

0 条评论