谷粒商城面试重点

2年前 (2022) 程序员胖胖胖虎阿
294 0 0
  • 主要是订单服务和购物车服务 秒杀服务  (计网  os 和 谷粒秒杀)

静态资源放到nginx中,实现动静分离 

谷粒商城面试重点

前端使用thymeleaf开发 引入gav,静态资源放到resource下的templates文件夹下边

在application.yml中导入关闭thymeleaf的缓存

spring:

thymeleaf:

cache:false

  • 查询一级分类(首页内容加载首页就需要加载这些数据)

@GetMapping("/")

public String getIndex(Model model){

List<CategoryEntity> catagories = categoryService.getLevel1Catagories();

return "index";

}

catagoryService 接口

List<CategoryEntity> getLevel1Catagories();

catagoryServiceImpl

@service

List<CategoryEntity> getLevel1Catagories(){

List<CategoryEntity> list  = this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq(“parent_cid”,0));  //查询父id为0的数据集合

return list;

}

三级分类查询,开始是先使用this.baseMapper.selectList(new QueryWrapper<categoryEntity>().eq("parent_id",0或者1或者2));  先查1级分类 根据一级查二级  这样查询的次数太多了

思路:查询表的数据,先查出一级分类,然后stream流遍历查询二级分类(根据stream.collect(toMap(k->k.getCategoryId, v->{

根据k查询二级分类 设置到vo中 重点就是 

设置一个vo承载传到前端的数据 最后记得collect.toList();

})))

一级分类的和二级分类的组合关系

Map<String,List<category2Vo>>   1个一级分类的分类id  对应1个category2Vo的集合

category2Vo属性又有三级分类的类  

Vo属性只需要保存必要信息 父子关系 通过stream流组合到一起 不需要父子id等信息

只需要保存三级分类的结合因为需要返回前端展示

可以考虑将这些分类数据一次性的load到内存中,在内存操作,不用频繁的查DB

@ResponseBody

@GetMapping("/index/json")

public Map<String,List<Catelog2Vo>> getCatelogJson(){

String catalogJSON = redsiTemplate.opsForValue().get("catalogJSON");

if(StringUtils.isEmpty(catalogJSON)){

Map<String,List<Catelog2Vo>>  map = getfromDB();

String json = Json.toJSONString(catalogJSON);  //这里注意要转换为String 

//Json.toJSONString(); 

redisTemplate.opsForValue().set("catalogJSON",json);

return catalogJSON;

}

如果查出来了的话,需要从json转换回来

Map<String,List<Catelog2Vo>> = 

JSON.parseObject(catalogJSON,new TypeReference(Map<String,List<Catelog2Vo>>{}));

//先查出所有数据,查询条件为null 其他的要查询 this.baseMapper(new QueryWrapper<CategoryEntity>().eq("parent_id",0));

  List<CategoryEntity> list = this.baseMapper.selectList(null);  //一表多用的三级分类表

       List<CategoryEntity> level1= getParent_cid(list,0);  //根据filter流完成分类查询

        //遍历各个1级分类和对应的他的二级分类的集合List

//如果是单字段 直接k-k.getCategoryId()  但是v->{ 需要return需要返回的数据}

Map<String, List<Catelog2Vo>> catalogJson =   //根据一级分类List集合来stream遍历

level1.stream().collect(Collectors.toMap(k->k.getCategoryId().toString()),

v->{

List<CategoryEntity> level2 = getParent_cid(list,v.getCategoryId()); //二级分类然后放入到vo返回前端的

//防止查出null 使用stream流进行设置vo字段需要判空

   List<Catelog2Vo> catelog2Vos =

    level2.stream().map(k->{

        Category2Vo category2Vo = new Category2Vo(k.getCategoryId.ToString(),null,k.getcategoryId(),k.getName());

//根据遍历设置vo的值 返回前端

List<CategoryEntity> level3    = getParent_id(list,k.getcategoryId()); //获得当前分类的三级分类

level3.stream().map(k->{

Category2Vo.Category3Vo catelog3Vo = new Category2Vo.Category3Vo(k.getgetCategoryId.toString(),l3.getCategoryId.ToString(),l3.getName;);

}).collect(Collectors.toList());

category2Vo.setcategory3Vo(catelog3Vo);}

return catelog2Vo;

}).collect(Collectors.toList());

});

}

//根据父亲id查找子类的集合 

List<CategoryEntity> getParent_cid(List<CategoryEntity> list, int Parentlevel){

List<categoryEntity> list =

 list.stream().filter(item->item.getParentId()==parentlevel).collect(Collectors.toList());

//使用stream流进行过滤 stream().filter(item->item.getParentId()==0);}


搭建域名访问环境

server块的配置     (配置匹配路径)

谷粒商城面试重点

listen 监听虚拟机的端口号

server_name 请求头的hosts信息 (http1.1才有这个hosts  1.0没有)(网页的请求头信息)匹配才能使用这个跳转 ,如果路径携带 /

proxy_pass  代理到http://gulimall网关的路径位置   这个代理到了自己的电脑ip /static/  代理到自己的虚拟机的具体位置

 谷粒商城面试重点

conf.d 配置反向代理的路径  以及匹配路径

nginx.conf 配置上游服务器的地址 (upstream)

谷粒商城面试重点

 这里就可以转发到网关了,网关配置路由规则,网关这时候要根据请求的host地址进行转发

- id: gulimall_host_route
  uri: lb://gulimall-product
  #负载均衡lb 到这个服务
  predicates:
    - Host=gulimall.com,item.gulimall.com  //根据域名host进行转发断言 转发到具体的模块

网页发送请求携带host:gulimall.com这个到nginx ,代理到网关的时候,会丢失请求头的host信息

然后去转发到网关丢失了数据不能断言

谷粒商城面试重点

设置 proxy_set_header Host  $host ;路由到网关携带host头,来让网关进行断言配置


  • 缓存使用

谷粒商城面试重点

缓存两种:本地缓存 (本地缓存缓存不共享,存在缓存一致性问题)分布式缓存(reids)

 整合redis,需要redis的序列化的配置文件 (序列化机制)转换为String等等

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

spring:

        reids:

                host:192.168.124.130

                        port:6379

综上:整合redis

1.添加spring-boot-starter-data-redis

2.配置host等信息

3.配置序列化配置文件

改造三级分类业务,修改上边的三级分类(先查redis缓存,没有去查数据库)

后边这种使用了@Cacheable直接解决 查不到缓存直接使用查询缓存(读模式下查)


高并发情况下缓存击穿 缓存雪崩 缓存穿透 

注意查出的是JSON字符串,查出来后还需要转换为对象类型(序列化和反序列化);

//

String s = redisTemplate.opsForValue().get("key");

JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>()

缓存穿透:查询一个不存在的数据,缓存不命中,将来查db,也没有将这个查询的null写入缓存,

导致不存在的数据每次都要去db查,

解决:null结果缓存起来,并且加入短暂的过期时间

缓存雪崩:多个键设置了相同的过期时间,导致缓存同时失效,

解决:加上一个随机值

缓存穿透:某个热点key 失效的瞬间,大量的请求请求到了db

解决:加锁

单体应用下加锁,一般设置dcl(双检锁),先去redis查询没有的话去db查询,

这时候设置一个syn锁,然后再加一个查询redis确定一下,防止syn锁中重复查询db的情况

肯那个别的线程同时了sync这个点

这里我们使用了双端检锁机制来控制线程的并发访问数据库。一个线程进入到临界区之前,进行缓存中是否有数据,进入到临界区后,再次判断缓存中是否有数据,这样做的目的是避免阻塞在临界区的多个线程,在其他线程释放锁后,重复进行数据库的查询和放缓存操作。
if (instance == null) {
			synchronized (SingleInstance.class) {
				if (instance == null) {
					instance = new SingleInstance();
				}
			}
		}
		return instance;

//读模式下
//缓存穿透  空的结果也要缓存 :有缓存空数据的功能

//缓存雪崩  同一时间都过期 加上随机时间
//缓存击穿  枷锁
//缓存 redis
//json转换为对应的对象  传出去序列化为json json.tojsonstring  反序列化需要逆转

分布式情况下,分布式锁出现了

分布式锁所得是所有分布式的查询db的线程数量,并且存放到redis中

redis:可以实现分布式锁 set  key value nx  ex+时间  保证了站位+过期时间的原子性

setnx 不设置过期时间的话,不保证原子性的话可能没有设置上过期时间key就不能自动删除了

并且这个value设置为uuid 为了避免删除别人的key,只能删除自己的key

redisTemplate.opsForValue().setIfAbsent("lock",uuid,TimeUnit.SECONDS);

判断+删除        使用redis+Lua脚本(比较uuid的值,如果是自己的uuid那么就删除)

查完数据后 删除锁,删除时候的判断和删除必须保持原子性,因为防止验证成功延迟了会删除了别人的锁

自己设置自旋锁,一直去查询自旋

Integer lock1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);

1.需要保证        站位+过期时间的原子性  判断+删除的原子性  还有锁的自动续期

public Map<String,List<Catelog2Vo>> getWithRedisLock(){

String uuid = UUID.random.toString();B

boolean lock =  redisTemplate.opsForValue().setIfAbsent("lock",uuid,TimeUnit.SECONDS);

set key value nx ex+time

if(lock){

getFromDb(); //锁的是查询数据库的内容

//获取值对比+对比成功删除=原子操作 Lua脚本解锁

 String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            //删除锁 //获取值对比+对比成功删除=原子操作 Lua脚本解锁
            Integer lock1 = redisTemplate.execute(new DefaultRedisScript<Integer>(script, Integer.class), Arrays.asList("lock"), uuid);
            return dataFromDB;
        }else {
            //加锁失败。。。重试。
            //没获取到锁,等待100ms重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return getCatelogJsonFromDBWithRedisLock();//自旋的方式        }

这个自己实现存在自动续期的问题,使用redisson的看门狗机制解决自动续期

/**
 * 使用Redisson分布式锁来实现多个服务共享同一缓存中的数据
 * @return
 */
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedissonLock() {
 
    RLock lock = redissonClient.getLock("CatelogJson-lock");
    //该方法会阻塞其他线程向下执行,只有释放锁之后才会接着向下执行
    lock.lock();
    Map<String, List<Catelog2Vo>> catelogJsonFromDb;
    try {
        //从数据库中查询分类数据
        catelogJsonFromDb = getCatelogJsonFromDb();
    } finally {
       lock.unlock();
    }
 
    return catelogJsonFromDb;
 
}


Redission锁的设置

Rlock lock = redisson.getLock("lock");

try{

lock.lock();

Thread.sleep(3000);}finally{

lock.unlock();}

return "hello";}

redission的看门狗机制

一个lock.lock()了之后 即使出现error没有释放锁,也会后边自动释放锁,因为Redisson会设置一个30s自动过期时间

(1)阻塞式等待。默认的锁的时间是30s。

(2)锁定的制动续期,如果业务超长,运行期间会自动给锁续上新的30s,无需担心业务时间长,锁自动被删除的问题。

(3)加锁的业务只要能够运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

                设置了超时时间后,就会发送给Redis的执行脚本,进行占锁,默认超时就是我们指定的时间。                                lock.lock(10,TimeUnit.SECOND)

关于续期周期,只要锁占领成功,就会自动启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期,续成30s。这个10s中是根据( internalLockLeasTime)/3得到的。

每隔10s看是否执行完,没有执行完就锁续期为30s,运行完了就取消续期。

lock(time,TimeUnit.SECOND);设置10s自动解锁,一定要大于业务执行时间

lock(long leaseTime, TimeUnit unit)存在到期后自动删除的问题,但是我们对于它的使用还是比较多

redisson的读写锁(写排他锁,读共享锁)

RReadWriteLock writeLock = redisson.getReadWriteLock("lock");  //获取锁 设置锁的名字

Rlock lock = writeLock.writeLock();

lock.lock();

redisTemplate.opsForValue().set("writeValue",uuid);

finally{lock.unlock();};

读操作就是把这个write改成read    

读操作时候不能写,只能读,写操作时不能读不能写

countdownLatch()                   计数器,同时运行多个线程

RcountDownLatch door = redisson.getCountDownLatch("door");

door.trySetCount(5);

door.await();  //等待锁

另一个方法接口

RcountdownLatch door = redisson.getCountdownLatch("door");

door.countDown();//计数-1

 CountDownLatch latch = new CountDownLatch(10);
latch.await();

latch.countDown();


实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数器为1的CountDownLatch,并让其他所有线程都在这个锁上等待,只需要调用一次countDown()方法就可以让其他所有等待的线程同时恢复执行。

信号量测试(限流操作)

@GetMapping("/park")

@Response

Rsemaphore park = redission.getSemaphore("park");

park.acquire(); -1

park.tryacquire();  //非阻塞抢占锁

if(true){执行业务}

else {return "error"};

@GetMapping("/go")

RSemaphore park = redisson.getSemaphore("park");

park.release();//释放  +1


redisson  解决了缓存击穿实现了分布式锁

但是缓存一致性没有解决 额

如果查询时候同时修改了三级分类数据,这时候redis中的三级分类都是旧的数据,所以就存在缓存一致性问题

缓存一致性

下边的都是数据更新时候出现了缓存一致性问题

1.双写模式(修改之后直接进行写db 写缓存)

2.采用失效模式(修改之后先删除缓存 然后下次查询的时候查询缓存)【要求高的可以加上读写锁,防止读到假数据】

在2的情况下还需要保证强一致性的话,读写锁(并发度比大锁力度小点)


SpringCache (对应方法实现上直接添加不需要写任何判断逻辑,实现缓存的读取)

1.Cache可以整合reids,想Redisson整合reids 一样都是存数据到redis中的

引入依赖(redis使用的lettuce)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

  1. <dependency>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-data-redis</artifactId>

  4. </dependency>

spring.cache.type = redis  指定选择redis为缓存

@Chcheable  触发数据保存到缓存,查询

@CacheEvict: 触发删除缓存//更改   失效模式

@CachePut 更新缓存 //更改  双写模式

@Caching 组合多个操作

主启动类一般都加@EnableCaching 这种注解,然后使用注解就完成缓存操作

@Cacheable 更新缓存

4.可以指定缓存分区 在注解后边

@Cacheable({"catagory"})

public List<CategoryEntity> getLevel1Categories(){

this.baseMapper.selectList(new QueryWrapper<CategoryEntity>);

return list;

}

已经保存到了redis,再次查的时候已经        直接去缓存中去查询

细节

1.如果缓存中有,方法不会调用

2.key默认自动生成

3.缓存的value默认jdk序列化机制

4.默认ttl时间为-1 表示永远不过期

但是

0我们希望可以指定缓存使用的key,@Cacheable(value={"category"},key=" ' ' ")注意必须加单引号

0指定缓存数据的存活时间

spring.cache.redis.time-to-live=3600000  

重要

0修改缓存序列化机制,自定义缓存配置,这个直接赋值粘贴

配置文件缓存自定义配置

spring.cache.type=redis
 
#设置超时时间,默认是毫秒
 
spring.cache.redis.time-to-live=3600000
 
#设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀
 
spring.cache.redis.key-prefix=CACHE_
 
spring.cache.redis.use-key-prefix=true
 
#是否缓存空值,防止缓存穿透
 
spring.cache.redis.cache-null-values=true


上边的查询三级分类使用了Redisson分布式锁,防止缓存击穿问题,已经存在直接缓存查询,否则查db

@Cahcheable 读模式下的获取数据,已经存在直接缓存查询,否则查db

写模式下,存在修改情况,

@CacheEvict

修改的话双写模式   @CachePut实现双写模式

失效模式    @CacheEvict实现失效模式,触发删除缓存,添加完成后删除缓存没然后下次直接查询缓存

需求:同时删除一级缓存和三级缓存的缓存

@Caching(evict={

@CacheEvict(value={"category"},key=" ' ' "),

@CacheEvict      ---------------------})

@Transactional  //多个操作事务一致性

public void update(){

  1. this.updateById(category);

  2. relationService.updateCategory(category.getCatId(),category.getName());

}

也可以删除同分区下的数据

@CacheEvict(value={"category"},allEntrties = true) //删除value下分区的所有缓存

可以批量删除同一个分区下的缓存,所以一般开启前缀存储

springCache

读模式下:

缓存穿透:springcache下的这个spring.cache.redis.cache-null-values=true

缓存击穿:大量并发查询放好失效的热点key,设置@Cacheable(sync=true)解决读模式下的缓存击穿,异步操作,不能同时查询

缓存雪崩:使用设置随机时间,和缓存击穿一样在配置文件加上了redis.cache-null-values=true

写模式下:缓存一致性的解决

读写锁+失效模式(@CacheEvit)查询删除缓存,读取加上读写锁。

读多写少直接查db

  • 常规数据(读多写少,即时性,一致性要求不高的数据):完全可以使用spring-cache;写模式,只要缓存的数据有过期时间就足够了;

商品详情:点击具体的商品搜索查看用户详情

CSDN别的

1.获取skuinfo的表(商品的具体信息 标题啥的)

2.skuimages 图片信息

3.当前sku的销售属性集合

一个销售属性包含一个集合(销售属性对应的所有值)

4.获取spu的介绍(一张表图片)

5.spu的规格参数信息【存在分组vo-里面有许多个具体的参数+对应的值】

分组- (规格参数,具体的值)也是个集合list


设置vo接收整个页面的属性,设置各个小的板块的vo,然后去查DB根据查询的数据封装到vo,一个套路,关键找清楚list数组的对应关系。

主要是各个查询都是在分布式跨板块的查询,使用异步编排+多线程节省了查询时间

添加线程池配置类,注入到容器中去

@ConfigurationProperties(prefix="gulimall.thread")

@component   //注入ioc容器后就不需要使用 @enableConfigurationProperties了

@data

public class ThreadPoolConfigProperties(){

private Integer Core;

private Integer maxSize;

private Integer keepAliveTime;

}

gulimall.thread.core = 20   //自动转为了 驼峰转换

gulimall.thread.max-size:200

gulimall.thread.keep-alive-time:10

设置线程池的参数配置

package com.atguigu.gulimall.product.config;
 
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
/**
 * @Description: MyThreadConfig
 * @Author: WangTianShun
 * @Date: 2020/11/17 13:31
 * @Version 1.0
 */
//如果ThreadPoolConfigProperties.class类没有加上@Component注解,那么我们在需要的配置类里开启属性配置的类加到容器中
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)

//配置文件中直接使用ioc容器中的配置信息 
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
        return new ThreadPoolExecutor(pool.getCore(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

接下来大招 使用异步编排+线程池实现了属性的快速查询

public SkuItemVo item(Long skuId){

SkuItemVo skuItemVo = new SkuItemVo();

CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(()->

{

getById(skuid);

skuItemVo.setInfo(info);

return info;  //supplyAsync 有返回值的操作  下边可以直接使用

},executor);

 f1 =infoFuture.thenAcceptsync((res)->{

spuInfoService.getById(res.getid);

skuItemVo.setDesc(edsc);

});

CompletableFuture.allOf(f1,f2,f3).get();

return skuItemVo;//返回真个页面的数据

}

认证服务:

springsession解决session的共享问题

@EnableRedisHttpSession

@enableCaching

同样需要配置序列化(默认jdk序列化,修改为json序列化)redis和springcache和springsession

可以设置cookie的作用域和redis的序列化修改

@Configuration
public class GulimallSessionConfig {
 
    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com"); 扩大作用域
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }
 
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

在这个@configuration的配置文件中设置cookie的作用域和redis的序列化机制


购物车

@EnableFeignClients

@enableDiscoveryClient

@springbootApplication(exclude="DatasourceAutoConfiguration.class")

数据模型分析

用户购物车,临时购物车都放入到redis中去,使用redis的hash数据类型

读多写少

使用hash存储购物车

key   field  value

谷粒商城面试重点

 key 表示购物车  field 表示skuid  value表示具体数据

购物项vo采用了充血模型(直接get方法设置乘除法)

购物车 包含购物项

public class Cart{

List<cartItem> cartItems;

//总数量等等  都是用充血模型 直接设置相应的值信息

}

配置redis  和 springsession(解决session的session的共享问题)

spring-session-data-redis

spring-boot-starter-data-redis

添加springsession的配置类

@EnableRedisHttpSession

@Configuration

public class GulimallSessionConfig{

@Bean

public CookieSerializer cookieSerializer(){

        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com"); //设置domiain
        cookieSerializer.setCookieName("GULISESSION"); //设置cookie的name
        return cookieSerializer;

}

  1. @Bean

  2. public RedisSerializer<Object> springSessionDefaultRedisSerializer(){

  3. return new GenericJackson2JsonRedisSerializer();

  4. 设置redis的序列化机制

}

session保留登录信息

ThreadLocal用户身份鉴定

Map<thread,value>

把user-key放到cookie中

1.先在implements Handlerinceptor(){

public static ThreadLocal<UserInfoTo> thread() = new ThreadLocal();

public boolean preHanlder(HttpServletRequest request,HttpserveltResponse response){

1.先查询session中是否有登录信息,然后根据是否存在userid 往userInfo添加信息

UserInfoTo userInfoTo = new UserInfoTo();

HttpSession session = request.getSession();

session.getAttribute(); //从session中去查具体的登录信息

if(member !=null){

userInfoTo.setUserId(member.getId());

}

//查询了userId后 就可以从To查询是否存在userid判断是否为登录用户

Cookie[] cookies = request.getCookies();

if(cookies!=null && cookies.length>0){

for(cookie cookie: cookies){

//从cookie查询是否存在userKey 有的话查询userKey设置到To对象中去,

cookie中没有UserKey(说明第一次登陆)使用uuid设置一个userkey,然后再在下边的handler后方法进行设置这个To对象中的userKey属性

if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){

userInfoTo.setUserKey(cookie.get());

userInfoTo.setTempUser(true);

}

}

这里就是刚才从cookie查询userkey不存在 就去设置一个uuid

执行这个方法说明第一次登录,设置一个临时用户此时tempUser 为false

if(StringUtils.isEmpty(userInfoTo.getUserKey)){

uuid.randomUUID().toString();

userInfoTo.setUserKey(uuid);

}

thread.set(userInfoTo);

return true;

}

  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = thread.get();

说明没有从cookie查询到当前用户的userkey

if(!userInfoTo.isTempUser){

Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey()); //设置cookie 用于前边的那种从cookie查询然后for遍历一个一个比较equals姓名

cookie.setDoman("gulimall.com");

cookie.setMaxAge();

reponse.addCookie(cookie) ; //response设置cookie信息回去

}

}

添加完成了HandlerInterceptor 那么就把它方到容器中

@Configuration

public class GulimallWebConfig implements WebMvcConfigurer(){

public void addIntercrptor(){

register.addInteceptor(new CartInterceptor().addPathPatterns("/**"));}

点击购物车的售后就会触发HandlerInterceptor

@Controller

public class CartController(){
@GetMapping("/cart.html")

public string cartList(){

CartInterceptor.thread.get();//从HandlerInteceptor查询当前用户信息        


完成登录拦截器和userInfoTo的设置完成之后,以及userKey userId 等的设置  可以判断当前用户是哪个,userkey存放到cookie中的 每次发送都携带,第一次需要去后置拦截器设置cookie中去,

后边直接去ThreadlLocal中去查询UserInfoTo的数据,如果存在userid说明寸在用户在登录(设置userid,后边添加商品的时候根据这个id判断为在线购物车),就是在线购物车,其他的就是离线购物车,单是都存在cookie中的userKey的设置

//

添加商品到购物车,开始创建购物车了

}

        添加skuid和数量到购物che

@GetMapping("/addToCart")

public String addTocart(@RequestParam("skuid") Long skuid,

@RequestParam("num") Integer num)

      CartItem cartItem =   cartService.addTocart(skuid,num);

        return cartItem;}

public interface CartServicec{

CartItem addTocart(Long SkuId,Integer num);

}

//实现将商品放入购物车中去

首先查找自己的购物车,根据threadlocal中存储的UserTo去查询当前用户的id key根据是否存在id判断是否为临时用户,在线用户,然后boundhashops,设置一个key field value到hash中去,然后就是去具体的业务。

1.如果当前商品已经存在购物车,只需要增添数量

2.否则需要查询所有vo数据存到redis的hash   key-field value

@Service

public class CartServiceImpl implements cartService{

        private final String CART_PREFIX = "gulimall:cart"; //购物车的前缀

        @autowried

        StringRedisTemplate redisTemplate;

        @Autowired

ProductFeignService productfeignService;

        @autowired

        ThreadPoolExecutor executor;

public CartItem addToCart(Long skuId,Integer num){

}

}

回顾:

/**添加购物车
 * 首先从threadlocal查询当前用户,然后去获取当前的购物车前缀,如果登录了的话有userid,没有登录的话用userKey去当做key去做个连接
 *
 BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
 * 去redis中去查询对应key对应的value也就是hash查出来后parseobject,return bpundhashops;
 *
 * //2.之后去获取了连接,直接get(skuid.ToString())  获取购物项
 * 判断这个购物项是否是新的 ,新的话使用异步编排+线程池去查询db  ,不是新的话就去查询的数据parobject为指定类型,然后去修改数量
 * 最后tostring()重新设置存储回去
 * cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));

接口幂等性问题,防止重复提交数据


添加购物车只是,添加到某个具体的购物车,获取才是要整合购物车数据

获取购物车,前边是添加购物车(添加可以直接判断当前是否登录状态,直接添加到对应的购物车)

获取购物车,可能登录了,之前存在临时购物车,合并两个购物车,并且删除临时购物车数据

1.用户未登录,直接使用user-key获取购物车数据

2.登录 使用userid获取购物车数据,并且合并(临时购物车+用户购物车)购物车

getCart(){

Cart cart = new Cart();

UserInfoTo userInfoTo = ThreadLocal.get();

if(userInfoTo.getUserKey()!=null){//存在用户登录

String cartKey = PREFIX +userInfo.getUserId();

//合并临时购物车,合并完成之后直接删除临时购物车redisTemplate.delete(userkey);

List<CartItem> tempCartItems  = getCartItems(userKEY);

if(tempCartItems!=null){

直接利用上边的添加在线购物车,如果是重复数据就修改数量即可

}

//添加完成之后再去获取登陆购物车的数据

redisTemplate.boundHashOps(cartKey);//在线购物车连接  使用values才能获取全部

}else{//没有用户登录

String cartKey = PREFIX+userInfo.getUserKey();

BoundHashOperations<String,Object,Object> hashOps =  redisTemplate.boundHashOps(cartKey);

List<Objext> values = hashOps.values(); //获取当前key的所有values 对象

if(values!=null&&values.size()>0){

List<CartItem> collect = values.stream().map((obj)->{

//遍历所有的value数据转换回cartitem数据

CartItem cartItem = JSON.parseObject(str,CartItem.class);

return cartItem;

}).collect(Collectors.toList());

}

cart.setItems(cartItems); //设置集合直接返回页面

}

return cart;

}

上边这个方法可以修改成获取购物车里面的所有购物项,只是输入的key不同都是使用values获取所有的数据,然后stream流返回全部数据即可

  1. cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));修改数据使用

  2. cartOps.delete(skuId.toString()); 删除购物项


消息队列和订单服务


订单服务(拦截器拦截,没有登录不能提交订单)

注意新模块添加到注册中心nacos

在主启动类添加@EnableDiscoveryClient注解

spring.application.name: gulimall-order

spring.cloud.nacos.discovery.server-addr = 127.0.0.1:8848

spring-session-data-redis 整合springsession完成session共享问题

spring-boot-starter-reids 使用的是lettuce-core

springsession配置cookie请求头的作用范围和序列化机制的信息

线程池的设置一些信息

订单生成-》支付订单

点击去结算,生成订单


订单服务和购物车服务一样设置interceptor拦截器去拦截当前用户

1.如果登录了(从session中去获取当前登录用户信息)设置threadlocal中

2.没登录直接返回登录页面

******订单确认页数据获取 

@controller

public class OrderWebController{

@Autowried

OrderService orderService;

@GetMapping("/toTrade") 订单确认页数据获取 

public String toTrade(){

OrderConfirmVo confirmVo = orderService.confimOrder();

}

}

设置订单确认页数据VO

public class OrderConfirmVo{

List<MemberAddressVo> address;

List<OrderiItemVo> items; //被选中的购物项

  1. //积分

  2. Integer integration;

  3. //订单总额

  4. BigDecimal total;

  5. //应付价格

  6. BigDecimal payPrice;

  7. //防重令牌,防止重复下单

  8. String orderToken;

}

获取订单,在获取购物项的时候购物项的价格要修改,别的取出来,但是这个价格需要重新查询

toTrade()这个方法里远程调用各种需要的数据

1.查询收货地址列表就是直接携带当前拦截器的user用户信息直接去查询

2.查询购物项

需要去购物车模块,查询当前购物车数据,因为购物车模块有自己的拦截器,远程调用不能携带cookie数据所以必须设置一下,不能丢弃了请求头数据信息。

查询出来了redis中的购物项,价格查询的时候需要去product远程服务继续调用查询当前的最新价格。

3.查询用户积分,当前拦截器的user中就存在这个积分信息。可以直接取出来放进去。


feign远程调用不能携带含有sessionId的cookie,所以购物车不能获得session数据,cart认为没有登录,获取不了用户信息。也就获取不了当前用户的购物项数据。

feign的调用过程中,会使用容器中的RequestInterceptorRequestTemplated的head进行处理,

所以我们导入自定义的RequestInterceptor为请求加上cookie

  • RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现。经过RequestInterceptor处理后的请求如下,已经加上了请求头的Cookie信息

@Bean

public RequestInterceptor requestInterceptor(){

return new RequestInterceptor(){

apply(){

ServletRequestAttributes attributes = RequestContextHolder.getRequestAttributes();

attributes.getRequest().getHeader("Cookie");  //从RequestContextHolder获取requestAttributes获取request获取header的cookie;

requestTemplate.header("cookie",cookie);  给新请求同步了老请求的cookie

}

}

}

在容器中设置自己的RequestInceptor 覆盖相应的requestTemplate.header(“Cookie”,cookie);


feign异步调用请求头丢失问题

  • 由于RequestContextHolder使用ThreadLocal共享数据,所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了。在这种情况下,我们需要在开启异步的时候将老请求的RequestContextHolder的数据设置进去,获取后设置到当前的requestcontextHolder
  • 因为这是不同的thread 所以有不同的requestContextHolder

编写获取订单页的方法

public OrderConfimVo confirmOrder(){

OrderConfirmVo confirmVo = new OrderConfirmVo();

MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

//获取之前的请求,就是串行的情况下是一个RequstContextHolder,每个thread的都不同

RequestAttributes requestAttrbutes = RequestContextHolder.getRequestAttributes();

CompletableFuture.runAsync(()->{

RequestContextHolder.setRequestAttributes(requestAttributes);//设置之前请求thread的请求数据

memberFeignService.getAddress(memberResponseVo.getId());

confirmVo.setAddress(address);

},executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //2、远程查询购物车所有选中的购物项
            System.out.println("cart线程..."+Thread.currentThread().getId());
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
            //feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor: requestInterceptors

CompletableFuture.allOf(getAddressFuture,cartFuture).get();全部完成后get释放

}

这里就是在completable.runasync外边使用RequetContextHolder.getRequestAttributes();获得请求参数

然后在各自的异步编排里面使用 RequestContextHolder.setRequestAttributes(上边查出的这个);

每次都携带这个,因为背的线程丢失了这个requestAttributes();

feign在远程调用之前要构造请求,调用很多拦截器RequestInterceptor interceptor


订单信息(去结算)

查询完购物项数据之后还需要去查询库存信息(thenRunAsync

list.stream().collect(Collectors.toMap(item -> item.getVersion());

list.stream().collect(Collectors.toMap(SkuStockVo::getSkuid,SkuStockVo::getHasStock);


接口幂等性(下单,提交订单)

对同一操作一次多次请求结果是一致的。不会因为多次点击不同的结果

接口幂等性:支付等接口点击

查询db天然幂等,update也是幂等的,插入有唯一主键的话也是幂等的

update tab1 set col1 = col1+1 where col2 = 2 这个update不是幂等的

幂等解决方案:【添加防重令牌】

1.

token方案:验证正确才可以

客户端生成一个token 服务端也有一个 验证是否相等,验证成果服务器删令牌

先删除令牌,然后再执行业务

gettoken 从redis获取

if(serverToken==token){

del(token);

service()}; //因为是先删除token   分布式锁是最后删除锁

保证获取 对比和删除令牌的原子性,

在redis使用lua脚本操作(删除redis中的令牌)

然后才能执行业务(保证了幂等性)

2.各种锁机制

数据库悲观锁(锁主键唯一索引,就是行锁  不是这些字段就是表锁)

乐观锁(加个版本号)

3.字段是唯一索引

4.全局请求唯一id


去结算获取订单的时候【此时选择收货地址】,在执行【就是点击下单】查数据项等业务时加上一个设置令牌(放到redis)传到前端

提交的时候携带这个令牌和服务端的令牌进行比较

设置令牌放到redis                         站锁和设置过期时间保持原子操作

{设置号submitOrderVo()

到去下单页面无序提交需购买的商品,去购物车去取一遍

带上令牌(刚才那个token)

}

订单结算直接去redis查询

下单功能:去创建订单【主要是订单号】,验令牌,验价格,锁库存

@PostMapping("/submit")

public String submitOrder(OrderSubmitVo vo){

orderService.submitOrder(vo);

}

先获取订单页面 (展示各种数据,选择地址信息)然后去下单

去下单返回数据vo

privateOrder

orderService.submitOrder(vo);下单操作最重要的

第一步:验证令牌(盐价格+占库存)

保证获取 对比和删除令牌的原子性,

生成令牌 的 站锁设置过期时间原子性

vo.getOrderToken();  前面获取订单页面设置了一个uuid令牌,现在取这个和现在查询的比较

并且设置进了redis

redisTemplate.get(key); //获取redis的令牌

对比和删除必须原子性    这里特殊加了获取  因为在一起先操作的验证令牌

分布式锁是最后再删除锁的

 // 1、验证令牌【令牌的获取和对比和删除必须保证原子性】
        // 0令牌失败 -1删除成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = submitVo.getOrderToken();
        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId()), orderToken);

去结算之后是去下单(获取订单页后去点击下单,重新查询购物车数据)

原子验证令牌成功后,去执行别的重要操作 占库存啊 构造订单(这里的数据和获取订单信息的数据不同,这里重新去redis查询下数据,防止数据已经更改了)

1.验完令牌去生成订单【这个就是验证令牌成功之后的操作】

public OrderCreateTo createOrder(){

OrderCreateTo createTo = new OrderCreateTo();

//1.用mp的  生成订单号

String orderSn = IdWorker.getTimeId();//生成订单号

OrderEntity entity = new OrderEntity();

entity.setOrderSn(orderSn);

//2.获取收货地址信息远程获取(根据上边从获取订单页查到地址id去远程获取)

//3.获取具体的订单项信息

从cart服务获取订单项,重新获取下

//4.验价格

}

@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
        confirmVoThreadLocal.set(submitVo);
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
        response.setCode(0);
        // 1、验证令牌【令牌的对比和删除必须保证原子性】
        // 0令牌失败 -1删除成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = submitVo.getOrderToken();
        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId()), orderToken);
        if (result == 0L) {
            // 令牌验证失败
            response.setCode(1);
            return response;
        } else {
            // 令牌验证成功 下单 去创建订单 验证令牌 核算价格 锁定库存
            // 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();

    /**
     * 生成一个订单
     * @return
     */
    public OrderCreateTo createOrder(){
        OrderCreateTo createTo = new OrderCreateTo();
        // 1、生成一个订单号
        String orderSn = IdWorker.getTimeId();
        // 创建订单  这里主要设置地址信息 就是除了价格 订单项之外的所有其他数据
        OrderEntity orderEntity = buildOrder(orderSn);
        createTo.setOrder(orderEntity);
        // 2、获取所有的订单项[获取之后用stream流进一步更新为数据vo]
        List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
        createTo.setOrderItems(itemEntities);
        // 3、计算价格、积分等相关
        computePrice(orderEntity,itemEntities);
        return createTo;
    }

1      .生成订单id

2.生成订单实体

3.遍历所有的订单项
            // 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = submitVo.getPayPrice();
            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) <0.01){
                // 金额对比成功
                // 3、保持订单
                saveOrder(order);
                // 4、库存锁定,只要有异常回滚订单数据。订单号,订单项信息(skuId,skuName,num)
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(orderItemVos);
                // TODO 远程锁库存
                R r = wmsFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0){
                    //锁成功了
                    response.setOrder(order.getOrder());
                    return response;
                }else {
                    //锁定失败
                    throw new NoStockException((String) r.get("msg"));
                }
            }else {
                response.setCode(2);
                return response;
            }
        }
//        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId());
//        if (orderToken != null && orderToken.equals(redisToken)){
//            //令牌验证通过
//            redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId());
//        }else {
//            //不通过
//        }
    }

 点击确认   =》点击下单 

订单确认页 主要是当前用户的所有地址都远程调用出来

异步调用所有的购物项信息-》thenRunAsync之后调用购物项对应的库存    (调用库存系统查询所有的库存信息)

查询用户积分(直接threadlocal信息中查出来了)

设置防重令牌,当点击了订单确认页设置一个uuid防重令牌(redis的占用和设置过期时间必须院子原子性操作)

点击下单了,直接去判断是否重复下单(通过防重令牌)

谷粒商城面试重点

 订单提交vo

OrderSubmitVo(封装订单提交的数据)

从订单确认页获取地址id  防重令牌(和reids中的去对比) 所有的价格 订单备注

subimitOrder(){

1.SubmitOrderResponseVo  responseVo = orderService.submitOrder(OrdersubmitVo);

然后判断传出来的错误

}

responseVo{

OrderEntity order;

Integer code;

}

submitOrder(OrderSubmitVo submitVo){

SubmitOrderResponseVo vo = new SubmitOrderResponseVo();

验证令牌(首先查出来redis中的数据,然后直接比较,成功直接删除)

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = submitVo.getOrderToken();
        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVO.getId()), orderToken);
        if (result == 0L) {
            // 令牌验证失败
            response.setCode(1);
            return response;}

else{

//令牌验证成功  下单【创建订单,获取价格,锁定库存】

OrderCreateTo  to = createOrder(); //创建订单,订单项等信息【重新查询redis购物车,防止修改了价格】

获得订单的价格【得到所有的价格总和】

验证价格完成后,【保存订单锁定库存

}

}

OrderCreateTo{

OrderEntity orderEntity;

List<OrderItemEntity> orderItems;

订单应付的价格

//运费

}

createOrder(){

OrderCreateTo createTo = new OrderCreateTo();

//1.生成一个订单号

String orderSn = IdWorker.getTimeId();

//2.创建订单

Orderentity orderEntity = buildOrder(orderSn);

createTo.setOrder(orderEntity);

//3.获取所有的订单项

 List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);

createTo.setOrderItems(itemEntities);

//4.计算价格

  1. return createTo;

}

buildOrder  //创建订单【设置订单号,用户id,地址信息id,运费信息,收货人信息【远程调用地址等收货人信息。设置订单状态】】

获取所有的订单项【orderItemTo】

1.获取所有的购物车信息,创建订单项

stream.map重新设置为OrderItemEntity{

OrderItemEntity orderItemEntity = new OrderItemEntity();
        // 1 订单信息 订单号
        // 2 SPU信息
        Long skuId = cartItem.getSkuId();
        R r = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>(){});
        orderItemEntity.setSpuId(data.getId());
        orderItemEntity.setSpuBrand(data.getBrandId().toString());
        orderItemEntity.setSpuName(data.getSpuName());
        orderItemEntity.setCategoryId(data.getCatalogId());
        // 3 SKU信息
        orderItemEntity.setSkuId(cartItem.getSkuId());
        orderItemEntity.setSkuName(cartItem.getTitle());
        orderItemEntity.setSkuPic(cartItem.getImage());
        orderItemEntity.setSkuPrice(cartItem.getPrice());
        String skuAttrs = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";"); //将集合转换成字符串
        orderItemEntity.setSkuAttrsVals(skuAttrs);
        orderItemEntity.setSkuQuantity(cartItem.getCount());
        // 4 优惠信息 [不做]
 
        // 5 积分信息
        orderItemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        orderItemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        // 6 订单项的价格信息
        orderItemEntity.setPromotionAmount(new BigDecimal("0"));
        orderItemEntity.setIntegrationAmount(new BigDecimal("0"));
        orderItemEntity.setCouponAmount(new BigDecimal("0"));
        // 当前订单项的实际金额
        BigDecimal origin = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));
        // 总额减去各种优惠后的价格
        BigDecimal subtract = origin.subtract(orderItemEntity.getCouponAmount()).subtract(orderItemEntity.getIntegrationAmount()).subtract(orderItemEntity.getPromotionAmount());
        orderItemEntity.setRealAmount(subtract);
        return orderItemEntity;

}

最后获得To信息并且,没有错误直接保存订单

    /**
     * 保存订单数据
     * @param order
     */
    private void saveOrder(OrderCreateTo order) {
        OrderEntity orderEntity = order.getOrder();
        orderEntity.setModifyTime(new Date());
        this.save(orderEntity);
        List<OrderItemEntity> orderItems = order.getOrderItems();
        orderItemService.saveBatch(orderItems);
    }

库存锁定,只要有异常回滚订单数据。订单号,订单项信息(skuId,skuName,num)

锁定库存-》商品尝试锁定库存-》全部锁定成功后-》修改全部的锁定状态

保存完订单后直接锁定库存

 WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();

//设置一个vo然后去远程调用库存服务
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(orderItemVos);
                // TODO 远程锁库存
                R r = wmsFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0){
                    //锁成功了
                    response.setOrder(order.getOrder());
                    return response;
                }else {
                    //锁定失败
                    throw new NoStockException((String) r.get("msg"));
                }
            }else {
                response.setCode(2);
                return response;

使用事务去锁定这个锁定库存操作

public Boolean orderLockStock(WareSkuLockVo vo) {

//获得所有商品

List<OrderItemVo> locks = vo.getLocks();

首先跟传过来的vo去查询那个仓库存在足够的库存,转换为List<Long>

//然后去遍历寻找随便一个去修改库存数据

提交订单后才会去锁定库存等信息

远程调用 本地事务不能回滚


分布式事务:最大原因  网络原因+分布式

seata的 AT分布式事务【相当于串行加锁了额】 @GlobalTransactional 使用于不是高并发的场景

高并发场景的分布式事务不能用seata

1.失败后可以发消息队列完成最终一致性


rabbitmq的延时队列

下订单  -》30分钟未支付 【定时任务,db压力很大】关闭订单

锁库存        -》40分钟后检查订单不存在了解锁库存

库存自动解锁

spring-boot-starter-amqp

spring.rabbitmq.host=

spring.rabbitmq.virtual-host=

主启动类添加注解

@EnableRabbit

@EnableFeignClients

@EnableDiscoveryCilent

添加rabbitmq的配置类信息

@Configuration

public class MyRabbitConfig{

1.使用JSON序列化机制,进行消息转换

@Bean

public MessageConverter messageConverter(){

return new Jackson2JsonMessageConverter();

直接在配置文件创建1个交换机2个队列有个延迟队列 一个消费者时刻监听的队列

两个binding连接 交换级和队列

}

}


库存的自动解锁添加一个中间表(库存工作单),记录仓库id和当前的状态

下订单首先使用本地事务用于回滚,还有延迟队列实现远程调用的回滚

public Boolean orderLockStock(WareSkuLockVo wareSkuLockVo){

1.//首先保存一个 订单号和 订单日期的信息,用于存根

然后遍历WareSkuLockVo 把所有skuid和 count 保存到一个数组,并且根据数量和skuid查询到你那个能使用的wareid;放入到一个vo中保存为list,出现错误使用了本地事务回滚

然后遍历所有的vo集合

去使用占库存的方法,使用update方法 如果返回0的话占用失败,返回1的话

保存工作单详情,防止锁定失败,保存这个工作单和上边的那个只有skuid和count的表相关联

之后发送延迟队列(发送的To包括工作单id+这个工作单详情的所有信息)

rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);

发送给了延迟队列 过期后发送给了最后那个被消费者连接的队列

2.监听队列

监听第二个队列,通过监听实现库存解锁,

  • 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队 

        @component

@RabbitmqListener(queues={"stock.release.stock.queue"})

public clas StockReleaseListener{

@RabbitHandler

void LockRelease(StockLockedTo stockLockedTo, Message message, Channel channel){

try{

wareSkuService.unlock(stockLockedTo); //解锁的主要逻辑

channle.basicAck();

}catch(){

//如果抛出异常,使用第二个回滚消息

channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);

}

}

}

库存解锁

如果订单详情不为空,库存锁定成功

        查询最新的订单状态,去第一个订单库存表查询,有个状态字段

从detail详情表查出订单库存表id,然后查询他的状态

如果为null||订单已经取消 解锁库存

别的抛出异常 手动ack reject回滚

锁库存成功后才能开始定时关单

版权声明:程序员胖胖胖虎阿 发表于 2022年10月19日 下午11:00。
转载请注明:谷粒商城面试重点 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...