- 主要是订单服务和购物车服务 秒杀服务 (计网 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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</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(){
this.updateById(category);
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;}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
设置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流返回全部数据即可
-
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));修改数据使用
-
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; //被选中的购物项
-
//积分
-
Integer integration;
-
//订单总额
-
BigDecimal total;
-
//应付价格
-
BigDecimal payPrice;
-
//防重令牌,防止重复下单
-
String orderToken;
}
获取订单,在获取购物项的时候购物项的价格要修改,别的取出来,但是这个价格需要重新查询
在 toTrade()这个方法里远程调用各种需要的数据
1.查询收货地址列表就是直接携带当前拦截器的user用户信息直接去查询
2.查询购物项
需要去购物车模块,查询当前购物车数据,因为购物车模块有自己的拦截器,远程调用不能携带cookie数据所以必须设置一下,不能丢弃了请求头数据信息。
查询出来了redis中的购物项,价格查询的时候需要去product远程服务继续调用查询当前的最新价格。
3.查询用户积分,当前拦截器的user中就存在这个积分信息。可以直接取出来放进去。
feign远程调用不能携带含有sessionId的cookie,所以购物车不能获得session数据,cart认为没有登录,获取不了用户信息。也就获取不了当前用户的购物项数据。
feign的调用过程中,会使用容器中的RequestInterceptor对RequestTemplated的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.计算价格
-
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回滚
锁库存成功后才能开始定时关单