商城笔记-----

sale_attr_value_id不为null的话is_check就是1

查出所有销售属性名和值并且用ischeck标记出当前sku

        select ssa.*,
               ssav.id    vid,
               ssav.sale_attr_value_name,
               IF(skuv.sale_attr_value_id IS NULL, "0", "1") is_checked
        from spu_sale_attr ssa
                 left join spu_sale_attr_value ssav
                           on ssa.spu_id = ssav.spu_id
                               and ssa.base_sale_attr_id = ssav.base_sale_attr_id
                 left join sku_sale_attr_value skuv on skuv.sku_id = #{skuId}
            and skuv.sale_attr_value_id = ssav.id
        where ssa.spu_id = #{spuId}
        order by ssa.base_sale_attr_id, ssav.id

这块两个查询:查询所有销售属性并且用ischeck表示当前sku是哪种销售属性(ischeck表示)。查询所有销售属性的组合,并且表示是哪种sku。

(sale_attr_value_id|sale_attr_value_id:skuid)

把这两个数据给前端,前端就能确定当前sku的销售属性是哪个组合,并且切换销售属性,前端就能知道往后端发送请求时候携带的skuid了。

用户切换一次销售属性,就往后端发送一次请求,并且携带相应的skuid

 
    <select id="getSpuValuesSkuJson" resultType="com.atguigu.gmall.product.dto.ValueSkuJsonDTO">
        select a.sku_id,
               GROUP_CONCAT(DISTINCT a.sale_attr_value_id
                   ORDER BY a.sale_attr_value_id ASC
                   SEPARATOR '|')
                   attr_value_concat
        from (select skuv.sku_id,
                     skuv.sale_attr_value_id
              from sku_sale_attr_value skuv
                       left join spu_sale_attr_value ssav
                                 on skuv.sale_attr_value_id = ssav.id
              where skuv.spu_id = #{spuId}
              ORDER BY skuv.sku_id, ssav.base_sale_attr_id, ssav.id) a
        GROUP BY a.sku_id
    </select>

最终

//最终(标准答案,记得加缓存null值)
    public SkuDetailVo skuDetailWithRedissonLockAndBloomFilter(Long skuId) {

        String key = RedisConst.SKU_DETAIL_CACHE_PREFIX + skuId;
        //1、先查询缓存
        SkuDetailVo data = cacheOps.getCacheData(key, SkuDetailVo.class); //基于异常机制。x的数据会被直接返回出去就
        //2、判断是否存在
        if (data != null) {
            //3、缓存中有直接返回
            return data;
        }

        //4、缓存中真没有:回源(缓存穿透、缓存击穿)
        boolean contain = redisson.getBloomFilter(RedisConst.BLOOM_SKUID).contains(skuId);
        if (!contain) {
            return null;
        }

        //5、100w问布隆49有没有?说有,开始回源(注意解决 击穿风险)
        //6、准备一个分布式锁解决击穿。同一个商品只放一个查询进去。注意:设计为细粒度的锁
        RLock lock = redisson.getLock(RedisConst.LOCK_PREFIX + skuId);//lock-49
        //7、试一下获取锁
        boolean b = lock.tryLock();
        if (b) {
            try {
                //8、拿到锁?回源; 双检查机制
                SkuDetailVo cacheData = cacheOps.getCacheData(key, SkuDetailVo.class);
                if (cacheData == null) {
                    //10、回源
                    log.info("商品详情回源: {}", skuId);
                    SkuDetailVo fromRpc = getSkuDetailFromRpc(skuId);
                    cacheData = fromRpc;
                    //11、放入缓存
                    cacheOps.saveData(key, fromRpc, 7L, TimeUnit.DAYS);
                }
                return cacheData;
            } finally {
                //12、解锁
                lock.unlock();
            }
        } else {
            try {
                //9、没拿到锁,等500ms。直接获取缓存中的数据
                Thread.sleep(500);
                return cacheOps.getCacheData(key, SkuDetailVo.class);
            } catch (InterruptedException e) {

            }
        }
        return null;
    }

嵌套三级分类

   
@Data
public class CategoryVo {

    private Long categoryId;  //当前分类id
    private String categoryName; //当前分类名
    private List<CategoryVo> categoryChild; //子分类

}





 <resultMap id="CategoryTreeRM"
               type="com.atguigu.gmall.web.CategoryVo">
        <id property="categoryId" column="c1id"></id>
        <result property="categoryName" column="c1name"></result>
        <!--        一级分类  -->
        <collection property="categoryChild"
                    ofType="com.atguigu.gmall.web.CategoryVo">
            <id property="categoryId" column="c2id"></id>
            <result property="categoryName" column="c2name"></result>
            <!--        二级分类  -->
            <collection property="categoryChild"
                        ofType="com.atguigu.gmall.web.CategoryVo">
                <id property="categoryId" column="c3id"></id>
                <result property="categoryName" column="c3name"></result>
                <!--        三级分类  -->
            </collection>
        </collection>
    </resultMap>



    <select id="getCategorysTree"
            resultMap="CategoryTreeRM">
        select bc1.id   c1id,
               bc1.name c1name,
               bc2.id   c2id,
               bc2.name c2name,
               bc3.id   c3id,
               bc3.name c3name
        from base_category1 bc1
                 left join base_category2 bc2 on bc2.category1_id = bc1.id
                 left join base_category3 bc3 on bc3.category2_id = bc2.id
    </select>

1、查询三级分类加了map缓存,吞吐量由300/s到8500/s。

2、缓存没有查询数据库==回源

一系列解决方案

1缓存数据,有bug

try{
        //1、查询redis
        String json = redisTemplate.opsForValue().get(key);
        if(StringUtils.isEmpty(json)){
            return null;
        }else {
//解决穿透,设置null值,不过我们设置的是x字符串。或者不用设置x,直接用布隆过滤器,把这个判断删除就行
        if("x".equals(jsonStr)){
            return null;
        }
     //3、缓存中有。直接返回
            return json ;
        }

//解决穿透
//布隆
RBloomFilter<Object> filter = redissonClient.getBloomFilter(bloomKey);
                   //判断布隆有没有
                if (!filter.contains("xxx")) {
                    return null;
                }

//解决击穿
//锁名一定不要为固定值,一旦为固定值查49,50商品为同一把锁。
            RLock lock = redissonClient.getLock("lock-" + skuId);

                //阻塞式加锁(无限等待,设置释放时间就不会无限等待了),
                // 一定要等到锁。会进行续期,默认30秒,每隔10秒续到30秒。
                // 参数:释放时间,单位。
                // lock.lock(10, TimeUnit.SECONDS);

                //开启锁功能.尝试加锁。返回值是布尔类型。三个参数:等待时间(最多等多久),释放时间,单位
                boolean b = lock.tryLock();
                if (b) {
                    //得到锁的逻辑
                    //8、加锁成功。回源。双检查机制:回源之前再查询一次缓存。(此代码没查缓存)原因:
                    //两个线程同时发起抢锁命令,一个卡住了,别人释放后,另一个拿到锁
                    //10、目标方法执行。如果自己抓异常一定要往出抛,否则多切面情况下有可能引起切面逻辑失效
                    result = pjp.proceed(args);//回源代码
                    //11、放入缓存,缓存中的每个数据都应该有过期时间;临时数据缓存短一点时间
                    String jsonData = data==null? "x":Jsons.toStr(data);
                   //缓存中的每个数据都应该有过期时间;临时数据缓存短一点时间
                    if(data == null){
                   redisTemplate.opsForValue().set(key,jsonData,30,TimeUnit.MINUTES);//存x,时间短一点
                   }else {
                  redisTemplate.opsForValue().set(key,jsonData,ttl,timeUnit);//存商品详情,时间长一点
                   }
                    //12、解锁放到finally
                    return result;
                }else {
                    //没得到锁的逻辑
                    //9、加锁失败。睡500毫秒再次查询缓存·
                    Thread.sleep(500);
                    return cacheOpsService.getCacheData(cacheKey, new TypeReference<Object>() {
                        @Override
                        public Type getType() {
                            return methodReturnType;
                        }
                    });
                }
}finally {
                //redisson自动判断是不是自己的锁
                //解锁一定要放到finally,不然会出现幽灵续期
                lock.unlock();

        }




-----------------------------
延迟双删

    //这个池子的队列是Integer.MAX;容易OOM
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(4);

    @Autowired
    StringRedisTemplate redisTemplate;

    @Override
    public void updateSkuInfo(SkuInfoUpdateVo vo) {
        //1、去数据库修改
//修改数据库数据的代码没写
        //2、延迟双删
        //2.1)、立即删
        redisTemplate.delete(RedisConst.SKU_DETAIL_CACHE_PREFIX + vo.getId());
        //提交延迟任务。有OOM风险
        pool.schedule(()->{
            redisTemplate.delete(RedisConst.SKU_DETAIL_CACHE_PREFIX+vo.getId());
        },10, TimeUnit.SECONDS);
    }

-------------------------------
布隆过滤器
--初始化代码就是下面的代码。service-product进行初始化,因为查skuid方便
    @PostConstruct
    public void initSkuIdBloom(){
        //1、初始化布隆
        RBloomFilter<Object> filter = redisson.getBloomFilter(RedisConst.BLOOM_SKUID);
        if(!filter.isExists()){
            //初始化布隆
            //期望插入多少数据,误判率
            log.info("布隆过滤器尚未初始化,正在初始化....");
            filter.tryInit(1000000,0.000001);
            List<Long> allSkuId = getAllSkuId();
            allSkuId.stream().forEach(item->{
                //查询skuid,添加skuid数据到布隆.(遍历放入)
                filter.add(item);
            });
            log.info("布隆过滤器初始化完成....");
        }
    }

--查询sku详情的时候的代码
RBloomFilter<Object> filter = redissonClient.getBloomFilter(bloomKey);
                   //判断布隆有没有
                    filter.contains("xxx");


--保存sku时候的布隆代码
        //把商品信息添加到布隆过滤器
        RBloomFilter<Object> filter = redisson.getBloomFilter(RedisConst.BLOOM_SKUID);
        filter.add(skuId);


--重置布隆过滤器。逻辑:创建新布隆,删除老布隆和老布隆的配置,修改新布隆的名字和配置为老布隆的名字和配置
    @Override
    public void resetBloom(String bloomSkuid) {
        //TODO 如何重建布隆?
        //删了重做?
        // 高速换胎
        //如何尽量缩短布隆不可用时间。 布隆重置期间不可用
        //1、创建一个新的布隆。保存所有数据
        RBloomFilter<Object> filter = redisson.getBloomFilter(bloomSkuid + "-new");
        if (!filter.isExists()) {
            log.info("正在重置布隆....新布隆创建中....");
            filter.tryInit(1000000, 0.000001);
            skuInfoService.getAllSkuId().stream().forEach(item -> {
                filter.add(item);
            });
        }

//这步原子操作是把删除布隆和给布隆改名两步操作写成lua脚本了,
        //4、原子操作  (要删除的key(skuid-bloom),把更名的key(skuid-bloom-new),改为要删除的key)
//KEYS[1]老布隆。KEYS[2]新布隆。
        String script = "redis.call(\"del\",KEYS[1]);" +
                "redis.call(\"del\",\"{\"..KEYS[1]..\"}:config\");" +
                "redis.call(\"rename\",KEYS[2],KEYS[1]);" +
                "redis.call(\"rename\",\"{\"..KEYS[2]..\"}:config\",\"{\"..KEYS[1]..\"}:config\"); return 0;";

        //RENAME key newkey
        //原子换轮胎
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(bloomSkuid, bloomSkuid + "-new"));//一个老布隆名字,一个新布隆名字
        log.info("布隆更换完成....新布隆上线....");
    }

2缓存数据

//最终
    public SkuDetailVo skuDetailWithRedissonLockAndBloomFilter(Long skuId) {

        String key = RedisConst.SKU_DETAIL_CACHE_PREFIX + skuId;
        //1、先查询缓存
        SkuDetailVo data = cacheOps.getCacheData(key, SkuDetailVo.class); //基于异常机制。x的数据会被直接返回出去就
        //2、判断是否存在
        if (data != null) {
            //3、缓存中有直接返回
            return data;
        }

        //4、缓存中真没有:回源(缓存穿透、缓存击穿)
        boolean contain = redisson.getBloomFilter(RedisConst.BLOOM_SKUID).contains(skuId);
        if (!contain) {
            return null;
        }

        //5、100w问布隆49有没有?说有,开始回源(注意解决 击穿风险)
        //6、准备一个分布式锁解决击穿。同一个商品只放一个查询进去。注意:设计为细粒度的锁
        RLock lock = redisson.getLock(RedisConst.LOCK_PREFIX + skuId);//lock-49
        //7、试一下获取锁
        boolean b = lock.tryLock();
        if (b) {
            try {
                //8、拿到锁?回源; 双检查机制
                SkuDetailVo cacheData = cacheOps.getCacheData(key, SkuDetailVo.class);
                if (cacheData == null) {
                    //10、回源
                    log.info("商品详情回源: {}", skuId);
                    SkuDetailVo fromRpc = getSkuDetailFromRpc(skuId);
                    cacheData = fromRpc;
                    //11、放入缓存
                    cacheOps.saveData(key, fromRpc, 7L, TimeUnit.DAYS);
                }
                return cacheData;
            } finally {
                //12、解锁
                lock.unlock();
            }
        } else {
            try {
                //9、没拿到锁,等500ms。直接获取缓存中的数据
                Thread.sleep(500);
                return cacheOps.getCacheData(key, SkuDetailVo.class);
            } catch (InterruptedException e) {

            }
        }

        return null;
    }

重置布隆

删除和改名两步操作一定要原子操作,

下图,布隆有两个key,一个存数据,一个存布隆的配置

3、缓存穿透

产生的原因:查找数据库中没有的数据。

(1)固定值穿透攻击解决方案

如果redis为空,查数据库,数据库如果也为空,但是redis还是会进行保存,这时候判断一下数据库返回值,为null的话随便给个数据就行了(比如字符串“x”,一定记得加过期时间,雷神给了半小时),数据库有值那就缓存真实的值。(反正雷神这么做的,其实穿透基本都是黑客攻击)

(2)随机值穿透攻击解决方案,大量请求过来,数据库扛不住。

《1》解决:redis全量缓存。查询只以redis为准,redis没有,数据库也就没有。

缺点:浪费空间。其实数据量小全量缓存也没事,几百兆而已。很多公司也在用。

《2》解决:公司存储比较紧张的话就redis全量缓存id,redis有数据就返回,没有就先问redis,看缓存有没有这个id,有就去数据库,没有就直接return

总结:上述全量缓存id其实主要是判断数据库中有没有数据,也可以使用布隆过滤器解决。

布隆过滤器:快速判断一个东西存不存在。优点:更省空间。缺点:会误判,只能增,不能删(可以定期重建),只能告诉我们有没有存过这个东西。

布隆说有不一定有,说没有就一定没有。

《3》使用redis的bitmap,1不用hash,2能增能改,3一个数据只占一位。

占用空间极少。

4.布隆过滤器

(1)本地实现:使用guava包进行实现

(2)分布式实现:redisson可以实现.

布隆过滤器可以用来专门过滤skuid,过滤订单id。

重置布隆过滤器:如何缩短布隆的不可用时间。方案:创建一个新的布隆,把数据放进去。删除老布隆。改名。删除和改名如果能原子化,就更帅了,lua脚本进行实现。操作的时候一定注意redison的布隆有两个key,一个是存数据的,一个是存配置的。

布隆重置的触发方式:定时。删除商品的时候进行记录次数,然后对100进行求余操作,每100次调用一次布隆重置。或者给个按钮,点一下就重置。

创建商品时记得给布隆也添加。

5.缓存击穿

原因:误删或者失效。在上一刻数据刚好过期,或者被删除。缓存失效:(缓存中无次数)。准备回源,问布隆过滤器,布隆说数据库有。100万请求同时找数据库要数据。数据库完蛋

解决:(1)加本地锁。只让一个请求进入数据库,查到后放到redis,其他99万9千9百99个请求全走redis就行。这块用reentrantlock(不敢把此锁new到方法中,一定要new到全局变量中)的trylock方法进行判断,不要用lock直接加锁,会阻塞的。trylock没获取到锁,就睡眠500毫秒(这个时间一定要让测试人员进行评估),线程醒了之后从redis中获取数据。

加锁疑问:500毫秒还是没有放到redis怎么办?

(2)加分布式锁

锁不释放问题(占坑+设置过期时间,必须原子,redis有原子方法)。删除被人的锁问题(比较锁值+删除锁必须原子,redis没有此原子方法,只能lua脚本实现)。自动续期问题(使用守护线程进行解决)。

断电不会执行finally代码。

6、缓存雪崩

加锁肯定是49号商品,50号商品等等加的都是不同的锁,而且不同业务加的也是不同的锁。

某一时刻商品业务的1000个商品的key同时失效,虽然有锁,但是每个商品锁都会放一个请求进入数据库,1000个请求数据库也扛不住。

锁粒度越细,并发性能越高。

解决:设置不同过期时间()。你看,单位为毫秒,设置成7天,然后随机值设置到100到10000。他一错开,哪怕错开的时间是两三秒,数据库也能轻松不少。雷神说京东都没发生过雪崩,他说有网络的存在,就会有误差,如果都设置成7天,误差也会有几毫秒。随机时间有可能弄巧成拙。

缓存雪崩只是理论上存在的东西,现实中不用解决这个问题,这是雷神亲口说的,因为有网络误差的存在。网络延迟本身相当于一个随机值。不仅网络,业务也会有延迟误差的存在。

7.缓存一致性。增删改会出现。

(下面两种方式的第二步都有可能失败。)canal也能解决,但是要加一层,雷神大概说了下。

双写模式保证一致性:写库+写缓存。解决:需要加分布式锁,保证写数据的一致性。

失效模式保证一致性:写库+删缓存。解决:延迟双删:写数据库+立即删缓存+延迟一段时间删缓存+数据过期兜底

金标准:

(1)缓存中的每个数据必须给过期时间(兜底),即便出现脏数据,也只脏一段时间。商品其他信息过期时间给7天,价格给1天。

(2)缓存中的数据永远都是给人看的,一旦涉及到计算交易等操作,一定要使用数据库中的数据进行实时查找。

(3)给后台管理系统设置一个手动清除缓存按钮。

雷神讲延迟双删的问题点是并发执行双写或者失效模式。分布式情况下就不是线程安全问题了,是机器安全问题了,得加分布式锁才能解决。并发读写在失效模式也有脏数据。但是雷神的解决方案是延迟双删,并且项目采取的是失效模式。

延迟双删的类:updateSkuInfo

8.redisson

lock(leasetime,时间单位)方法:无限等待锁,阻塞住了。lock和trylock方法不给参数就会不停续期,默认ttl为30秒,每次在ttl在20秒的时候续期。leasetime/3进行续期

trylock(等待时间(waittime),释放时间(leasetime),时间单位)方法。

redisson的续期bug(幽灵续期):每个请求进来,tomcat会分配一个线程进行处理,处理完成以后线程回收放到线程池。redisson续期的时候,设置的续期线程是这个请求线程的守护线程,redisson没做好这个判断,导致守护线程一直在续期。有线程跑定时任务

解决幽灵续期(卡死,不释放锁,就一直续期):保证解锁

幽灵续期问题雷神方案:解锁一定要放到finally,不然会出现幽灵续期

如果断电:断电的话续期线程必定死掉,锁到达30秒后就自动释放

就幽灵续期(业务异常)和断电两种情况吧

所以说不管业务异常,还是断电,只要放到finally中,一定会放锁,所以不用设置锁的释放时间。再说,设置释放时间,看门狗也会失效

锁不续期的两种方法:

1。设置释放时间:lock.lock(10, TimeUnit.SECONDS);

2。设置释放时间:boolean b = lock.tryLock(10,10, TimeUnit.SECONDS);

下面两种加锁方式都会续期(可能出现幽灵续期):

lock.lock();

boolean b = lock.tryLock();

关于看门狗机制:你设置释放时间,不走看门狗代码。你不设置释放时间,看门狗就会生效。

看门狗部门代码:线程被打断就会取消续期。线程正常,就三分之一看门狗时间执行一次续期代码,当然就是递归调用本身代码啦。

给锁指定释放时间(leasetime)锁就不具备续期功能。不指定leasetime,锁就会续期。

redisson底层源码:如果线程(续期时间的线程)被中断,才会取消续期。如果代码中有异常,执行那段代码的线程不一定被中断哦,这个线程还是存在的。(redisson的问题点:感知不到代码异常结束的问题,只会去判断线程有没有被中断。)

执行业务有一条线程,执行续期的业务也有一条线程,redisson判断的是执行续期的线程如果被打断,才会取消续期。

放锁两种情况如下:

(1)业务异常,finally一定执行解锁

(2)业务断电,续期线程没发运行,30s倒计时一到自动解锁

从以上两种情况可以得出:在项目中,一定不要设置锁的释放时期,一设置,锁的续期功能就消失了,雷神说的。

解锁代码:1删掉key2取消续期的那个定时任务

完整代码:

9.分布式信号量

可以用来限流。确实挺厉害。

10。最终实现redis锁和解决穿透和击穿

没拿到锁,判断的时候进行递归,这个方案我觉得挺叼,但是雷神说:这样很容易出现栈异常。看看第十五讲9:44。雷神说:睡眠500毫秒,有可能缓存还是没有,没有就没有嘛,用户再查一次就行了。

但是还有一个bug,虽然加锁的trylock()方法是原子的,但是就是有两个线程同时加锁,一个成功,另一个卡在网线上了,成功的那个把数据库数据放缓存了并且释放锁,延迟的线程又去回源,怎么解决?答:双检查机制。就是抢到锁之后再查询一次缓存,如果还没有再去查数据库。

哎,抢锁命令就是卡在网络上,你有啥办法?。。。

雷神查数据库没有的时候给redis缓存x“,”给“x”这种临时数据的过期时间为1个小时。数据库有数据的给7天过期时间。

延迟双删讲解:雷神说不要在系统中new thread,有oom风险。

雷神说线程池的队列的容量在现实中调到几千几万都有可能。

线程池参数中的工厂指的是由谁new那临时线程,keepalivetime表示几分钟临时线程不干活就杀掉

。线程池主要限制队列的大小。

反射可以暴力破解jdk里面的属性。

什么时候往redis中缓存商品?查询的时候,缓存中没有,进行回源,然后放到缓存。

什么时候往布隆中缓存商品id?项目启动的时候。

11线程池

雷神的线程池参数设置:

线程工厂:雷神有给设置工厂的线程数量,而且拿分布式计数,线程工厂只能设置这么多的线程。而且雷神还给每个线程设置了个名字,后期看日志好分析,线程名:"app-thread-pool"+(i++)。

项目经理说:我想监控我们全系统全整个集群他们开启线程的速度,当前开了多少个线程,每个线程的销毁速度,等等。监控整个线程池情况,怎么实现?

线程池想对接监控平台,监控平台自己从redis中拉取各种指标,进行可视化展示。怎么感知线程池跑起来?让任务包任务。雷神主要是在自定义线程工厂中进行监控,用任务包任务。

除了在线程工厂可以监控,也可以发请求进行监控。

雷神不仅在线程名字中加了计数,而且还在线程名中加了微服务的名字,后期有bug好排查。666

discardOldestPolicy:删除最老的任务,能给线程池提交的任务都是能跑的,这个不合适,一般线上不用。

AbortPolicy:满了之后,你提交任务,这个策略会拒绝并且抛一个异常。这个也不太使用,因为提交的任务都要执行的,不能拒绝。

CallerRunsPolicy:不再异步执行,谁给我提交的任务,谁执行run方法,进行执行。就是比较慢。虽然慢,但是这个任务执行了。一般线上用这个策略。

discardPolicy:AbortPolicy起码还抛异常了,这个什么都不说,就是一个空方法。这个更不使用。

核心线程:4,最大线程池大小:8,时间5分钟,队列大小:1000(调成压测峰值的两倍,或者依据内存大小调整成压测峰值的2~5倍都可以)

线程池执行任务的两个方法:submit:有返回值,可以调用get方法让主线程进行等待。execute:无返回值。雷神用的execute。

execute配合countDownLatch进行异步。注意:countDownLatch贵贱不敢写到方法外面,一定要设置成局部变量。几个execute就配合几个countDownLatch。

一般在项目中会有两个线程池,一个corepool,一个otherpool。corepool负责订单这种重要的任务。otherpool负责一些普通的任务,比如发短信,发邮件。

熔断:系统保护机制。降级:手动保护机制。

雷神的意思是双11的时候,订单业务非常繁忙,就可以取消注册业务等一些不重要的业务,或者关闭otherpool,这就是手动降级。手动关闭线程池:给后台一个按钮,调用shutdown方法就关了。

面试官问ioc,然后引申注入,说到qualifier的时候,说说这两个线程池的注入就使用到了qualifier。

12.异步编排

多异步任务的时候,异步之间有比较复杂的关系,可以通过异步编排,快速编写出他们之间的关系。

雷神第一个方案讲的就是闭锁+线程池实现编排关系。(就执行了两个execute)

雷神把第一个方案的代码注释了,现在使用completablefuture实现编排关系。

启动异步线程的方式:继承thread,实现runnable,callable,线程池,异步编排。

其实底层都是调new thread(task).start;就是这一种方式。

启动:runAsync(无返回值)和supplyAsync(有返回值)

thenrun表示和上一个任务用同一个线程。

whenComplete(返回值,异常)。

给面试官就说用的自己的线程池进行的异步编排。

get方法太烦了,就可以用allof方法进行结尾,都是阻塞等待的方法。

给面试官讲:completablefuture+线程池实现异步编排。

13.es

bean。这个mall是索引,感觉好奇怪。

将来数据库中的数据都要给es中存一份。

spu只是定义了属性名信息。sku定义了平台属性的每一个精确值。

排序:

热度分排序:商品每被点击一次加一。

平台属性和销售属性是什么?

下面检索条件栏红框展示的是所有商品涉及的属性名和属性值。这些值和属性在每一次检索后都有可能发生变化。雷神说这块都是平台属性。

同步es的数据模型:

给面试官讲的时候把几个分片几个副本也加上,这样更逼真一点,问es的部署就说是其他室的人部署的,或者说项目数据量不大,就没搞分片,就弄了一个副本。

关于es的存储:雷神用的冗余存储,京东也一样,把不参与检索的但是需要展示的商品图片也存储了。检索,排序。

关于价格类型:在javabean中是BigDecimal,在es中是double。

像平台属性就不需要分词。商品名需要分词。就一个是否需要分词,一个是否需要索引。

分词:text,不分词:keyword

给面试官讲的时候把类型加上吧,就像nested也说说。

避免扁平化存储。修改映射为nested,使用了nested就不能再普通检索,需要嵌入式检索。

金标准:如果es中保存数组对象,而且要用来参与检索,就必须nested(额外建立索引)。如果不参与检索就不用nested(省空间)。

写好接口后启动确认你的mapping是对的。

day11第七届中的上架功能SkuController,保存数据到es就用了个save方法。。。删除数据用的deleteById方法。面试说说上架功能。上架是给es中加数据,下架是给es中删除数据。

第八届:给面试官说搜索就说:好好从携带参数开始说。(这块雷神讲了接参数的注解)1分类入口。2模糊搜索。

按照分类,品牌,属性检索,排序检索,页码条件。

14.dsl

match用来做全文匹配的(文本),term精确查询(数字),

雷神在分类检索用的是must+term。嵌入式查询:must+nested。品牌查询:must+match。

字段类型:text,keyword。查询类型:term,match。

聚合:aggs。和query同级。

aggs==aggregations

聚合讲了两种:就这两种聚合,给面试官也讲讲,毕竟要给前端显示的。

第一种:品牌聚合,品牌id。子聚合:品牌名字,品牌logo。

第二种:属性聚合:属性id聚合,属性名字聚合,属性值聚合。

商品详情功能远程调用es增加热度分。防止频繁远程调用,累积更新,每满100发一次远程调用,这个临时变量存到redis了,每次加1都是往redis里加,每满100往es同步。

15.单点登录

一处登录,处处可用。

redis存储用户信息。redis中的key为uuid,同时用户手中的token也为uuid。

以/api/user/开头的请求都发给user的微服务,在网关的yml中配置的。

雷神不让用redistemplate,他建议使用Stringredistemplate.

雷神反正就弄了个登录和退出。

他把token也就是uuid存到前端一份,存到cookie中了,这是前端代码做的。

登陆后请求每次到达不同的微服务都会携带token,但是换浏览器就不行了哦。

服务器自己控制浏览器cookie逻辑的话,需要放大cookie的作用域到整个域名,才能让浏览器每次访问任何域名都带上。

用户id透传:就是网关把用户id查询出来,放到请求头,每次不同的服务从请求头查用户id就行,不用再去查redis或者数据库。

url:https://mp.csdn.net/mp_blog/creation/editor/150419089?not_checkout=1

uri:/mp_blog/creation/editor/150419089?not_checkout=1

uri不带协议和域名。

响应式编程:返回结果为Mono<T>。原理:方法不阻塞,感兴趣的自己订阅即可,就是他会把响应的结果暂时存到一个地方,到时候你自己拿就行。

网关20多分钟把我看得恶心的,跳过了,就是订单和秒杀没登录的话要跳到登陆页面。

重定向:302+location响应头

雷神一直在拿请求头中是否有token来判断是否登录。再次判断token的真假。能在redis查到就是真的,查不到就是假的。

金标准:永远不要相信前端带来的数据。

静态资源直接放行,内部远程调用的路径直接拒绝,需要登录的走正常流程,普通请求走相应的逻辑。还有token为假的情况需要考虑。假cookie问题。

16。购物车

没把数据存到数据库,不要看数据库中的表。

redis表设计:

购物车业务:

1.读写都多,并且无复杂检索(所以使用redis进行保存数据)

2.无复杂的CRUD,查询所有,修改某一个,无需按照其他字段进行检索

使用redis保存所有数据(线上开启aof+rdb)

优点:天然支持分片(理论上存储无限量数据),速度快

缺点:不支持复杂检索。复杂检索:es。一般复杂检索:mysql。无需复杂检索:redis。

购物车的设计:

1。临时购物车:前端自己生成一个唯一id,用户没登陆都可以加入购物车。

2.用户购物车:userid作为标识。

如果未登录,只给tempid(临时id),登录的话,后端两个id都能接收到

redis采用hash存储结构:大key:cart:info:用户id/临时id。小key:skuid。值:商品完整信息。skuid是小key。实体类:CartItem

redis使用list的话,修改商品数量的话,就得在redis中一个一个遍历,list只是查询比较快。使用hash修改就很快,可以通过skuid二次精确获取数据。

购物车特殊字段:cartprice:放入购物车时价格。skuprice:实时价格。价格有变动可以提示给用户。

项目难点:远程调用不能传userid和tempuserid。请求头里面的数据,远程是得不到的。feign远程调用丢失请求头问题。

同一个链路,之前请求头的数据应该在,实际却丢失了

把对象转成0101这种流叫序列化。字节流或者字符流。

把流转成对象叫反序列化。

序列化方案:json或者xml或者其他

远程调用丢失请求头解决方案:全局Map变量,key是线程,value是userid和tempuserid,在拦截器中,都放到模板中就好了。记得删这个线程的map,防止oom。第二种使用threadmap,也要记得删除哦。

跨层传递数据:直接方法参数传,利用线程绑定,redis传

雷神这块就是在做隐式传递用户id,好sb啊。

添加功能:登录了就是用户id,没登陆就是临时id。购物车有则数量修改,并且更新一次skuprice,无则新增。

查询功能:点击我的购物车或者去购物车结算,就要查询购物车列表了。根据创建时间进行排序。

修改功能:1,加一。-1,减一。有可能会有并发问题吗?雷神先get,后set,没处理并发,自己的操作自己的购物车应该也没并发问题。

删除购物车中选中的商品功能:先获取,再得到所有skuid,再删除。有数组操作,面试官问数组可以结合着说说。

购物车合并:一旦登录,未登录的购物车数据就要合并到当前登录的购物车中,然后清空临时购物车。有则修改,无则新增。合并后,还要清空临时购物车,不是原子操作,需要加事务,但是雷神没加。合并时机:登录状态下,用户查看购物车。

价格同步方案功能:

方案1:后台价格修改,顺便修改redis中的数据。如果有百万用户,商品价格修改,难道要遍历百万用户,看谁的购物车有这个商品吗?代码简单,性能极低,不推荐

方案2:获取购物车价格之前,查一下数据库价格,并更新到redis。性能稍好,用户体验差。不推荐

方案3:把每个商品的价格放入缓存更新,需要查价的时候查缓存。改价的业务(双写模式)+价格缓存+购物车获取缓存价格。结构:key:skuid,value:价格。就是新增和修改的时候顺便改一下价格缓存,让所有用户都来查询redis。

说白了,就是每次查购物车的时候,单独再查一次价格,雷神专门把商品价格存到redis了,结构:key:skuid,value:价格。速度快,用户体验好,每次都是最新价格。需维护双份数据(一致性问题)。这个方案也没问题。没问题。

方案4:查询购物车的时候,异步查询数据库价格,并且更新到redis。雷神用的这种。当你进入购物车时,信息不一定准确,当你刷新一下,才是你上次进入购物车时异步查询的价格。缺点:用户看到的是伪实时价格,数据库负担大。优点:没有过多设计,用户体验好。

我的理解:雷神这块只保持价格的一致性,需要注意。其实其它字段没必要和数据库一致,因为我发现在京东的购物车中,你点击商品,直接就到商品详情页了,显示的是商品详情数据,在购物车中,敏感的数据就是价格。

商品数量限制功能:if判断一下,在添加购物车的时候,超了就抛异常。

临时购物车1年有效期功能:用户购物车:永久保存。临时购物车:1年。如果临时购物车有过期时间(设置过过期时间)就不操作。如果没有设置过,那就给一年过期时间。在用户查询购物车的时候进行操作。ttl=-1,是永久不过期。你偶尔一登陆就合并购物车了哦。

表字段:cartprice:购物车价格(永远不变)。skuprice:实时价格,如果sku表中的价格改了,才会修改这个价格。

注意:购物车数据是存在redis了,mysql中的表并没有使用哦。

17订单

cart_info表已经搬家到redis了。

order_info:订单信息表。

order_detail:订单明细表

order_detail_activity:优惠活动表。已删除

order_detail_coupon:优惠券表。已删除

payment_info:支付信息表,这个单的支付情况。未来对接支付宝,也需要把支付信息存储到这张表。

order_status_log:订单状态表。未支付,已支付,已发货(出库)

订单模块才四张表。退货表呢?发货表呢?地址表呢?。。。

数据库设计。数据库架构。

阿里手册:如果把所有的订单都存在一张表里,单表超过500w,性能就会急剧下降。

时间维度是只增不减的数据。由于订单需要检索,不止要存储。

分片存储:每个存储节点只存少量数据

分库(分片思想)

微服务开发的情况下已经天然分库

如下图,大量数据在一个库会影响性能,大量数据在一张表也会影响性能。所以既要分库,也要分表。

分片健:根据哪个字段算出存到哪个库,哪个表,这个字段叫分片健。

本项目是有3个数据库实例的。

分库思想是在多个机器下载多个mysql数据库吗?

ai回答:

  • “下载多个 MySQL 数据库” 这个说法不太严谨。
    正确的说法是:在多台机器上分别部署(安装并运行)多个 MySQL 服务端(mysqld)实例,每个实例可以包含一个或多个逻辑数据库(schema)。

  • 每个 MySQL 实例:

    • 独立运行(有自己的进程、内存、磁盘);
    • 存储不同业务或不同数据子集
    • 通过不同 IP + 端口访问(如 192.168.1.10:3306192.168.1.11:3306)。

mysql不支持天然的分库,需要mycat。redis天然支持集群,

一个数据库实例的不同库,可以放在不同的机器吗?

ai回答:一个 MySQL 数据库实例(MySQL Instance)的所有数据库(Database / Schema),必须位于同一台物理机器(或同一个操作系统进程空间)上,无法跨机器存储。该实例下可以创建多个逻辑数据库(Schema)

------

因为用户查询订单都是基于自己的数据进行查询,所以使用分片键使用order_info表的user_id字段最合适。你会发现,只要同一个用户他的所有单都会落到同一个库的同一个表。订单crud请求,一定要带上user_id,否则就会全库全表扫描。

分库的意思实际是分实例吧?

ai回答:

是的!你的理解非常准确

“分库”的本质,就是“分 MySQL 实例”(更严谨地说:是将数据拆分到多个逻辑或物理隔离的数据库实例中)。

本项目是在shangpinhui实例创建的两个库,分别是gmall_order_0和gmall_order_1两个虚拟库,我觉得不对,正确的应该是创建两个数据库实例分别把gmall_order_0和gmall_order_1分别放到两个数据库实例中去。

因为需要分表,gmall_order_0和gmall_order_1库中的order_detail,payment_info,order_status_log三张表本身没userid字段,现在雷神都给加上userid这个字段了。就是必须都要有分片键,然搜索很麻烦。order_info都分表了,其他表也得分。这四张表你想分多少张就多少张,只要你能算出来就行。

面试官问的话就说:有两台机器,一台存的order0库,另一台存的order1库,order0库四张表都分了三张表,order1库也一样,所有的表都有userid字段,userid字段就是分片键。

sharding_jdbc拦截所有订单业务发出去的sql,sharding_jdbc听起来很神秘,内部原理就是拦截,字符串替换。

先导入sharding_jdbc,再配置下面的东西就行。配置时间:第15节,8分钟

本项目数据库架构:两个mysql实例,也就是两个库。分了三张表。

gmall_order0:order_detail_0,order_detail_1,order_detail_2,order_info_0,order_info_1,order_info_2,order_status_log_0,order_status_log_1,order_status_log_2,payment_info_0,payment_info_1,payment_info_2

gmall_order1:同上

注意:当你在master操作库和表时,从机自动就同步了。

这个项目应该是垂直分库分表

查询不带分片键,就会全库全表扫描。增删改不带分片键就会报错。

订单主键生成策略:

订单-雪花算法生成唯一id:

第一个方案:

uuid:由于无序性,导致在插入的时候索引树结构会经常变化,会非常慢。不建议使用。

第二个方案:

每个库每个表设置自增步长。因为稳定增长,索引树就好维护。还好吧,公司挺爱用的。

第三个方案:本项目使用。

雪花算法:

雷神说雪花算法其实就是long。。。。我惊了。雪花也64位,雪花也不用第一位,也是2的63次方。雷神用sharding_jdbc生成的,在yml配的key-generate-strategy。

long类型:8个字节,64位,一位符号位,2的63次方

int类型:4个字节。2的31次方。

long类型:8个字节,一个字节8位,一共64位。因为java是有符号的,所以第一位0代表负数,1代表正数,所以其实是63位。最大值是2的63次方减一。bigint就是这个long。long自增完全够用。

int类型:4个字节。int一般够用。

所以在数据库底层设计自增主键的时候,对于数据量大的数据一定要设置为bigint。

但是很多公司在数据库底层设计自增主键的时候也会使用int,因为反正存更多除了慢没有用,索引数小,查询起来也快。

订单确认页流程:

购物车界面点击结算到跳转到订单确认页。再点击提交订单就可以支付了。

订单确认页数据:商品图片,名字,订单价格,数量。

用户地址。

交易号:对外唯一号,唯一追踪号,防重令牌

总数,总金额

面试官让说泛型就说stream中的reduce。

远程调用购物车数据和用户地址。远程调用ware判断库存是否有库存。

点击提交订单的校验:校验分为基本校验+业务校验。

基本校验:

其实就是加注解,参数加注解,全局异常捕捉异常。还可以自定义校验,雷神没讲。

面试官问hashmap就是用hashmap装异常信息了,代码就在校验异常这里。

等于校验这里是后端进行校验了。

业务校验:

根据业务的特殊性,对核心字段进行校验

比如:用户名是否重复,,,,

-------------------

第一种业务校验:

防重校验:

用户有可能多次点击提交订单按钮,每次都给订单表保存一条记录的话就会有问题。

所以重复提交的时候,只处理一次。

其实在进入提交订单页面的时候,后端已经给前端给了一个uuid,也就是tradeNo。只要页面不刷新,uuid就不会变。你点一万次提交订单也是这个uuid。

具体解决方案:在进入提交订单页面的是时候,不仅后端给前端生成一个uuid,也要在后端把这个uuid放到redis,保存订单的时候,删除redis中的uuid。当第二次点击的时候,redis中就没有了uuid,没有uuid就要进行拒绝,就不能保存到订单表了。

redis有,就删除,执行业务。redis没有就拒绝。

redis结构设计:string类型,key是uuid,值随便,过期时间30分钟。

第一种代码:不能解决并发问题

第二种代码:原子删除uuid。判断有没有uuid和删除uuid必须原子。

把判断和删除都写到脚本中。

第二种业务校验:

校验价格:

面试官问map就说收集所有前端价格和后台实时价格不相等的商品。

远程调用查询实时价格跟前端传来的价格进行比较,自己看代码吧

检验库存:

去结算和提交订单都会查一下。

----------------------

算总价代码:面试官问stream,就说这个

BigDecimal totalAmount = submitVo.getOrderDetailList()
        .stream()
        .map(item -> {
            Integer skuNum = item.getSkuNum();
            BigDecimal orderPrice = item.getOrderPrice(); //已经验过了
            return orderPrice.multiply(new BigDecimal(skuNum.toString()));
        }).reduce((o1, o2) -> o1.add(o2)).get();
//订单总价: 原始金额 - 优惠额度
orderInfo.setTotalAmount(totalAmount);

订单状态:

UNPAID("未支付"),
PAID("已支付" ),
WAITING_DELEVER("待发货"), //库存扣减成功,只剩发货逻辑,物流服务(电子面单【快递鸟、菜鸟】)
WAITING_SCHEDULE("等待调货"), //库存扣减失败,需要从另一个仓库再调货,扣另外仓库的库存
DELEVERED("已发货"),
CLOSED("已关闭"),
FINISHED("已完结") ,
SPLIT("订单已拆分");

订单处理状态:ProcessStatus。更详细,订单状态对应多个订单处理状态。

UNPAID("未支付", OrderStatus.UNPAID),
PAID("已支付", OrderStatus.PAID),
NOTIFIED_WARE("已通知仓储", OrderStatus.PAID),
WAITING_DELEVER("待发货", OrderStatus.WAITING_DELEVER),
STOCK_EXCEPTION("库存异常", OrderStatus.PAID),
DELEVERED("已发货", OrderStatus.DELEVERED),
CLOSED("已关闭", OrderStatus.CLOSED),
FINISHED("已完结", OrderStatus.FINISHED) ,
PAY_FAIL("支付失败", OrderStatus.UNPAID),
SPLIT("订单已拆分", OrderStatus.SPLIT);

发货之后才有物流号

副订单号:就是大单拆成小单

提交订单业务有两次校验,三次保存数据库操作,应该有回滚操作,但是雷神没做,面试应该给面试官讲讲。

17订单2刷

我悟了:分库分表都是在一个机器,放到不同机器就是集群,就是主从了。
本项目不仅分库,而且分表。

mysql结构:一共三个实例没变,一主两从。但是分库分表了,gmall_order库分了2个,库里的所有表都分了3张,改了之后,其他两个实例自然会同步主实例的库和表的变化。

gmall_order库里面所有的表都必须加上userid字段,四张表都加上。

订单数据库层架构:分库分表,读写分离

分库:满足海量数据存储

分表:满足快速查询

余数为0在1库,余数为1在2库,余数为2在3库。

分片:解决数据大容量存储问题

副本:解决单点故障问题

面试官问多少用户?
答:1000多个
面试官:这么点还分库分表,读写分离?
答:不是未现在做的,是为未来做的,到时候用户量上来,再分库分表,再动底层代码,就完蛋了,代码都得改,我们会预估未来5年的数据量。不能只管现在。

分片键:userid

查询不带userid就会引起全库全表扫描

下图是主库负责写,其他两个从库和主库结构一样,负责读。

使用sharding-jdbc分库分表。

sharding-jdbc先计算是哪张表,在计算是哪个库,看是读操作还是写操作,再决定去主库还是从库中的gmall_order_0或者gmall_order_1

sharding-jdbc主要配置的东西:

1主库和从库(订单表主库两个,订单表从库4个,因为一主两从)

2读写分离规则

3分片键

4分片规则(分库分表规则)

18关单

feign远程调用超时问题: 我跳过了。

导入库存系统:

ware_info:仓库表,区域id

ware_sku:仓库里面保存的sku库存量表。warehose_id:仓库id。stock:库存数。stock_locked:库存锁定数,用户下单就把相应的数量给锁住,支付之后就减掉库存,就等于是erp中的挑库。

ware_order_task:库存订单表。      tracking_no,order_id

ware_order_task_detail:库存订单表扣减详情表。        task_id:库存订单表的主键

真实库存数=库存数-挑库数

库存系统没有加入到我们的nacos,所以只能发api请求了。

普通订单允许超卖

秒杀单:不能超卖

京东自营方案:支付后扣库存。

京东商家和淘宝:第三方商家在申请入驻的时候,对库存会有选项:下单锁库存?支付锁库存?(锁和扣要区分)

有些商家得方案:下单锁,支付扣。那个锁的字段可以大过库存数。----本项目的方案

这块跟面试官聊,怎么都可以,逻辑自洽就行。

乐观锁:需要给表加上version字段

定时关单功能:

订单关闭和订单状态改为已支付同时运行,会出现并发问题。加锁。

这块的并发问题会有两种情况:谁先谁后的情况,保证业务正确。

第一种情况:支付:状态改为已支付-------->关单:发现已支付就不用关单。未支付的订单改为已关闭。

第二种情况:关单:订单未支付改为已关闭-------->支付:改为已支付状态

关单的sql:uodate order_info v set v.order__status='close',process_status='close'
where id = xxx

这个并发只会在某个临界点偶尔发生并发风险,悲观锁浪费性能,建议用乐观锁。

version=1 version=2 类似CAS思想的改订单状态:

数据库会自己加锁,同一时刻,只有一个用户修改111这个单。unpaid(未支付)。已支付的单不会被关闭。

利用cas乐观锁写的幂等的sql:利用 CAS思想 实现 轻量级乐观锁 ,幂等性关单

uodate order_info v set v.order__status='close',process_status='close'
where id = xxx and order_status='unpaid' and process_status='unpaid';

接口:

void changeOrderStatusByCAS(List<OrderStatus> expectOrderStatus,--期望的状态
                            List<ProcessStatus> expectProcessStatus,--期望的处理状态
                            OrderStatus orderStatus,--修改后的状态
                            ProcessStatus processStatus,--修改后的状态
                            Long orderId,
                            Long userId--分库分表用的
);

使用的是cas的思想。

CAS思想和乐观锁是一个东西。

定时功能:

延迟任务:如果让线程池执行,断电后订单就关闭不了,任务丢失。

定时任务:sql条件:过期时间<=当前时间。这个肯定不行,时间不准,不怕断电,断电任务不丢失。

雷神没提用redis关单。

redis+定时任务扫描+每天扫描一次数据库进行兜底。京东用的这种方案,我惊呆了。说这是弱实时性需求,zset性价比很高。

rabbitmq-延迟队列实现定时关单(本项目使用这种方式):

direct交换机:绑定唯一的队列。

direct和topic公司用的多一点。

第一种方法:(本项目使用)消息时间维度有序。持久化不怕宕机,消息重新消费机制,集群容错。

给延迟队列设置30分钟过期时间,消息进入延迟队列,待够30分钟后,变成死信,发到死信交换机,再进入普通队列(是个普通队列),再到消费者,执行关单业务。(有可能是消费者监听死信队列,自己查查吧)

第二种方法:(不建议使用)

下面的图少了个交换机,知道就行。给消息单独设置过期时间30分钟,不给延迟队列设置过期时间。

设计标准:每个服务都有且只有一个自己的交换机,负责自己微服务的所有事件。

之前feign远程调用时间太长,服务启动不了,开启feign的重试功能或者放大超时时间就好了。

判断有没有货使用了服务降级,调用失败返回1,表示有货。

保证消息不丢失:

因为是tcp长连接,一旦是网络问题就会抛出异常,可以try catch进行捕捉。java就是socket链接,底层是tcp链接,可以try catch的到。catch中可以保存到数据库一会再发一次。

方案:双端确认机制+队列持久化。这种就确保了消息到broker不丢失,消息在队列不丢失(持久化),消息从broker到消费者不丢失。

项目回调类:MyRabbitConfiguration

消息机制在微服务中叫事件总线

给面试官说在exchange页面,durability为durable(消息要持久化),auto delete为no(不自动删除),internal为no(这个交换机不是内部交换机)。在queues页面,durability也为durable(消息要持久化),auto delete也为no(不自动删除)

内部交换机:接收交换机发送消息的交换机。

确保发送成功:服务器收到,并且存到队列了,才叫发成功。抵达队列才触发confirmcallback.

确保接收成功:消费端消费后返回给broker说成功了,队列才删除消息。

spring:
  rabbitmq:
    host: 192.168.200.100
    port: 5672
    username: admin
    password: admin
    virtual-host: /
    publisher-confirm-type: correlated #发布端进行确认
    publisher-returns: true #发送端发送出消息以后,每个消息有明确的返回
    #发送端发送出消息以后,每个消息有明确的返回
    listener:  #配置消费端
      simple:
        prefetch: 10 #消费端监听器每次拿10条
        acknowledge-mode: manual #手动确认模式。消费端消费后返回给broker说成功了,队列才删除消息

延迟队列和普通队列的区别:

有时候代码里面创建交换机和队列不成功的原因是没有消费者监听

观察下图,过期消息给交换机的时候,路由键发生了变化,变成order.delay

题外话:黑马教程是给消息设置过期时间,而本项目是给队列设置过期时间。不一样。

ready:队列存储数量,

unacked:发给消费者但是消费者未回复的消息

total:

提交订单的时候给mq发消息说要关单,传userid和orderid。

ioexception是网络异常,雷神说的。

消息重复问题:在CloseOrderListener类里面处理的,面试也爱问

            //关闭订单
            orderBizService.closeOrder(msgTo.getOrderId(),msgTo.getUserId());
//            int i = 10/0;
            //当业务出现问题,导致消费失败,返回给MQ == 消息又抵达过来,成为一个无限死循环
            //消息重复:
            //1)、消费失败(业务失败),返回给mq服务器重新入队以后,重新派发过来
            //2)、消费成功(业务成功),没来得及回复,炸了,重新派发过来
            //现象:同一个关单消息被订单关单服务收到很多次
            //解决:
            //  1、一定保证消费消息的业务是幂等的:关单是幂等性操作;
            //  2、判断这个消息是否重复消费的。[业务、redelivered、每个消息有唯一id]
            //  3、有限次尝试。如果有消息一直失败,保存到告警库,然后人工处理
            //
            channel.basicAck(deliveryTag,false);

19.支付

页面要订单id和总金额

第一步:

创建网页移动应用

appid,这个id跟支付宝进行绑定

第二步:

绑定应用

绑定应用所需要的功能

第三步:

配置密钥

要在支付宝配置各种密钥

提交申请审核

第四步:

开发上线

沙箱:支付宝已经帮每一个用户创建好了一个示例应用,这个应用对接支付宝的功能都有。开发期间用这个把功能全部测通过以后。应用上线,只需要少量修改(改一下APP_ID);模拟了支付整个流程,但是不用等审核。

对称加密:加密的密钥和解密的密钥是一样的。

非对称加密:加解密用不同的钥匙

私钥:自己私藏的钥匙叫私钥

公钥:给出去别人用的叫公钥

私钥和公钥是一对钥匙,私钥加密,公钥解密。传得是密文。 

但是以上太重量级,支付宝的方案是加签,验签

加签,验签传得是明文

一共四把钥匙,加签验签

1.下载密钥生成器

2.生成私钥和公钥,私钥自己用。公钥给支付宝,支付宝同时把自己的公钥也给你。

配置:appid,支付宝公钥,商家私钥,支付宝网关地址

支付成功后跳转的url,支付成功发送消息的地址(改订单状态)

下图生成支付二维码流程:

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.33.39.ALL</version>
</dependency>

以后学习读取配置文件就看支付的第十八天第十五节6分41秒。

给面试官说开发的时候用的沙箱,项目上线的时候只更改yaml中的配置即可,比如:appid等等。

private String gatewayUrl; //支付网关
private String appId; //应用id
private String merchantPrivateKey;//商户私钥
private String format; //数据格式化方式 "json"
private String charset;//字符集
private String alipayPublicKey; //支付宝公钥
private String signType; //签名方式 RSA
private String returnUrl; //同步跳转页,当浏览器支付成功以后要跳转的位置(这个是给支付宝的地址,支付宝进行跳转,我们的页面跳到returnUrl)
private String notifyUrl;//异步通知,支付宝支付成功以后要通知我们支付ok

我理解了:为什么要用那个拦截器传递用户信息,因为远程调用,不仅不是一个线程,而且更不是一个应用,根本无法共享用户信息,所以只能带userid参数传递(雷神说太麻烦)或者拦截器。

面试官问map就说支付携带参数那块代码bizContent

面试官问加密就说支付宝的加密。

第一步点击扫码支付按钮,先把二维码弄出来。用户然后扫码支付,支付成功后跳到支付成功页,并且给支付业务发送一条支付成功的消息,支付业务收到成功的消息,必须再次给支付宝返回”successs“这七个字符,负责支付宝会继续给支付业务发送支付成功的消息。这种再次给支付业务发送消息的方案叫做分布式事务-最大努力通知方案最终一致性,这是支付宝的方案,25小时8次,又学到了。

下面:获取全部参数

public String listenpayed(@RequestParam Map<String, String> params)

下面:只获取aaa参数

public String listenpayed(@RequestParam("aaa") Map<String, String> params)

内网穿透:

你给支付宝的notify-url,是你的内网,支付宝访问不到的,需要内网穿透。

开发期间为了能让支付宝异步通知访问到我们的微服务,需要用一个内网穿透,把内部的地址暴露成公共可以访问的东西,

网络上的dns服务器:用来保存域名和ip的对应关系。我们访问百度,会问dns要百度的ip,然后访问百度的ip就好了。支付宝的阿里云服务器想访问我们的域名也是一样,但是dns并没有我们的域名,所以会ping不通。

申请公网ip才可以被访问。

我懂了,内网穿透就是:咱不用申请域名,就用别人申请好的域名,支付宝访问别人申请好的域名,然后别人的域名的服务器把支付宝的数据发送给咱。

自己电脑装一个客户端,跟雷雷科技建立连接。

装内网穿透客户端。雷神用的哲西云内网穿透工具,一个月9块钱。把自己的域名给哲西云,哲西云给你一个外网可以访问的域名。把正经的域名给notify-url,把内网域名给哲西云。

支付成功后,支付宝给notify-url发送参数,支付业务给mq发送消息,订单微服务进行监听,监听到后修改订单状态为已支付。

验签:不验签,谁都能伪装成支付宝发请求。

给面试官说关单和支付成功改订单状态为已支付的单的交换机用的一样。

绿色线代表监听,虚线代表声明

rabbitService.retry(channel,deliveryTag,json,5);//有异常重试5次

给面试官说简单的乐观锁就是加where条件。

如下where条件:因为存在关单和改为已支付的临界点,所以条件如下

关单条件:订单为未支付状态,可以改成已关闭(30分钟到了的情况下)

支付条件:订单为未支付和已关闭状态,可以改成已支付(已支付的情况下)

changeOrderStatusByCAS这是之前的cas方法,雷神自己写的,其实就是把期望值等等写到参数,传到sql进行执行。

void changeOrderStatusByCAS(List<OrderStatus> expectOrderStatus,//--期望的状态
                            List<ProcessStatus> expectProcessStatus,//--期望的处理状态
                            OrderStatus orderStatus,//--修改后的状态
                            ProcessStatus processStatus,//--修改后的状态
                            Long orderId,
                            Long userId//--分库分表用的
);

手动确认:channel.basicAck(deliveryTag,false);

保存支付信息,修改订单状态。

第四节20分钟讲支付大概流程

下单后发30分钟关单的mq,支付后发送改订单状态为已支付的mq。

下单成功后,返回订单id给浏览器,用户点击支付宝支付按钮,请求携带订单id到generateOrderPayPage方法,方法返回支付二维码页面,用户支付后,发送成功消息,同时在发送修改订单状态的mq消息(携带支付宝所有参数)给订单业务。用mq保持分布式事务的一致性

支付-自动收单:

time_expire:订单的绝对超时时间。

这个参数是支付宝的参数。

关于幂等:

在购物车列表页,点击结算

调用库存系统进行扣库存和出库; RPC(在线等)、消息(只需要对方发命令要求做事)。所有用发消息。



扣库存:

也是在支付成功后发送notify-url时候扣库存(其实是锁库存)的。

库存微服务扣减库存(其实是锁库存)后,还会发送一个减库存消息,发给ware_order交换机,然后订单微服务监听对应的队列,若锁库存成功就修改订单状态为等待发货,若扣减失败,修改订单状态为等待调货。根据库存扣减结果修改订单状态。库存发送的两种消息status:已减库存,库存超卖。

            case "DEDUCTED": ps = ProcessStatus.NOTIFIED_WARE; os = OrderStatus.WAITING_DELEVER; break;
            case "OUT_OF_STOCK": ps = ProcessStatus.STOCK_EXCEPTION; os = OrderStatus.WAITING_SCHEDULE; break;

支付成功后才锁库存。

物流服务监听到等待发货状态,生成电子面单(发货单)。再由第三方物流公司揽件。这块就可以对接菜鸟开放平台,菜鸟物流科技这些专门做物流的平台

拆单:

锁库存之前进行checkordersplit(拆单)。检查订单是否需要拆单。

拆单原因:当前订单涉及多个店铺。物品配送性质不同,仓库不同(北京,上海)。

拆单目的:发货的时候可能很多商品走不同的物流:需要一个订单对应一个物流号。一个大订单,被拆分成很多小订单,每个小订单独立配送。

本项目拆单逻辑:按仓库拆单

(各个公司拆单逻辑不一样),库存服务发现不需要拆单,就直接扣减库存,订单服务监听后修改订单状态。如果需要拆单,需要给订单业务发送拆单请求,请求参数:所有商品分布在哪些仓库,参数1:orderid。参数2:map:key:wareid(库存),value:skuid(商品)。

订单业务返回多个子订单,库存再把多个子订单的扣减结果发给订单服务。

等于是订单业务进行拆单,库存业务只发拆单请求。

拆单后,重新插入订单表和订单明细表,并且重新计算商品和总价,并且重新生成订单id。

订单表和拆单后的订单表需要分开吗?

ai:是否需要将原始订单表拆单后的子订单表分开设计,取决于你的业务复杂度、数据一致性要求和系统规模。但在大多数中大型电商或交易系统中,强烈建议分开设计

我感觉还得在订单明细表再加一个订单id,因为主订单表也有可能不用拆单

-- 主订单表(记录用户一次下单行为)
orders (
  order_id       VARCHAR(32) PRIMARY KEY,  -- 主订单号,如 O20260103123456
  user_id        BIGINT,
  total_amount   DECIMAL(10,2),
  status         TINYINT,                   -- 待支付、已取消、已完成等
  create_time    DATETIME,
  ...
)

-- 子订单表(拆单后每个履约单元)
order_items (
  sub_order_id   VARCHAR(32) PRIMARY KEY,  -- 子订单号,如 SO20260103123456_01
  order_id       VARCHAR(32),              -- 关联主订单
  warehouse_id   INT,                      -- 仓库ID(拆单依据)
  logistics_type TINYINT,                  -- 物流方式(快递/自提等)
  amount         DECIMAL(10,2),            -- 子订单金额
  status         TINYINT,                  -- 子订单独立状态(待发货、已签收等)
  ...
)

-- 订单商品明细表(属于子订单)
order_products (
  id             BIGINT PRIMARY KEY,
  sub_order_id   VARCHAR(32),              -- 关联子订单
  product_id     BIGINT,
  sku_id         BIGINT,
  quantity       INT,
  price          DECIMAL(10,2),
  ...
)

20其他

Map<String, SoftReference<ProductDetail>> cache = new ConcurrentHashMap<>();什么意思?

答:ConcurrentHashMap保证多线程并发安全,SoftReference保证内存不足时自动清理缓存

第四讲,10.00分钟看一下。13.51看一下。

第14讲:分布式锁测试成功,实现了下本地锁和分布式锁,15.00讲解分布式锁实现,17.27lua脚本。吊!!!拿while实现阻塞

第15讲:所有的juc锁必须对应上分布式的锁。就像countdownlatch是jvm级别的,还是得实现分布式的。redisson提供了所有锁的分布式版本。

第五节redisson整合,14.05

第十二节13.14,用到反射了。15.01运行时异常。

第十三讲5.06写全局异常

第二讲aop:04:00动代码了

第十讲:aop切面调试成功,又把redis加锁,布隆解决穿透讲了一遍

查询三级分类。查询商品详情。

day9,第十一节,讲解设置注解属性,拼接rediskey,提高aop通用性

day10,第十节,5:28,第十二届导包,24.03配配置文件。

day11,商品上架功能,第二节model包加实体类。第六届商品上架存储到es中。第六届,13.21,上架功能异步编排。第十五届01:43讲日志。

day12,第九节和第十节没看。

day13,网关的match方法不认识。AntPathMatcher。第十三届听不懂。

幂等处理:

21es

商品检索页

导包。设置实体类,注解@Document,@Field(type = FieldType.Text,analyzer = "ik_smart")

设置service,注解:@Repository。参数1:实体类。参数2:主键类型

@Repository
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long>{
}

注入ElasticsearchRestTemplate,调用方法

------------------------------------------

给es设计sku的模型:

基本信息字段,hotscore(热度分),private List<SearchAttr> attrs(平台属性,type=Nested)

存储的字段:核心参与检索的字段+商品检索页需要展示的字段

数据一致性问题:

存的时候最大分词,搜的时候smart分词。雷神全都是smart,雷神应该错了。

综合排序就是热度排序

只有title分词了,也就是商品名字。

上架:往es添加sku商品

下架:删除es中的sku商品

进入商品检索页的两种方式:点击三级分类和搜索框输入关键词进行搜索这两种方式

检索条件:平台属性,综合排序,价格排序,分页

疑问:搜索框的数据应该是text啊,怎么是keyword?

下图是接收前端发送的数据的方式:

er图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值