105 初步检索
105.1 _cat
GET /_cat/nodes : 查看所有节点 GET /_cat/health : 查看es健康状况 GET /_cat/master : 查看主节点 GET /_cat/indices: 查看所有索引
讯享网
107 乐观锁字段
_seq_no :并发控制字段 , 每次更新就会 + 1 , 用来做乐观锁
_primary_term : 同上,主分片重新分配 , 如重启 , 就会变化.
107.1 并发修改
发送两次修改同时对同一文档进行修改 , 为了控制并发就可以加上if_seq_no=1&if_primary_term=1 就可以修改
讯享网PUT http://xxx.xxx.xxx.xxx/index/type/1?if_seq_no=1&if_primary_term=1 { "name":1 }
PUT http://xxx.xxx.xxx.xxx/index/type/1?if_seq_no=1&if_primary_term=1 { "name":1 }
113.match_phrase短语匹配
将需要匹配的值当成一个整体单词,(不分词)进行检索
讯享网GET bank_search { "query";{ "match_phrase":{ "address":"mill road" } } }
128.sku在es中存储模型分析
es DSL
PUT product { "mappings": { "properties": { "skuId": { "type": "long" }, "spuId": { "type": "keyword" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "skuPrice": { "type": "keyword" }, "skuImg": { "type": "keyword", "index": false, "doc_values": false #不可聚合等操作 }, "saleCount": { "type": "long" }, "hasStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catalogId": { "type": "long" }, "brandName": { "type": "keyword", "index": false, "doc_values": false }, "brandImg": { "type": "keyword", "index": false, "doc_values": false }, "catalogName": { "type": "keyword", "index": false, "doc_values": false },"attrs": { "type": "nested", #数组扁平化处理 "properties": { "attrId": { "type": "long" }, "attrName": { "type": "keyword", "index": false, "doc_values": false }, "attrValue": { "type": "keyword" } } } } } }
冗余存储的字段不需要参与检索 , 所以"type"和"doc_value"设置成false
132.R-泛型结果封装
132.1 从List中获取值
如果List里面封装的不是基本类型 , 并且引用类型(对象)里面有非重字段(对应数据库中entity的主键id),如果想要从里面快速的获取数据可以封装成Map.
对象
讯享网@Data public class SkuHasStockVo { private Long skuId; #主键Id 唯一 private Boolean hasStock; }
将对象集合封装成Map
try { R skuHasStock = wareFeignService.getSkuHasStock(skuIdList); List<SkuHasStockVo> data = (List<SkuHasStockVo>) skuHasStock.get("data"); stockMap = data.stream().collect(Collectors.toMap( skuHasStockVo -> skuHasStockVo.getSkuId(), //key skuHasStockVo -> skuHasStockVo.getHasStock()//value )); }catch (Exception e){ log.error("库存服务查询异常:原因{}",e); }
在一层循环中,就不必在遍历对象集合,即根据主键Id,查找自己封装的Map
讯享网 List<SkuEsModel> upProduct = skus.stream().map(sku -> { //...... //TODO : 库存 if(finalStockMap == null){ esModel.setHasStock(true); }else{ esModel.setHasStock(finalStockMap.get(sku.getSkuId())); //根据自己封装的Map找到对应的Value,而不是循环遍历查找 } return esModel; }).collect(Collectors.toList());
132.2 远程调用的失败Catch
远程调用可能存在失败,需要自己手动处理远程调用失败的情况
Map<Long, Boolean> stockMap = null; try { R skuHasStock = wareFeignService.getSkuHasStock(skuIdList); List<SkuHasStockVo> data = (List<SkuHasStockVo>) skuHasStock.get("data"); stockMap = data.stream().collect(Collectors.toMap( skuHasStockVo -> skuHasStockVo.getSkuId(), skuHasStockVo -> skuHasStockVo.getHasStock() )); }catch (Exception e){ log.error("库存服务查询异常:原因{}",e); }
135.TypeReference
对于远程调用返回R对象想要得到具体的类型不用强转,利用fastjson的TypeReferece
R
讯享网 //利用fastjson进行逆转 public <T> T getData(TypeReference<T> typeReference){ Object data = this.get("data"); //默认是map String s = JSON.toJSONString(data); T t = JSON.parseObject(s, (Type) typeReference); return t; }
从R中获取具体的类型
R r = wareFeignService.getSkuHasStock(skuIdList); TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {}; stockMap = r.getData(typeReference).stream().collect(Collectors.toMap( skuHasStockVo -> skuHasStockVo.getSkuId(), skuHasStockVo -> skuHasStockVo.getHasStock() ));
137.渲染一级分类数据
137.1 thymeleaf 名称空间
讯享网<html lang="en" xmlns:th="http://www.thrmeleaf.org">
137.2 快速编译页面
快捷键 : ctrl+shift+f9
139.搭建域名访问环境一
server { listen 80; server_name gulimall.com; location / { proxy_pass http://192.168.31.57:7000; #window本机内网地址 } }
140.搭建域名访问环境二-负载均衡到网关
140.1 nginx conf
http块
讯享网 upstream gulimall { server 192.168.31.57:88; }
server块
server { listen 80; server_name gulimall.com; location / { proxy_pass http://guilimall; } }
140.2 gateway-applicatoin.yml
一定要放在所有路由的最后面
讯享网 - id: gulimall-host-route #renrenfast 路由 uri: lb://product-service predicates: - Host=.gulimall.com,gulimall.com
140.3 nginx代理给网关的问题
nginx代理给网关的时候,会丢失请求的host信息.
server { listen 80; server_name gulimall.com; location / { proxy_set_header Host $host; #设置上host头 proxy_pass http://guilimall; } }
145.JvisualVM
启动 :
cmd -> jvisualvm
145.1线程
![]()
运行 : 正在运行的
休眠 : sleep
等待 : wait
驻留 : 线程池里面的空闲线程
监视 : 阻塞的线程,正在等待锁
146 中间件对性能的影响
146.1 docker 监控容器状态
讯享网docker stats
150.优化三级分类数据获取
150.1 Version Origin
不断的嵌套与数据库交互,导致IO开销太大,频繁的网络交互导致接口的性能非常差劲!
@Override public Map<Long, List<Catelog2Vo>> getCatalogJson() { //1.查出所有1级分类 List<CategoryEntity> level1Categorys = this.getLevel1Categorys(); //2.封装数据 Map<Long, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap( k -> k.getCatId(), v -> { List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId())); List<Catelog2Vo> catelog2Vos = null; if (categoryEntities != null) { catelog2Vos = categoryEntities.stream().map(l2 -> { Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName()); //找当前二级分类的三级分类封装成vo List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.getCatId())); if(level3Catelog != null){ List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> { //2.封装成指定格式 Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName()); return catelog3Vo; }).collect(Collectors.toList()); catelog2Vo.setCatalog3List(collect); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; } )); return parent_cid; }
150.2 Version X
将数据库的多次查询变为1次 , 如果再有根据父Id查找子分类的需求 , 直接在本地的集合中去找.
讯享网 @Override public Map<Long, List<Catelog2Vo>> getCatalogJson() { List<CategoryEntity> selectList = baseMapper.selectList(null); //1.查出所有1级分类 List<CategoryEntity> level1Categorys = selectList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).collect(Collectors.toList()); //2.封装数据 Map<Long, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap( k -> k.getCatId(), v -> { List<CategoryEntity> categoryEntities = findSonCategory(selectList,v.getCatId()); List<Catelog2Vo> catelog2Vos = null; if (categoryEntities != null) { catelog2Vos = categoryEntities.stream().map(l2 -> { Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName()); //找当前二级分类的三级分类封装成vo List<CategoryEntity> level3Catelog = findSonCategory(selectList,l2.getCatId()); if(level3Catelog != null){ List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> { //2.封装成指定格式 Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName()); return catelog3Vo; }).collect(Collectors.toList()); catelog2Vo.setCatalog3List(collect); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; } )); return parent_cid; }
156 .锁-解决缓存击穿问题
让高并发情况下,一个服务实例只访问一次数据库.
156.1 version origin

getCatalogJSONFromDB
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() { synchronized (this){ //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(!StringUtils.isEmpty(catalogJSON)){ //缓存不为null直接返回 Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } System.out.println("查询了数据库......"); //查db List<CategoryEntity> selectList = baseMapper.selectList(null); //1.查出所有1级分类 List<CategoryEntity> level1Categorys = selectList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).collect(Collectors.toList()); //2.封装数据 Map<String , List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap( k -> k.getCatId().toString(), v -> { List<CategoryEntity> categoryEntities = findSonCategory(selectList,v.getCatId()); List<Catelog2Vo> catelog2Vos = null; if (categoryEntities != null) { catelog2Vos = categoryEntities.stream().map(l2 -> { Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName()); //找当前二级分类的三级分类封装成vo List<CategoryEntity> level3Catelog = findSonCategory(selectList,l2.getCatId()); if(level3Catelog != null){ List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> { //2.封装成指定格式 Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName()); return catelog3Vo; }).collect(Collectors.toList()); catelog2Vo.setCatalog3List(collect); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; } )); return parent_cid; } }
@Override
getCatalogJSON
讯享网@Override public Map<String, List<Catelog2Vo>> getCatalogJson() { String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(StringUtils.isEmpty(catalogJSON)){ //缓存中没有,查询数据库 System.out.println("缓存不命中.....查询数据库"); Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); //查到的数据再放入缓存,将对象转为json放在缓存中 String s = JSON.toJSONString(catalogJsonFromDb); redisTemplate.opsForValue().set("catalogJSON", s,1, TimeUnit.DAYS); return catalogJsonFromDb; } System.out.println("缓存命中.....直接返回"); Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {}); return result; }
156.2 version X
将老三样三个动作原子性放入同步代码块中.
将数据放入缓存的操作放入同步代码块
//查到的数据再放入缓存,将对象转为json放在缓存中 String s = JSON.toJSONString(catalogJsonFromDb); redisTemplate.opsForValue().set("catalogJSON", s,1, TimeUnit.DAYS);
getCatalogJSONFromDb
讯享网public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() { synchronized (this){ //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(!StringUtils.isEmpty(catalogJSON)){ //缓存不为null直接返回 Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; } System.out.println("查询了数据库......"); //查db List<CategoryEntity> selectList = baseMapper.selectList(null); //1.查出所有1级分类 List<CategoryEntity> level1Categorys = selectList.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).collect(Collectors.toList()); //2.封装数据 Map<String , List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap( k -> k.getCatId().toString(), v -> { List<CategoryEntity> categoryEntities = findSonCategory(selectList,v.getCatId()); List<Catelog2Vo> catelog2Vos = null; if (categoryEntities != null) { catelog2Vos = categoryEntities.stream().map(l2 -> { Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName()); //找当前二级分类的三级分类封装成vo List<CategoryEntity> level3Catelog = findSonCategory(selectList,l2.getCatId()); if(level3Catelog != null){ List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> { //2.封装成指定格式 Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(),l3.getCatId().toString(),l3.getName()); return catelog3Vo; }).collect(Collectors.toList()); catelog2Vo.setCatalog3List(collect); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; } )); //查到的数据再放入缓存,将对象转为json放在缓存中 String s = JSON.toJSONString(parent_cid); redisTemplate.opsForValue().set("catalogJSON", s,1, TimeUnit.DAYS); return parent_cid; } }
@Override
getCatalogJSON
@Override public Map<String, List<Catelog2Vo>> getCatalogJson() { String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(StringUtils.isEmpty(catalogJSON)){ //缓存中没有,查询数据库 System.out.println("缓存不命中.....查询数据库"); Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); return catalogJsonFromDb; } System.out.println("缓存命中.....直接返回"); Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {}); return result; }

158.分布式锁原理以及使用
总结 : 加锁(setnx , set expire time)的时候保证步骤是原子化的,解锁(根据key查看锁,删除锁)的时候保证步骤是原子化的
利用Lua脚本保证,查看锁和删除锁的动作是原子性的
讯享网 / * 获取二三级分类 , 从数据库中查找 , 并封装分类数据 , 用redis分布式锁 * @return */ public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1.占分布式锁 , 去redis占坑 String uuid = UUID.randomUUID().toString(); Boolean lock = redisTemplate.opsForValue().setIfAbsent("local", uuid,300,TimeUnit.SECONDS); if(lock == true){ //lua 脚本解锁 Map<String, List<Catelog2Vo>> dataFromDb; try { dataFromDb = getDataFromDb(); }finally { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid); } return dataFromDb; }else { //加锁失败 ... 重试 try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDbWithRedisLock(); //自旋的方式 } }
159.分布式锁 Redission
@GetMapping("/hello") public void hello(){ //1.获取一把锁,只要锁的名字一样,就是同一把锁 RLock lock = redisson.getLock("my-lock"); //2.加锁,阻塞式等待,只要没拿到,就等待锁 lock.lock(); try { System.out.println("加锁成功,执行业务=>"+Thread.currentThread().getId()); TimeUnit.SECONDS.sleep(30); }catch (Exception e){ }finally { //3.解锁 System.out.println("解锁=>"+Thread.currentThread().getId()); lock.unlock(); }
Redission自己封装的分布式锁框架,解决了一些问题
锁的自动续期,如果业务执行时间超长,运行期间自动给锁续上新的30s,不用担心由于业务执行时间长,从而导致锁自动过期删除的问题
加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s(默认)之后自动删除
161.WatchDog
161.1 lock.lock(20,TimeUnit.SECONDS);
手动设置锁的过期时间之后,不会自动续期,所以自动解锁的时间设置一定要大于业务的执行时间.
161.2 **实践
明显的设置锁的过期时间,省掉了整个续期操作,手动解锁.
162.读写锁
162.1 写锁
讯享网@GetMapping("/write") public String writeValue(){ String s = ""; RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); RLock wlock = lock.writeLock(); wlock.lock(); try { s = UUID.randomUUID().toString(); TimeUnit.SECONDS.sleep(30); redisTemplate.opsForValue().set("writeValue", s); } catch (InterruptedException e) { e.printStackTrace(); }finally { wlock.unlock(); } return s; }
162.2 读锁
@GetMapping("/read") public String readValue(){ RReadWriteLock lock = redisson.getReadWriteLock("rw-lock"); RLock rlock = lock.readLock(); String s = ""; rlock.lock(); try { s = redisTemplate.opsForValue().get("writeValue"); }finally { rlock.unlock(); } return s; }
162.3 补充
只要有写锁的存在,都必须等待
163.Semaphore-信号量
bash
讯享网set park 3
acquire 占一个车位
@GetMapping("/park") public String park() throws InterruptedException { RSemaphore park = redisson.getSemaphore("park"); park.acquire();// 获取一个信号量,获取一个值,占一个车位 return "acquire ok"; }
release 释放一个车位
讯享网 @GetMapping("/go") public String go(){ RSemaphore park = redisson.getSemaphore("park"); park.release(); return "release ok"; }
164.countDownLatch-闭锁
164.1 set
@GetMapping("/lockDoor") public String lockDoor() throws InterruptedException { RCountDownLatch countDownLatch = redisson.getCountDownLatch("door"); countDownLatch.trySetCount(5); countDownLatch.await(); return "放假了....."; }
164.2 countDown
讯享网 @GetMapping("/gogogo/{id}") public String gogogo(@PathVariable("id") Long id){ RCountDownLatch countDownLatch = redisson.getCountDownLatch("door"); countDownLatch.countDown(); return id+"班的人都走了"; }
166.缓存一致性
166.1 利用Redision加锁
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissionLock() { RLock lock = redisson.getLock("catalogJson-lock"); lock.lock(); Map<String, List<Catelog2Vo>> dataFromDb; try { dataFromDb = getDataFromDb(); }finally { lock.unlock(); } return dataFromDb; }
166.2 缓存一致性-双写模式

由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现了不一致
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
166.3 缓存一致性-失效模式

166.4 缓存一致性-解决方案
我们系统的一致性解决方案 :
缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
对于不经常写的数据,读写数据的时候,加上分布式的读写锁
168. 整合SpringCache
168.1 依赖
讯享网 <!--springCache--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--redis template--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
168.2 配置
spring: cache: type: redis
168.3 启动
讯享网@EnableCaching
168.4 hello-springCache
测试使用缓存
/ * 获取一级分类 * @return */ @Cacheable({"category"}) @Override public List<CategoryEntity> getLevel1Categorys() { List<CategoryEntity> categoryEntities = baseMapper.selectList( new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }

169 注解
169.1 @Cacheable
@Cacheable : 触发将数据保存到缓存的操作,如果缓存中有,则方法不用调用.如果缓存中没有,会调用方法,最后将方法的结果放入缓存.每一个缓存的数据我们都来指定要放到哪个名字的缓存[缓存的分区(按照业务类型分),便于管理]
169.1.1 默认行为 :
① 如果缓存中有,方法不会调用
② key默认自动生成 =>缓存的名字 :: SimpleKey[] (自主生成的key值)
③ 缓存的value值,默认使用jdk序列化机制,将序列化的数据存入Redis
④ 默认ttl时间 : -1 (永不过期)
169.1.2 自定义配置
① 指定生成的缓存使用key : key属性指定,接受一个SpEL表达式
讯享网/ * 获取一级分类 * @return */ @Cacheable(cacheNames = {"category"} , key = "'level1Categorys'") @Override public List<CategoryEntity> getLevel1Categorys() { List<CategoryEntity> categoryEntities = baseMapper.selectList( new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }
@Cacheable(cacheNames = {"category"} , key = "#root.method.name")
② 指定缓存的数据的存活时间 , 在配置文件中配置
讯享网spring: cache: type: redis redis: time-to-live:
③.① 将数据保存为json格式 , version Orign 会使配置文件中的配置失效
@Configuration @EnableCaching public class MyCacheConfig { / * 配置文件会失效 * @return */ @Bean RedisCacheConfiguration redisCacheConfiguration(){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith( RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return config; } }
③.② 注入CacheProperties.class , 让配置文件配置项生效
讯享网@EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { @Autowired private CacheProperties cacheProperties; / * 配置文件会失效 * @return */ @Bean RedisCacheConfiguration redisCacheConfiguration(){ CacheProperties.Redis redisProperties = cacheProperties.getRedis(); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith( RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
169.1.3 其他配置
spring: cache: type: redis redis: time-to-live: key-prefix: CHCHE_ #前缀 use-key-prefix: true #是否使用前缀 cache-null-values: true #是否缓存空值,应对缓存穿透

169.2 @CacheEvict
@CacheEvict : 触发将数据从缓存删除的操作.
@Caching 组合以上多个操作.
讯享网@CacheEvict(cacheNames = {"category"},allEntries = true) //删除某个分区下的所有数据
或
@Caching(evict = { @CacheEvict(value = "category",key = "'getLevel1Categorys'"), @CacheEvict(value = "category",key = "'getCatalogJson'") })
讯享网 / * 计量更新所有的关联数据 * @param category */ //@CacheEvict(cacheNames = {"category"},allEntries = true) @Caching(evict = { @CacheEvict(value = "category",key = "'getLevel1Categorys'"), @CacheEvict(value = "category",key = "'getCatalogJson'") }) @Override @Transactional public void updateCascade(CategoryEntity category) { categoryDao.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(),category.getName()); }
@CachePut : 不影响方法执行更新缓存. 用于双写模式
@CacheConfig : 在类级别共享缓存的相同配置.
172.SpringCache-原理与不足
读模式
缓存穿透 : 查询一个Null数据=>解决:缓存空数据;ache-null-values=true
缓存击穿 : 大量并发进来查询一个正好过期的数据.解决:加锁:?
缓存雪崩 : 大量key同时过期.解决:加随机时间.加上过期时间 : spring.cache.redis.time-to- live
写模式(缓存与数据库一致)
读写加锁
引入Cannl,感知Mysql的更新去更新数据库
读多写多,直接去数据库查询即可
172.1 关于数据加分布式锁问题
SpringCache默认是无加锁的.所以要加锁有两种解决办法.
第一种是手写缓存逻辑.
第二种是在注解上加上配置,但是实现的是本地锁
sync = true
讯享网@Cacheable(cacheNames = {"category"} , key = "#root.method.name",sync = true) @Override public List<CategoryEntity> getLevel1Categorys() { List<CategoryEntity> categoryEntities = baseMapper.selectList( new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }
172.总结
常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache,写模式只要缓存的数据有过期时间即可.
特殊业务.特殊设计
178.检索DSL测试-聚合测试
178.1 数据迁移
POST _reindex { "source":{ "index":"twitter" }, "dest":{ "index":"new_twitter" } }
178.2 商品数据新映射
讯享网PUT gulimall_product { "mappings": { "properties": { "attrs": { "type": "nested", "properties": { "attrId": { "type": "long" }, "attrName": { "type": "keyword" }, "attrValue": { "type": "keyword" } } }, "brandId": { "type": "long" }, "brandImg": { "type": "keyword", "index": false, "doc_values": false }, "brandName": { "type": "keyword" }, "catalogId": { "type": "long" }, "catalogName": { "type": "keyword" }, "hasStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "saleCount": { "type": "long" }, "skuId": { "type": "long" }, "skuImg": { "type": "keyword" }, "skuPrice": { "type": "keyword" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "spuId": { "type": "keyword" } } } }
182.ES-Response封装
182.1 关于聚合类型
//得到品牌的名字 String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
bucket获取聚合时,返回的类型如何判断?
通过Debug模式分析 es 返回的Response 从而获得 聚合类型

182.2 代码
讯享网 / * 封装返回结果 * @param response * @return */ private SearchResult buildSearchResult(SearchResponse response,SearchParam searchParam) { SearchResult result = new SearchResult(); SearchHits hits = response.getHits(); / * 商品集合 */ List<SkuEsModel> esModels = new ArrayList<>(); if(hits.getHits() != null && hits.getHits().length > 0){ for (SearchHit hit : hits.getHits()) { String sourceAsString = hit.getSourceAsString(); SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class); if(!StringUtils.isEmpty(searchParam.getKeyword())){ HighlightField skuTitle = hit.getHighlightFields().get("skuTitle"); String highLightField = skuTitle.getFragments()[0].string(); esModel.setSkuTitle(highLightField); } esModels.add(esModel); } } result.setProducts(esModels); / * 分类聚合 */ ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg"); List<SearchResult.CatalogVo> catalogVos = new ArrayList<>(); List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets(); for (Terms.Bucket bucket : buckets) { SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(); //得到分类id String keyAsString = bucket.getKeyAsString(); catalogVo.setCatalogId(Long.parseLong(keyAsString)); //得到分类名 ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg"); String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString(); catalogVo.setCatalogName(catalog_name); catalogVos.add(catalogVo); } result.setCatalogs(catalogVos); / * 品牌聚合 */ List<SearchResult.BrandVo> brandVos = new ArrayList<>(); ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg"); for (Terms.Bucket bucket : brand_agg.getBuckets()) { SearchResult.BrandVo brandVo = new SearchResult.BrandVo(); //得到品牌的id long brandId = bucket.getKeyAsNumber().longValue(); //得到品牌的名字 String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString(); //得到品牌的图片 String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString(); brandVo.setBrandId(brandId); brandVo.setBrandName(brandName); brandVo.setBrandImg(brandImg); brandVos.add(brandVo); } result.setBrands(brandVos); / * 属性聚合 */ List<SearchResult.AttrVo> attrVos = new ArrayList<>(); ParsedNested attr_agg = response.getAggregations().get("attr_agg"); ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg"); for (Terms.Bucket bucket : attr_id_agg.getBuckets()) { SearchResult.AttrVo attrVo = new SearchResult.AttrVo(); //得到属性的id long attrId = bucket.getKeyAsNumber().longValue(); //得到属性的名字 String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString(); //得到属性的所有值 List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(item -> { String keyAsString = item.getKeyAsString(); return keyAsString; }).collect(Collectors.toList()); attrVo.setAttrId(attrId); attrVo.setAttrValue(attrValues); attrVo.setAttrName(attrName); attrVos.add(attrVo); } result.setAttrs(attrVos); //分页信息 - 当前页码 result.setPageNum(searchParam.getPageNum()); //分页信息 - 总记录数 long total = hits.getTotalHits().value; result.setTotal(total); //分页信息 - 总页码 int totalPages = (int)total%ESConstant.PRODUCT_PAGE_SIZE == 0 ? (int)total/ESConstant.PRODUCT_PAGE_SIZE : ((int)total/ESConstant.PRODUCT_PAGE_SIZE + 1); result.setTotalPages(totalPages); return result; }
194.线程池
194.1 参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
threadFactory : 线程池的创建工厂
handler : 如果队列满了 , 按照我们指定的拒绝策略执行任务
194.2 工作顺序
线程池创建 , 准备好core数量的核心线程,准备接受任务
core满了 , 就将再进来的任务放入阻塞队列中 . 空闲的core就会自己去阻塞队列获取任务执行
阻塞队列满了 , 就直接开启新线程执行 , 最大只能开到max指定的数量
max满了就用RejectedExecutionHandler拒绝任务
max 都执行完成 , 有很多空闲时间 , 在指定的时间keepAliveTime以后 , 释放max-core这些线程
讯享网ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 200, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), //默认为Integer的最大值 , 太大了 , 根据项目和服务器的内存大小设置 Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); //默认抛弃
194.3 举列子
一个线程池 { "corePoolSize" : 7, "maximumPoolSize" : 20 , "LinkedBlockingQueue" : 50 }
请问 : 100 并发进来线程池是怎么分配任务的?
进来7个核心线程池执行任务 , 50个任务占满阻塞队列 , 开启再13个线程执行任务
194.4 其他线程池
194.1.1 CachedThreadPool()
core是0,所有都可以回收
讯享网Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
194.1.2 FixedThreadPool(int core)
固定大小, core == max ; 都不可回收 , 全部是核心线程
讯享网Executors.newFixedThreadPool(10);
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
194.1.3ScheduledThreadPool(int core)
定时任务的线程池
讯享网Executors.newScheduledThreadPool(10);
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }
194.1.4 SingleThreadExecutor();
单线程线程池,后台从队列中取,逐个执行
讯享网Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
195 Hello-CompletableFuture
195.1 异步无返回值 runAsync
讯享网CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { System.out.println("执行业务"); }, executor);
195.2 异步有返回值 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("执行业务"); return 1; }, executor); //阻塞获取返回值 System.out.println(future.get());
197 CompletableFuture-回调与异常感知
197.1 回调
讯享网.whenComplete(); //在上一个任务完成之后执行,与上一个任务在同一个线程 .whenCompleteAsync(); //在上一个任务完成之后执行,在线程池中开启新的线程
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("执行业务"); return 1; }, executor).whenComplete((r,e) -> { System.out.println("异步任务完成了...结果是: " + r); System.out.println("异步任务完成了...异常是: " + e); });
197.2 异常 exceptionally
讯享网CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("执行业务"); return 1; }, executor).whenComplete((r,e) -> { System.out.println("异步任务完成了...结果是: " + r); System.out.println("异步任务完成了...异常是: " + e); }).exceptionally(throwable -> { //可以感知异常,并处理异常的返回(catch) return 10; }); //阻塞获取返回值 future.get();
198.CompletableFuture-handle最终处理
whenCompletable 虽然能感知结果并且获取异常 , 但是无法修改救过 , 所以有需求的时候利用handle方法进行异步调用的结果修改
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("执行业务"); return 1; }, executor).handle((r,t) ->{ if(r != null){ return r * 2; } if(t != null){ return 0; } return 0; }); //阻塞获取返回值 future.get();
199.CompletableFuture-线程串行化

thenApply方法 : 当一个线程依赖另一个线程时,获取上一个任务返回的结果 , 并返回当前任务的返回值
thenAccept : 消费处理结果.接收任务的处理结果,并消费处理,无返回结果
thenRun方法 : 只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后 , 执行thenRun的后续操作
带有Async默认是异步执行的,就是在上一个任务基础上开一个新线程
200.CompletableFuture-两任务组合-都要完成 &&
runAfterBoth : 组合两个future,不需要获取future的结果 , 只需要两个future处理完任务后,处理该任务

thenAcceptBoth : 组合两个future任务的返回结果,然后处理任务,没有返回值

runAfterBoth : 组合两个future ,获取future的结果 , 并返回当前任务的返回值

200.1 代码
讯享网//future01 CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> { System.out.println("任务1线程" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("任务1结束"); return i; }, executor); //future02 CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> { System.out.println("任务2线程" + Thread.currentThread().getId()); System.out.println("任务2结束"); return "Hello"; }, executor); //runAfterBothAsync future01.runAfterBothAsync(future02, () ->{ System.out.println("任务1 和 任务2 执行完毕 不能感知结果"); }); //thenAcceptBothAsync future01.thenAcceptBothAsync(future02,(r1,r2) ->{ System.out.println("任务1 和 任务2 执行完毕 感知结果 r1 =>" +r1 +","+" r2 => " + r2); },executor); //thenCombineAsync CompletableFuture<String> future = future01.thenCombineAsync(future02, (r1, r2) -> { System.out.println("任务1 和 任务2 执行完毕 感知结果,有返回值 r1 =>" + r1 + "," + " r2 => " + r2); return r1 + "," + " r2 => " + r2; }, executor); future.get();
201 CompletableFuture-两任务组合-一个完成 ||
当两个任务中 , 任意一个future任务完成的时候 , 执行任务.
runAfterEither : 两个任务有一个执行完成 , 不需要获取future的结果 , 处理任务 , 也没有返回值

acceptEither : 两个任务有一个执行完成 , 获取它的返回值 , 处理任务 , 没有新的返回值

applyToEither : 两个任务有一个执行完成 , 获取它的返回值 , 处理任务并有新的返回值

202 CompletableFuture-多任务组合
202.1 allOf 等待所有任务完成
CompletableFuture<String> imgFuture = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品的图片信息"); return "hello.jpg"; },executor); CompletableFuture<String> attrFuture = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品的属性信息"); return "hello.jpg"; },executor); CompletableFuture<String> descFuture = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品的描述信息"); return "hello.jpg"; },executor); CompletableFuture<Void> future = CompletableFuture.allOf(imgFuture, attrFuture, descFuture); future.get(); System.out.println(imgFuture.get()+"|"+attrFuture.get()+"|"+descFuture.get());
202.2 anyOf 只要有一个任务完成
讯享网 CompletableFuture<String> imgFuture = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品的图片信息"); return "hello.jpg"; },executor); CompletableFuture<String> attrFuture = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品的属性信息"); return "hello.jpg"; },executor); CompletableFuture<String> descFuture = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品的描述信息"); return "hello.jpg"; },executor); CompletableFuture<Void> future = CompletableFuture.allOf(imgFuture, attrFuture, descFuture); future.get();
205.mybatis内部类映射
![]()
SpuItemAttrGroupVo为内部类,"<resultMap>"无法映射找不到!
206 sql-组连接
GROUP_CONCAT //组连接
206.1 例子

讯享网select GROUP_CONCAT(distinct name) from `pms_category` GROUP BY cat_level

211.认证
211.1 viewController
将请求和页面映射过来 , 无需在写controller
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
}
}
相当于
讯享网@Controller
public class LoginController {
@GetMapping("/login.html")
public String login(){
return "login";
}
}
@Controller
public class RegController {
@GetMapping("/reg.html")
public String reg(){
return "reg";
}
}
214.接口防刷策略
214.1 再次校验
用户在接收到验证码之后,验证码在一定时间内有效
将手机号和验证码放入redis暂存
redisTemplate.opsForValue().set( AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone , //sms:code: code, 10, TimeUnit.MINUTES); //设置验证码10分钟内有效
214.2 防止快速获取
真正实现60秒内只能发送一次验证码 , 而不是可以通过刷新页面 , 让60秒重新停止计时.
将key加上当前系统时间A,当这个手机号再次进来的时候,获取当前系统时间B,对key进行截串获取系统时间A , 判断系统时间B-系统时间A 是否大于60秒
讯享网 String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); if(!StringUtils.isEmpty(redisCode)){ long l = Long.parseLong(redisCode.split("_")[1]); //接口防刷 if(System.currentTimeMillis() - l < 60000){ return R.error(BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getMessage()); } } String code = UUID.randomUUID().toString().substring(0, 5) + "_" +System.currentTimeMillis();
214.3 代码
@Controller public class LoginController { @Autowired private ThirdPartyFeignClient thirdPartyFeignClient; @Autowired private StringRedisTemplate redisTemplate; @GetMapping("/sms/sendCode") @ResponseBody public R sendCode(@RequestParam("phone") String phone){ String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); if(!StringUtils.isEmpty(redisCode)){ long l = Long.parseLong(redisCode.split("_")[1]); //接口防刷 if(System.currentTimeMillis() - l < 60000){ return R.error(BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getMessage()); } } String code = UUID.randomUUID().toString().substring(0, 5) + "_" +System.currentTimeMillis(); System.err.println(code); redisTemplate.opsForValue().set( AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone , //sms:code: code, 10, TimeUnit.MINUTES); //设置验证码10分钟内有效 thirdPartyFeignClient.sendCode(phone,code); return R.ok(); } }
215 注册
215.1 转发下的405
405 : 请求方式不正确
讯享网 @PostMapping("/regist")
public String regist(@Valid UserRegisterVo vo , BindingResult result , Model model){
//...
return "forward:/reg.html"; //页面跳转都是get请求
}
请求 regist 的方式为 Post 方式 , 转发是将请求原封不动进行传递 , 而页面跳转是 get 请求 , 所以会 405
解决方案
将页面跳转改为页面渲染
@PostMapping("/regist") public String regist(@Valid UserRegisterVo vo , BindingResult result , Model model){ //... return "reg"; //页面跳转都是get请求 }
215.2 重复提交
重复提交
讯享网return "reg" //转发,刷新页面相当于重复提交
解决:使用重定向的方式
return "redirect:/reg.html";
215.3 请求域
Model model 的作用范围无法作用于重定向的情况下
解决:使用RedirectAttribute的方式
RedirectAttributes redirectAttributes 模拟重定向携带数据
讯享网@PostMapping("/regist")
public String regist(@Valid UserRegisterVo vo , BindingResult result , RedirectAttributes redirectAttributes){
if(result.hasErrors()){
//校验出错,转发到注册页
Map<String, String> errorsMap = result.getFieldErrors().stream().collect(Collectors.toMap(
k -> k.getField(),
v -> v.getDefaultMessage()
));
redirectAttributes.addFlashAttribute("errors",errorsMap);
return "redirect:http://auth.gulimall.com/reg.html";
}
//注册 调用远程服务进行注册
//注册成功回到首页 , 回到登录页
return "redirect:/login.html";
}
216.注册 - 异常机制
将用户名存在 和 手机号的存在 的情况以异常的形式返回给controller
216.1 service
/ * 检查邮箱是否唯一 * @param email * @return */ void checkPhoneUnique(String email) throws PhoneExistException; / * 检查用户名是否唯一 * @param userName * @return */ void checkUsernameUnique(String userName) throws UserNameExistException;
216.2 serviceImpl
讯享网/ * 检查邮箱是否唯一 * @param phone * @return */ @Override public void checkPhoneUnique(String phone) throws PhoneExistException { Integer mobile = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone)); if (mobile >= 1){ throw new PhoneExistException(); } } / * 检查有户名是否唯一 * @param userName * @return */ @Override public void checkUsernameUnique(String userName) throws UserNameExistException { Integer username = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName)); if(username >= 1){ throw new UserNameExistException(); } }
注册
/ * 注册 * @param vo */ @Override public void register(UserRegisterVo vo) { MemberEntity entity = new MemberEntity(); //设置默认等级 entity.setLevelId(1L); //检查用户名和手机号是否唯一 checkPhoneUnique(vo.getPhone()); checkUsernameUnique(vo.getUserName()); entity.setMobile(vo.getPhone()); entity.setUsername(vo.getUserName()); }
216.3 controller
讯享网 / * 注册 */ @PostMapping("/regist") public R register(@RequestBody UserRegisterVo vo){ try { memberService.register(vo); }catch (PhoneExistException p){ return R.error(1000,p.getMessage()); }catch (UserNameExistException u){ return R.error(1001,u.getMessage()); } return R.ok(); }
217 密码 明文 加盐 保存
217.1 MD5 盐值加密
MD5 信息摘要虽然是不可逆的 , 但是 明文 和 密文 存在严格的一对一函数关系 , 现有的激活成功教程网站就是基于暴力存储的检索的方式进行"激活成功教程"的. 比如"" 的 加密算法 "aaaaa".
所谓 , 加盐就是在原有明文的基础上 , 进行修改 , 让得出的明文更加具有不确定性.
218.2 Spring工具类
public class Test { public static void main(String[] args) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode = passwordEncoder.encode(""); System.err.println(encode); //$2a$10$ZVf5r/1uGlR1wtEWxwI0GOfewAPrCu3jkZvXKmeyrUBk2MlxUaf0G //$2a$10$pDoj2ZAprhZ.Wu2yAGBLD.ix3/oeijJRh8wuGi5ZZ9Q0urB8Kb/Ba passwordEncoder.matches("","$2a$10$ZVf5r/1uGlR1wtEWxwI0GOfewAPrCu3jkZvXKmeyrUBk2MlxUaf0G"); //true passwordEncoder.matches("", "$2a$10$pDoj2ZAprhZ.Wu2yAGBLD.ix3/oeijJRh8wuGi5ZZ9Q0urB8Kb/Ba"); //true } }
存在用户的密码存在相同的情况 , 利用工具类自动给明文加不同的盐 , 再由工具类判断 , 数据库字段就不需要再设置salt字段 6!
讯享网//密码 加盐 加密 存储 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode = passwordEncoder.encode(vo.getPassword()); entity.setPassword(encode);
225.分布式Session
225.1 普通Sessoin的问题
无法跨域名共享Session数据
不同服务,session不能共享
225.2 问题解决-统一存储

227.SpringSession
227.1 start
227.1 auth服务
将用户信息保存再redis session中
依赖
<!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>2.5.1</version> </dependency>
配置
讯享网 spring: session: store-type: redis server: port: 20000 servlet: session: timeout: 30m
start
@EnableRedisHttpSession
讯享网@EnableRedisHttpSession @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) public class AuthApp { public static void main(String[] args) { SpringApplication.run(AuthApp.class,args); } }
放入session
session.setAttribute("loginUser",userName);
讯享网 @PostMapping("/login")
public String login(UserLoginVo vo , RedirectAttributes redirectAttributes , HttpSession session ){
R login = memberFeignService.login(vo);
if(login.getCode() == 0){
//成功
String userName = "leifengyang mock";
//SpringData
System.err.println("将用户信息存储值redis session");
session.setAttribute("loginUser",userName);
return "redirect:http://gulimall.com";
}else {
HashMap<String, String> errors = new HashMap<>();
errors.put("msg","账号或密码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
227.2 product服务
需要从rediss session中获取 , 也需要引入依赖 , 配置相同!
228.SpringSession-自定义
228.1 子域Session共享 与 序列化
@Configuration public class GulimallSessionConfig { / * 子域共享session,扩大作用域 * @return */ @Bean public CookieSerializer cookieSerializer(){ DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); cookieSerializer.setDomainName("gulimall.com"); cookieSerializer.setCookieName("GULISESSION"); return cookieSerializer; } / * 序列化 * @return */ @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer(){ return new GenericJackson2JsonRedisSerializer(); } }
认证服务 和 商品 服务都需要配置 !
页面获取session信息
讯享网 <a href="http://auth.gulimall.com/login.html">你好,请登录 : [[${session.loginUser == null ?'':session.loginUser}]] </a>
231.单点登录
231.1 多系统
127.0.0.1 ssoserver.com 127.0.0.1 client1.com 127.0.0.1 client2.com
中央认证服务器 ; ssoserver.com
其他系统,想要登录去ssoserver.com登录,登录成功跳转回来
只要有一个登录,其他都不用登录
全系统唯一一个sso.sessionid
232.2 单点登录-流程一

客户端有一个受保护的方法 , 需要登录后才能访问 , 如果没登录 (session中没有) , 则去重定向到登录页面(登录服务器),
但是认证服务器成功认证之后需要调回原路径 , 所以再重定向地址上加上参数 , 让认证服务器能够跳转回来
<span style="background-color:#"><span style="color:#c88fd0">return</span> <span style="color:#d26b6b">"redirect:"</span> <span style="color:#b8bfc6">+</span> <span style="color:#b8bfc6">ssoServerUrl</span> <span style="color:#b8bfc6">+</span><span style="color:#d26b6b">"?redirect_url=http://client1.com:8081/employees"</span>;</span>
client1
controller
讯享网@Controller public class HelloController { @Value("${sso.server.url}") public String ssoServerUrl; / * 无需访问就可登录 * @return */ @ResponseBody @GetMapping("/hello") public String hello(){ return "hello"; } / * 需要登录后才能访问 * @param model * @param session * @return */ @GetMapping("/employees") public String employees(Model model , HttpSession session){ Object loginUser = session.getAttribute("loginUser"); if(loginUser == null){ //没登录 , 跳转到登录服务器进行登录 //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的 return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees"; }else{ ArrayList<String> emps = new ArrayList<>(); emps.add("张三"); emps.add("李四"); model.addAttribute("emps",emps); return "list"; } } }
application.yml
server:
port: 8081
sso:
server:
url: http://sso.com:8080/login.html
ssoServer
controller
讯享网@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String redirectUrl){
return "login";
}
@PostMapping("/doLogin")
public String doLogin(){
//登录成功跳转 , 跳回到之前的页面
return "";
}
}
232.3 单点登录-流程二
232.3.1 redirectUrl丢失
执行 /login.html 的请求映射(跳转到登录页面) 后需要进行登录 , 传递的url需要传给 /doLogin方法进行跳转 , 但是这样会丢失 redirectUrl , 所以将得到的url放入页面(model)中
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url , Model model){
model.addAttribute("url",url); //将url放入model中
return "login";
}
讯享网 <form action="/doLogin" method="post"> 用户名 : <input name="username"><br/> 密码 : <input name="password"> <br/> <input type="hidden" name="url" th:value="${url}"> <!--放入model中取,doLoin方法再从表单的数据中取--> <input type="submit" value="登录"/> </form>
@PostMapping("/doLogin") public String doLogin(@RequestParam("username") String username , @RequestParam("password") String password , @RequestParam("url")String url){ if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){ //登录成功 , 跳回之前的页面 return "redirect:" + url; } //登录成功跳转 , 跳回到之前的页面 return "login"; }
232.3.2 死循环
client1
讯享网 @GetMapping("/employees") public String employees(Model model , HttpSession session){ Object loginUser = session.getAttribute("loginUser"); if(loginUser == null){ //没登录 , 跳转到登录服务器进行登录 //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的 return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees"; }else{ ArrayList<String> emps = new ArrayList<>(); emps.add("张三"); emps.add("李四"); model.addAttribute("emps",emps); return "list"; } }
从登录页跳转回来之后 , 重定向相当于再发一次 /employees 请求 , 此时客户端无法判断这次请求是正常的请求,还是跳转回来的请求,这时 session依然为空 , 还会跳转到 登录页面 !
所以 , 再登录跳转回来的时候 , 再在请求路径上添加一个 token 参数 , 并且将 用户信息保存起来(方式不限)
ssoServer
/ * 需要登录后才能访问 * @param model * @param session * @param token 登录跳转回来才会带上token * @return */ @GetMapping("/employees") public String employees(Model model , HttpSession session , @RequestParam( value = "token" , required = false) String token){ if(!StringUtils.isEmpty(token)){ //去sessino登录成功跳回来就会带上 //TODO : 去 ssoServer获取当前token真正对应的用户信息 session.setAttribute("loginUser","zhangsan"); } Object loginUser = session.getAttribute("loginUser"); if(loginUser == null){ //没登录 , 跳转到登录服务器进行登录 //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的 return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees"; }else{ ArrayList<String> emps = new ArrayList<>(); emps.add("张三"); emps.add("李四"); model.addAttribute("emps",emps); return "list"; } }
232.4 单点登录-流程三 一处登录,处处登录
给当前系统留一个记号(cookie)sso_token,客户端只要由任何一个登录成功了,就让服务器端给网页留下一个cookie(key=sso_token,value=uuid)
浏览器以后访问这个域名都要带上这个域名下的所有cookie
sso-server
讯享网 Cookie sso_token = new Cookie("sso_token", uuid); response.addCookie(sso_token);
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url , Model model){
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username ,
@RequestParam("password") String password ,
@RequestParam("url")String url,
HttpServletResponse response
){
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//登录成功 , 跳回之前的页面
//....
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
return "redirect:" + url + "?token=" +uuid;
}
//登录成功跳转 , 跳回到之前的页面
return "login";
}
}
判断是否登录过,判断是否有关键cookie,sso_token,如果没有就展示登录页,如果有就直接返回之前的页面
讯享网
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url ,
Model model,
@CookieValue(value = "sso_token",required = false) String sso_token
){
if(!StringUtils.isEmpty(sso_token)){
//说明之前有人登录过,浏览器留下了痕迹
return "redirect:"+url+"?token="+sso_token;
}
model.addAttribute("url",url);
return "login";
}
234.5 代码
234.5.1 sso-server
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token){
String s = redisTemplate.opsForValue().get(token);
return s;
}
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url ,
Model model,
@CookieValue(value = "sso_token",required = false) String sso_token
){
if(!StringUtils.isEmpty(sso_token)){
//说明之前有人登录过,浏览器留下了痕迹
return "redirect:"+url+"?token="+sso_token;
}
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username ,
@RequestParam("password") String password ,
@RequestParam("url")String url,
HttpServletResponse response
){
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//登录成功 , 跳回之前的页面
String uuid = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(uuid,username);
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
return "redirect:" + url + "?token=" +uuid;
}
//登录成功跳转 , 跳回到之前的页面
return "login";
}
}
234.5.2 client1
讯享网<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>员工列表</title>
</head>
<body>
<h1>欢迎 :[[${session.loginUser}]]</h1>
<ul>
<li th:each = "emp:${emps}">姓名 : [[${emp}]]</li>
</ul>
</body>
</html>
@Controller public class HelloController { @Value("${sso.server.url}") public String ssoServerUrl; / * 无需访问就可登录 * @return */ @ResponseBody @GetMapping("/hello") public String hello(){ return "hello"; } / * 需要登录后才能访问 * @param model * @param session * @param token 登录跳转回来才会带上token * @return */ @GetMapping("/employees") public String employees(Model model , HttpSession session , @RequestParam( value = "token" , required = false) String token){ if(!StringUtils.isEmpty(token)){ //去sessino登录成功跳回来就会带上 //TODO : 去 ssoServer获取当前token真正对应的用户信息 RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> forEntity = restTemplate.getForEntity("http://sso.com:8080/userInfo?token=" + token, String.class); String body = forEntity.getBody(); session.setAttribute("loginUser",body); } Object loginUser = session.getAttribute("loginUser"); if(loginUser == null){ //没登录 , 跳转到登录服务器进行登录 //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的 return "redirect:" + ssoServerUrl +"?redirect_url=http://client1.com:8081/employees"; }else{ ArrayList<String> emps = new ArrayList<>(); emps.add("张三"); emps.add("李四"); model.addAttribute("emps",emps); return "list"; } } }
234.5.3 client2
讯享网@Controller public class HelloController { @Value("${sso.server.url}") public String ssoServerUrl; / * 无需访问就可登录 * @return */ @ResponseBody @GetMapping("/hello") public String hello(){ return "hello"; } / * 需要登录后才能访问 * @param model * @param session * @param token 登录跳转回来才会带上token * @return */ @GetMapping("/boss") public String employees(Model model , HttpSession session , @RequestParam( value = "token" , required = false) String token){ if(!StringUtils.isEmpty(token)){ //去sessino登录成功跳回来就会带上 //TODO : 去 ssoServer获取当前token真正对应的用户信息 RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> forEntity = restTemplate.getForEntity("http://sso.com:8080/userInfo?token=" + token, String.class); String body = forEntity.getBody(); session.setAttribute("loginUser",body); } Object loginUser = session.getAttribute("loginUser"); if(loginUser == null){ //没登录 , 跳转到登录服务器进行登录 //跳转过去之后 , 使用url的参数表示我们是哪个页面跳转到登录页面的 return "redirect:" + ssoServerUrl +"?redirect_url=http://client2.com:8082/boss"; }else{ ArrayList<String> emps = new ArrayList<>(); emps.add("张三"); emps.add("李四"); model.addAttribute("emps",emps); return "list"; } } }
236 购物车 - redis 存储模型分析
236.1 redis 存储模型分析

236.2 Cart
/ * 购物车 * 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算 */ publi c class Cart { private List<CartItem> items; private Integer countNum; //商品数量 private Integer countType; //商品类型数量 private BigDecimal totalAmount; //商品总价 private BigDecimal reduce = new BigDecimal("0.00"); //减免价格 public List<CartItem> getItems() { return items; } public void setItems(List<CartItem> items) { this.items = items; } / * 获取商品数量 * @return */ public Integer getCountNum() { int count = 0; if(items != null && items.size()>0){ for (CartItem item : items) { count += item.getCount(); } } return count; } / * 获取商品类型数量 * @return */ public Integer getCountType() { int count = 0; if(items != null && items.size()>0){ for (CartItem item : items) { count += 1; } } return count; } / * 获取购物车总金额 * @return */ public BigDecimal getTotalAmount() { BigDecimal amount = new BigDecimal("0"); //计算购物项总价 if(items != null && items.size() > 0){ for (CartItem item : items) { BigDecimal totalPrice = item.getTotalPrice(); amount = amount.add(totalPrice); } } //减去优惠总价 BigDecimal subtract = amount.subtract(getReduce()); return subtract; } public BigDecimal getReduce() { return reduce; } public void setReduce(BigDecimal reduce) { this.reduce = reduce; } }
236.3 CartItem
讯享网public class CartItem { private Long skuId; private Boolean check = true; private String title; private String image; private List<String> skuAttr; private BigDecimal price; private Integer count; private BigDecimal totalPrice; public BigDecimal getTotalPrice() { return this.price.multiply(new BigDecimal(""+this.count)); } public void setTotalPrice(BigDecimal totalPrice) { this.totalPrice = totalPrice; } public Long getSkuId() { return skuId; } public void setSkuId(Long skuId) { this.skuId = skuId; } public Boolean getCheck() { return check; } public void setCheck(Boolean check) { this.check = check; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public List<String> getSkuAttr() { return skuAttr; } public void setSkuAttr(List<String> skuAttr) { this.skuAttr = skuAttr; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public Integer getCount() { return count; } public void setCount(Integer count) { this.count = count; } }
239.ThreadLocal
239.1 拦截器-判断用户是否登录了
拦截器 : 再执行controller方法之前做一些事情
@Component public class CartInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserInfoTo userInfoTo = new UserInfoTo(); HttpSession session = request.getSession(); String userName = (String) session.getAttribute(AuthServerConstant.LOGIN_USER); if(userName != null && !userName.equals("")){ //登录了 userInfoTo.setUserId(userName); return true; }else{ //没登录 return false; } } }
.
添加配置
addInterceptors
讯享网@Configuration public class GulimallWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new CartInterceptor()).addPathPatterns("/"); } }
239.2 ThreadLocal
拦截器Interceptor 与 controller 再同一次请求中一直使用的是一个线程,所以利用ThreadLocal进行数据传递
ThreadLocal-interceptor
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
ThreadLocal-controller
讯享网UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
interceptor
public class CartInterceptor implements HandlerInterceptor { public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserInfoTo userInfoTo = new UserInfoTo(); HttpSession session = request.getSession(); String userName = (String) session.getAttribute(AuthServerConstant.LOGIN_USER); if(userName != null && !userName.equals("")){ //登录了 userInfoTo.setUserId(userName); threadLocal.set(userInfoTo); return true; }else{ //没登录 return false; } } }
controller
讯享网@Controller
public class CartController {
@GetMapping("/cart.html")
public String cartListPage( ){
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
return "cartList";
}
}
241 添加购物车
241.1 opsForHash
绑定对hash的操作
/ * 获取到要操作的购物车 * @return */ private BoundHashOperations<String, Object, Object> getCartOps() { UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); String cartKey = ""; if(userInfoTo.getUserId() != null){ cartKey = CART_PREFIX + userInfoTo.getUserId(); } BoundHashOperations<String, Object, Object> stringObjectObjectBoundHashOperations = redisTemplate.boundHashOps(cartKey); return stringObjectObjectBoundHashOperations; }
241.2 code
讯享网 @Override public CartItem addToCart(Long skuId, Integer num) { BoundHashOperations<String, Object, Object> cartOps = getCartOps(); CartItem cartItem = new CartItem(); //添加商品到购物车 getSkuInfoTask CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> { R skuInfo = productFeignService.getSkuInfo(skuId); SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {}); cartItem.setCheck(true); cartItem.setCount(num); cartItem.setImage(data.getSkuDefaultImg()); cartItem.setSkuId(skuId); cartItem.setPrice(data.getPrice()); }, executor); //远程查询sku销售属性组合信息 getSkuSaleAttrTask CompletableFuture<Void> getSkuSaleAttrTask = CompletableFuture.runAsync(() -> { List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId); cartItem.setSkuAttr(skuSaleAttrValues); }, executor); //阻塞等待任务全部完成 CompletableFuture<Void> task = CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrTask); try { task.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } //将添加的商品放入对应用户的redis hash 中 String cartItemJSON = JSON.toJSONString(cartItem); cartOps.put(skuId,cartItemJSON); return cartItem; }
241.3 细节处理
如果购物车不存在这个购物项则为添加新购物项操作
如果购物车中存在该商品则为增加数量操作
@Override public CartItem addToCart(Long skuId, Integer num) { BoundHashOperations<String, Object, Object> cartOps = getCartOps(); //判断购物车现在是否有此商品 String skuInfoJSON = (String) cartOps.get(skuId.toString()); if(StringUtils.isEmpty(skuInfoJSON)){ CartItem cartItem = new CartItem(); //购物车无此商品 //添加商品到购物车 getSkuInfoTask CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> { R skuInfo = productFeignService.getSkuInfo(skuId); SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {}); cartItem.setCheck(true); cartItem.setCount(num); cartItem.setImage(data.getSkuDefaultImg()); cartItem.setSkuId(skuId); cartItem.setPrice(data.getPrice()); }, executor); //远程查询sk销售属性u组合信息 getSkuSaleAttrTask CompletableFuture<Void> getSkuSaleAttrTask = CompletableFuture.runAsync(() -> { List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId); cartItem.setSkuAttr(skuSaleAttrValues); }, executor); //阻塞等待任务全部完成 CompletableFuture<Void> task = CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrTask); try { task.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } //将添加的商品放入对应用户的redis hash 中 String cartItemJSON = JSON.toJSONString(cartItem); cartOps.put(skuId,cartItemJSON); return cartItem; } else { //购物车有此商品,修改数量 CartItem cartItem = JSON.parseObject(skuInfoJSON, CartItem.class); cartItem.setCount(cartItem.getCount()+num); cartOps.put(skuId.toString(),JSON.toJSONString(cartItem)); return cartItem; } }
241.4 重复添加
再添加成功页面刷新会重复添加商品
添加商品成功后重定向到另一个页面,在这个页面没有添加购物车操作,只是展示成功添加了的商品详情
241.4.1 RedirectAttribute
讯享网/ * *RedirectAttributes redirectAttributes * redirectAttributes.addFlashAttribute(); 将数据放在session里面可以再页面中取出,但是只能取一次 * redirectAttributes.addAttribute(); 将数据放在url后面 * 添加购物侧 * @return */
241.4.2 code
/
*
*RedirectAttributes redirectAttributes
* redirectAttributes.addFlashAttribute(); 将数据放在session里面可以再页面中取出,但是只能取一次
* redirectAttributes.addAttribute(); 将数据放在url后面
* 添加购物侧
* @return
*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes redirectAttributes
){
cartService.addToCart(skuId,num);
//将数据自动放到url后面
redirectAttributes.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
/
* 跳转到购物车添加成功页
* @param skuId
* @param model
* @return
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
//重定向到成功页面,再次查询购物车数据即可
CartItem cartItem = cartService.getCartItem(skuId);
model.addAttribute("item",cartItem);
return "success";
}
讯享网return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
//重定向到下一个页面 , 刷新下一个页面没有添加商品的操作 , 只有展示的商品详细信息的操作
249 RabbitMQ

249.1 安装
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
默认账户名和密码 : guest , guest
249.2 SpringBoot rabbit starter
pom
讯享网 <!--rabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
yml
spring: rabbitmq: host: 192.168.64.140 port: 5672 virtual-host: /
249.3 SpringBootTemplate
249.3.1 declearExchange
讯享网 @Autowired private AmqpAdmin amqpAdmin; / * 创建direct交换机 */ @GetMapping("/declareDirectExchange") public void createDirectExchange(){ / * DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments) */ DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false); amqpAdmin.declareExchange(directExchange); log.info("exchange[{}]创建成功","hello-java-exchange"); }
249.3.2 declearQueue
/ * 创建队列 */ @GetMapping("/declareQueue") public void createQueue(){ / * public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete,@Nullable Map<String, Object> arguments) */ Queue queue = new Queue("hello-java-queue",true,false,false); amqpAdmin.declareQueue(queue); log.info("queue[{}]创建成功","hello-java-queue"); }
249.3.3 declearBinding
讯享网 / * 绑定交换机和队列 */ @GetMapping("declearBinding") public void createBinding(){ Binding binding = new Binding("hello-java-queue", //目的地 , 交换机或者队列的名称 Binding.DestinationType.QUEUE, //目的地类型 , 枚举 : 交换机或者队列 "hello-java-exchange",//交换机名 "hello.java", //路由键 null //map ); amqpAdmin.declareBinding(binding); log.info("bingding[{}]创建成功","hello-java-binding"); }
249.3.4 sendMessage
/ * 发送消息 */ @GetMapping("/sendMessage") public void sendMessage(){ //发送消息 , 如果发送的消息是一个对象 , 我们会使用序列化机制 , 将对象写出去 , 对象必须实现Serializable OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity(); orderReturnReasonEntity.setId(1L); orderReturnReasonEntity.setCreateTime(new Date()); orderReturnReasonEntity.setName("哈哈"); rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnReasonEntity); log.info("消息发送完成entity:{}",orderReturnReasonEntity); }
默认将对象存储在队列,队列会被jdk的方式序列化.

将序列化方式改为JSON序列化
讯享网@Configuration public class MyRabbitConfig { / * 消息序列化转换器默认使用jdk序列化,转变为JSON序列化 * @return */ @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } }

249.4 @RabbitListener & @RabbitHandler
@RabbitListener : 可以标注在类和方法上
@RabbitHandler : 可以标注方法上(区分不同的消息)
对于同一队列中不同类型的消息对应不同的listener接收
@RabbitListener(queues = {"hello-java-queue"}) public class QueueConsumer{ / * RabbitHandler 接收 hello-java-queue 中 OrderReturnReasonEntity的消息 */ @RabbitHandler public void recieveMessage1(Message message, OrderReturnReasonEntity orderReturnReasonEntity, Channel channel ){ log.error("从队列hello-java-queue消费消息,message:{}",message); log.error("消息体:{}",entity); } / * RabbitHandler 接收 hello-java-queue 中 Order的消息 */ @RabbitHandler public void recieveMessage2(Message message, Order order, Channel channel ){ log.error("从队列hello-java-queue消费消息,message:{}",message); log.error("消息体:{}",entity); } }
249.5 RabbitMQ 消息确认机制
- publisher confirmCallback 确认模式
- publisher returnCallback 未投递到 quene 退回模式
- consumer ack机制

249.5.1 可靠抵达-ConfirmCallback (过时了)
- 在创建 connectionFactory的时候设置PublisherConfirm(true)选项,开启confirmCallback.
- CorrelationData: 用来表示当前消息唯一性
- 消息只要被broker接收就会执行confirmCallback,如果是cluster模式,需要被所有broker接收到才会调用confirmCallback
- 被broker接收到只能表示message已经到达服务器,并不能保证消息一定会被投递到queue中.所以需要用到接下来的returnCallback
第一步 : 开启生产者发送确认回调
讯享网<span style="background-color:#"><span style="color:#84b6cb">spring</span><span style="color:#b7b3b3">:</span> <span style="color:#84b6cb"> rabbitmq</span><span style="color:#b7b3b3">:</span> <span style="color:#84b6cb"> publisher-confirms</span><span style="color:#b7b3b3">: </span>true <span style="color:#da924a">#开启confirmCallback</span></span>
第二部 : 自定义rabbitTemplate,设置确认回调
@Configuration public class MyRabbitConfig { @Autowired private RabbitTemplate rabbitTemplate; / * 消息序列化转换器默认使用jdk序列化,转变为JSON序列化 * @return */ @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法 public void initRabbitTemplate(){ //设置确认回调 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { / * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id) * @param ack 消息是否成功收到 * @param cause 失败的原因 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.err.println("correlationData"+correlationData); System.err.println("ack"+ack); System.err.println("cause"+cause); } }); } }
249.5.2 消息抵达队列-returnCallback
- confirm 模式只能保证消息到达broker , 不能保证消息准确投递到目标queue里,在有些业务场景中,我们需要保证消息一定要投递到目标queue里,此时就需要用到return退回模式.
- 这样如果未能投递到目标queue里将调用returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据.
第一步 : 开启生产者发送消息成功抵达队列回调
讯享网spring: rabbitmq: publisher-returns: true #开启发送端消息抵达队列的确认 template: mandatory: true #只要抵达队列,一异步发送优先回调我们这个returnconfirm
第二部 : 自定义rabbitTemplate,设置消息抵达队列的确认回调
@Configuration public class MyRabbitConfig { @Autowired private RabbitTemplate rabbitTemplate; //.................. //设置消息抵达队列的确认回调 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { / * 只要消息没有投递到制定的队列,就触发这个失败回调 * @param message 投递失败的消息详细信息 * @param replyCode callback 状态码 * @param replyText callback 文本内容 * @param exchange * @param routingKey */ @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.err.println("message:"+message); System.err.println("replyCode:"+replyCode); System.err.println("replyText:"+replyText); System.err.println("exchange:"+exchange); System.err.println("routingKey:"+routingKey); } }); } }
249.5.3 指定消息的id
讯享网 rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", orderReturnReasonEntity, new CorrelationData(UUID.randomUUID().toString())//指定消息的id );
249.5.4 消费者成功消费 - ack
默认自动ack,只要消息接收到,客户端会自动确认(通道一打开,消息一进来就自动确认了),服务端就会移除这个消息
问题 : 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了,发送消息丢失,所以需要手动ack
手动ack,只要我们没有明确的告诉MQ消息已经被消费,没有ack,消息就会一直处于unacked状态,即使Consumer宕机,消息也不会丢失,会重新变为ready状态,下一次有新的consumer连接进来就发给他
- 消费者获取到消息,成功处理 , 可以回复Ack给broker
- basic.ack : 用于肯定确认 , broker将移除此消息.
- basic.nack : 用于否认确定 , 可以指定broker是否丢弃此消息 , 可以批量
- basic.reject : 用于否认确定 , 同上 但是不能批量
yml
spring: rabbitmq: listener: simple: acknowledge-mode: manual #设置手动ack
ack
讯享网/ * 接收消息 * @param message * @param entity * @param channel 通道 */ @RabbitListener(queues = {"hello-java-queue"}) public void recieveMessage(Message message, OrderReturnReasonEntity entity, Channel channel ){ log.error("从队列hello-java-queue消费消息,message:{}",message); log.error("消息体:{}",entity); long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { //手抖ack channel.basicAck(deliveryTag,false); / * channel.basicNack():用于否认确定 , 可以指定broker是否丢弃此消息 ,可以批量 * requeue : 是否重新入队列 */ //channel.basicNack(deliveryTag,false,false); / * channel.basicReject():用于否认确定 , 可以指定broker是否丢弃此消息 ,不可以批量 * requeue : 是否重新入队列 */ //channel.basicReject(deliveryTag,false); } catch (IOException e) { //网络中断 e.printStackTrace(); } }
263. 订单
263.1 返回订单结算数据
@Override public OrderConfirmVo confirmOrder() { OrderConfirmVo orderConfirmVo = new OrderConfirmVo(); String userName = LoginUserInterceptor.loginUser.get(); //远程查询所有的收货地址列表 List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L); orderConfirmVo.setAddress(addresses); //远程查询购物车所有选中的购物项 List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem(); orderConfirmVo.setItems(currentUserCartItems); //查询用户积分 orderConfirmVo.setIntegration(1); //TODO : 防重令牌 //其他数据自动计算 return orderConfirmVo; }
263.2 Feign远程调用丢失请求头问题
远程查询购物车所有选中的购物项的serviecImpl
讯享网 / * 获取当前用户购物车所有购物项 * @return */ @Override public List<CartItem> getUserCartItems() { UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); if(userInfoTo.getUserId() == null){ return null; }else { String cartKey = CART_PREFIX + userInfoTo.getUserId(); List<CartItem> cartItems = getCartItems(cartKey); //获取所有被选中的购物项 List<CartItem> collect = cartItems.stream(). filter(cartItem -> cartItem.getCheck()). map(cartItem -> { //远程去数据库查询最新商品价格 BigDecimal price = productFeignService.getPrice(cartItem.getSkuId()); cartItem.setPrice(price); return cartItem; }). collect(Collectors.toList()); return collect; } }
feign再远程调用之前会经过拦截器,会对请求进行增强

template请求模板中没有参数也没有头

263.2.1 原因

263.2.2 解决

如上,给feign的远程调用之前增加拦截器,对feign远程调用进行增强.
Controller , 返回订单确认页需要的数据入口controller
/ * 点击"去结算"调到订单结算页,返回订单确认页需要的数据 * @return */ @GetMapping("/toTrade") public String toTrade(Model model , HttpServletRequest request){ OrderConfirmVo orderConfirmVo = orderService.confirmOrder(); model.addAttribute("orderConfirmData",orderConfirmVo); return "confirm"; }
RequestInterceptor 给feign远程调用增加拦截器
讯享网ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest();这里获取的request相当于入口controller中获取的request
@Configuration public class GuliFeignConfig { @Bean("requestInterceptor") public RequestInterceptor requestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { //RequestContextHolder 拿到刚进来的这个请求 ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //原请求 //同步请求头数据 , Cookie String cookie = request.getHeader("Cookie"); //给新请同步了原请求的cookie template.header("Cookie",cookie); } }; } }
263.3 Feign异步调用丢失上下文问题
263.3.1 异步编排
为了让加速程序运行,无关的查找进行异步编排
讯享网/ * 返回订单确认页需要的数据 * @return */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo orderConfirmVo = new OrderConfirmVo(); String userName = LoginUserInterceptor.loginUser.get(); //getAddressListTask CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //远程查询所有的收货地址列表 List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L); orderConfirmVo.setAddress(addresses); }, executor); //getCartItemsTask CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //远程查询购物车所有选中的购物项 List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem(); orderConfirmVo.setItems(currentUserCartItems); }, executor); //查询用户积分 orderConfirmVo.setIntegration(1); //TODO : 防重令牌 //阻塞等待全部异步任务完成 CompletableFuture.allOf(getAddressFuture,cartFuture).get(); //其他数据自动计算 return orderConfirmVo; }
263.3.2 原因
再RequestInterceptor中
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest();RequestContextHolder,的原理是ThreadLocal,同一线程内共享数据


如图,一号线程中ThreadLocal数据共享不到二号线程中的RequestInterceptor中
363.3.3 解决
异步编排在各自的线程在次设置头信息
讯享网 / * 返回订单确认页需要的数据 * @return */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { //.....获取之前的请求 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); //getAddressListTask CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //远程查询所有的收货地址列表 //每一个线程都来共享之前的请求数据 RequestContextHolder.setRequestAttributes(requestAttributes); //..... }, executor); //getCartItemsTask CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //远程查询购物车所有选中的购物项 //每一个线程都来共享之前的请求数据 RequestContextHolder.setRequestAttributes(requestAttributes); //.... }, executor); //... return orderConfirmVo; }
/ * 返回订单确认页需要的数据 * @return */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo orderConfirmVo = new OrderConfirmVo(); String userName = LoginUserInterceptor.loginUser.get(); RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); //getAddressListTask CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //远程查询所有的收货地址列表 RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L); orderConfirmVo.setAddress(addresses); }, executor); //getCartItemsTask CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //远程查询购物车所有选中的购物项 RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem(); orderConfirmVo.setItems(currentUserCartItems); }, executor); //查询用户积分 orderConfirmVo.setIntegration(1); //TODO : 防重令牌 //阻塞等待全部异步任务完成 CompletableFuture.allOf(getAddressFuture,cartFuture).get(); //其他数据自动计算 return orderConfirmVo; }
274.订单-接口幂等性讨论
274.1幂等性解决方案
274.1.1 token机制(验证码)


在查询显示订单页面信息的后端方法ToTrade中保存一个令牌α
在查询显示订单页面信息的页面中保存一个令牌β
每次调用submitOrder方法时对比令牌α和令牌β,如果相等则删除令牌,如果不能则为重复提交的情况
获取令牌 , 对比令牌 , 删除令牌 应该是一个原子操作 , 使用lua脚本进行实现
讯享网if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
274.1.2 代码
/ * 返回订单确认页需要的数据 * * @return */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { //...... //TODO : 防重令牌 String token = UUID.randomUUID().toString().replace("-",""); //给redis 端保存令牌 (server端) redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES); //给网页端 保存令牌(client端) orderConfirmVo.setOrderToken(token); //其他数据自动计算 return orderConfirmVo; }
生成订单.java
讯享网 @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo orderConfirmVo = new OrderConfirmVo(); String userName = LoginUserInterceptor.loginUser.get(); RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); //getAddressListTask CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //远程查询所有的收货地址列表 RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L); orderConfirmVo.setAddress(addresses); }, executor); //getCartItemsTask CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //远程查询购物车所有选中的购物项 RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem(); orderConfirmVo.setItems(currentUserCartItems); }, executor).thenRunAsync(() -> { //查询商品库存信息 List<OrderItemVo> items = orderConfirmVo.getItems(); List<Long> skuIds = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList()); R r = wareFeignService.getSkuHasStock(skuIds); List<SkuHasStockVo> skuHasStockVos = r.getData(new TypeReference<List<SkuHasStockVo>>() { }); if (skuHasStockVos != null && skuHasStockVos.size() > 0) { Map<Long, Boolean> skuHasStockMap = skuHasStockVos.stream().collect(Collectors.toMap( k -> k.getSkuId(), v -> v.getHasStock())); orderConfirmVo.setSkuHasStockMap(skuHasStockMap); } }); //查询用户积分 orderConfirmVo.setIntegration(1); //TODO : 防重令牌 String token = UUID.randomUUID().toString().replace("-",""); //给redis 端保存令牌 (server端) redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES); //给网页端 保存令牌(client端) orderConfirmVo.setOrderToken(token); //阻塞等待全部异步任务完成 CompletableFuture.allOf(getAddressFuture, cartFuture).get(); //其他数据自动计算 return orderConfirmVo; }
275.订单下单
275.1 连接字符串集合
StringUtils.collectionToDelimitedString(@Nullable Collection<?> coll, String delim);
以符号连接字符串集合
讯享网public class Test { public static void main(String[] args) { List<String> stringList = new ArrayList<>(); stringList.add("a"); stringList.add("b"); stringList.add("c"); String s = StringUtils.collectionToDelimitedString(stringList, ";"); System.out.println(s); } } //结果 : a;b;c
275.2 流程
下订单是一个复杂的流程 , 即使这里也是简化了很多步骤;
275.2.1 验证令牌
在前面分别在Redis和网页端保存了令牌 , 在点击提交订单即下单的时候 , 需要校验令牌.
同上解释 , 为了防止多次提交 , 获得令牌α , 对比令牌β , 删除令牌γ必须是原子性的 , 所以必须用lua脚本
/ * 验证令牌 */ String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; String orderToken = vo.getOrderToken(); //0 : 验证令牌失败 //1 : 验证令牌成功 , 删除令牌 Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName), orderToken ); if(result == 0L){ //令牌验证失败 ... }else{ //令牌验证成功 / * 创建订单 */ / * 验价 */ / * 锁定库存 */ }else{ //令牌校验失败 ... }
275.2.2 创建订单
给订单实体类的属性赋值 , 为保存数据库做好准备
275.2.3 验证价格
从页面数据获取并且计算后的价格也不不是最新价格 , 当用户下单的时候要验证价格(京东并没有进行价格验证 , 生成订单的时候的价格 , 与支付页面的实际应付金额 , 不一样是正常情况 , 京东并没有进行任何提示)
275.3.3 锁定库存
下单完成后需要锁定库存.
第一步 : 找出SkuId对应那些仓库有库存
讯享网 //找到每个商品在哪个仓库都有库存 List<OrderItemVo> locks = vo.getLocks(); List<SkuWareHasStockVo> collect = locks.stream().map(item -> { SkuWareHasStockVo stock = new SkuWareHasStockVo(); Long skuId = item.getSkuId(); stock.setSkuId(skuId); stock.setNum(item.getCount()); //查询这个商品在哪里有库存 List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId); stock.setWareId(wareIds); return stock; }).collect(Collectors.toList());
<!--查询这个商品在哪里有库存--> <select id="listWareIdHasSkuStock" resultType="java.lang.Long"> select ware_id from `wms_ware_sku` where sku_id = #{skuId} and stock-stock_lock > 0 </select>
第二步 : 按地理位置优先的顺序(这里并没有),扣减库存,只要有一个购物项库存不足都无法生成订单
讯享网 Boolean allLock = true; / * 锁定库存 */ for (SkuWareHasStockVo hasStockVo : collect) { Boolean skuStocked = false; Long skuId = hasStockVo.getSkuId(); List<Long> wareIds = hasStockVo.getWareId(); if(wareIds == null || wareIds.size() == 0 ){ //没有任何仓库有这个商品的库存 throw new NoStockException(skuId); } //尝试每一个仓库 for (Long wareId : wareIds) { //成功返回1 , 否则返回0 Long l = wareSkuDao.lockSkuStock(skuId,wareId,hasStockVo.getNum()); if(l == 1){ skuStocked = true; break; }else { //当前仓库锁定失败,重试下一个仓库 } if(skuStocked == false){ //当前商品所有仓库都没有锁住 throw new NoStockException(skuId); } }
<!--锁定并且扣减库存--> <select id="lockSkuStock" resultType="java.lang.Long"> UPDATE `wms_ware_sku` set stock = stock_locked + #{num} WHERE sku_id = #{skuId} and ware_id = #{wareId} and stock-stock_locked >= #{num} </select>
283.分布式事务
283.1 本地事务在分布式下的问题
- 远程服务假失败
- 远程服务其实成功了, 由于网络故障等没有返回.
- 导致 : 订单回滚 , 库存却扣减了.
- 远程服务执行完成 , 下面的其他方法出现问题
- 导致 : 已执行的远程请求 , 肯定不能回滚.
@Transactional 本地事务 , 在分布式系统中 只能控制住自己的回滚 , 控制不了其他服务的回滚
出现分布式事务的最大原因就是网络问题
mysql 默认隔离级别:
该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间 点的状态,因此,同样的 select 操作读到的结果会是一致的.
283.2 Spring本地事务
传播行为
OrderServiceImpl.class
讯享网@Transactional(timeout = 30) public void a(){ b(); c(); int a = 10/0; /* * a中有异常需要回滚 , b会回滚 , 而c不会 */ } @Transactional(propagation = Propagation.REQUIRED , timeout = 2) pubic void b(){ //b的传播行为为 Propagation.REQUIRED 与调用者共享事务 , 自己的设置全部失效 } @Transactional(propagation = Propagation.REQUIRED_NEW , timeout = 20) public void c(){ //c的传播行为为 Propagation.REQUIRED_NEW 自己新建一个事务 }
同一对象(this)内事务方法互相调用默认失效问题 , 原因 : 绕过了代理对象 , 事务使用代理对象来控制的
解决
- 引入aop-stater;spring-boot-starter-aop 引入 aspectj
- @EnableAspectAutoProxy(exposeProxy = true);开启aspectj动态代理功能
- 本类对象(this.)互相调用
@Transactional(timeout = 30) public void a(){ OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy(); orderService.b(); orderService.c(); int a = 10/0; /* * a中有异常需要回滚 , b会回滚 , 而c不会 */ } @Transactional(propagation = Propagation.REQUIRED , timeout = 2) pubic void b(){ //b的传播行为为 Propagation.REQUIRED 与调用者共享事务 , 自己的设置全部失效 } @Transactional(propagation = Propagation.REQUIRED_NEW , timeout = 20) public void c(){ //c的传播行为为 Propagation.REQUIRED_NEW 自己新建一个事务 }
285.分布式事务X-Raft理论
这里就不赘述了 , 详见谷粒商城P285
286.分布式事务X-Base理论
实际情况
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所 以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%(N 个 9),即保证 P 和 A,舍弃 C。=>AP
最终一致性
是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可 以采用适当的采取弱一致性,即最终一致性
287.分布式事务解决方案
287.1 柔性事务-TCC 事务补偿型方案
详见博客.
287.2 柔性事务-最大努力通知型方案
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种 方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调
287.3 柔性事务-可靠消息+最终一致性方案(异步确保型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。 防止消息丢失:
288.Seata-AT(很挫)
前面说性能很差还演示....
第一步 : 每一个参与事务的微服务的数据库必须创建undo_log表
讯享网CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
第二步 : 下载java项目 seata-server , 并且启动(TC) , 版本应该与seata-server一致 1.3.0

第三步 : 引入依赖
<!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
第四步 : 配置seata-server

registry.conf : 注册中心相关配置和配置中心相关配置.
file.conf : 配置文件
第五步 : 在事务方法上加上@GlobalTransactional注解
讯享网@GlobalTransactional @Transactional @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {}
第六步 : 所有想要用到分布式事务的微服务使用seata DatasourceProxy代理自己的数据源
@Configuration public class MySeataConfig { @Autowired private DataSourceProperties dataSourceProperties; @Bean public DataSource dataSource(DataSourceProperties dataSourceProperties){ HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); if(StringUtils.hasText(dataSourceProperties.getName())){ dataSource.setPoolName(dataSourceProperties.getName()); } return new DataSourceProxy(dataSource); } }
第七步 : 每个微服务导入 registry.conf 和 file.conf
290.最终一致性库存解锁逻辑
seata-samples/tcc at master · seata/seata-samples · GitHub TCC-github参考代码
利用延时队列 , 如果库存锁定成功 , 但是害怕订单下失败 , 可以把锁定库存成功的消息发给消息队列(延时) , 30分钟后再把成功锁定的消息发给库存服务 , 库存服务查询相应的订单 , 来对库存操作(如果失败就解锁库存等...)
291.RabbitMQ延时队列
291.1 使用场景

292.1 延时队列实现
利用死信路由实现延迟队列.
给队列设置过期时间

292.3 业务用元素

讯享网@Configuration public class MyMQConfig { @Bean public Queue orderDelayQueue(){ Map<String,Object> arguments = new HashMap<>(); arguments.put("x-dead-letter-exchange","order-event-exchange"); arguments.put("x-dead-letter-routing-key","order.release.order"); arguments.put("x-message-ttl",6000); Queue queue = new Queue("order.delay.queue", true, false, false, arguments); return queue; } @Bean public Queue orderReleaseOrderQueue(){ Queue queue = new Queue("order.release.order.queue", true, false, false); return queue; } @Bean public Exchange orderEventExchange(){ TopicExchange topicExchange = new TopicExchange("order-event-exchange", true, false); return topicExchange; } @Bean public Binding orderReleaseOrderBinding(){ return new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null ); } @Bean public Binding orderCreateOrderBinding(){ return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.order", null ); } }
293.利用延迟队列完成库存解锁功能
293.1 业务用元素

创建库存服务用队列元素
需要监听一下队列 , rabbitMQ发现指定元素没有 , 才会创建出来 , 因此可以增加一个消费的空方法(只使用一次)
@Configuration public class MyMQConfig { @RabbitListener(queues = "stock.release.stock.queue") public void handle(Message message){ } @Bean public Exchange stockEventExchange(){ return new TopicExchange("stock-event-exchange",true,false); } @Bean public Queue stockReleaseStockQueue(){ return new Queue("stock.release.stock.queue",true,false,false); } @Bean public Queue stockDelayQueue(){ Map<String,Object> arguments = new HashMap<>(); arguments.put("x-dead-letter-exchange","stock-event-exchange"); arguments.put("x-dead-letter-routing-key","stock.release"); arguments.put("x-message-ttl",12000); return new Queue("stock.delay.queue",true,false,false,arguments); } @Bean public Binding stockReleaseBinding(){ return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.release.#", null); } @Bean public Binding stockLockedBinding(){ return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.locked", null); } }
293.2 监听库存解锁(P294)
- 库存解锁的场景
- 下订单成功 , 订单过期没有支付 , 被系统自动取消或被用户手动取消.都要解锁库存.
- 下订单成功 , 库存锁定成功 , 接下来业务调用失败, 导致订单回滚 , 之前锁定的库存就要自动解锁.
293.3 远程调用时,排除路径
远程调用order服务的方法时, 由于order服务有需要登录的拦截器 , 而远程请求不需要登录 , 所以需要排除一些路径
讯享网 String uri = request.getRequestURI(); boolean match = new AntPathMatcher().match("/order/order/status/", uri); if(match){ return true; }
order服务的登录拦截器
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<String> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher(.match("/order/order/status/", uri);
if(match){
return true;
}
String userName = (String) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if(userName != null && !userName.equals("")){
loginUser.set(userName);
return true;
}else {
//没去登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
293.4 订单解锁的主动通知
正常情况

库存无法释放的情况

解决 : 当订单释放完成之后注定进行通知

讯享网 / * 订单释放直接和库存释放直接绑定 */ @Bean public Binding orderReleaseOtherBinding(){ return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.order.#", null); }
300.消息的不可靠因素
300.1 消息丢失
做好消息确认机制(无论是生产者还是消费者), consumer(手动ack)
每一个发送的消息都在数据库做好记录,定期将失败的消息再次发送一次
300.1.1 消息发送出去 , 由于网络问题没有抵达服务器
CREATE TABLE `mq_message` ( `message_id` char(32) NOT NULL, `content` text, `to_exchane` varchar(255) DEFAULT NULL, `routing_key` varchar(255) DEFAULT NULL, `class_type` varchar(255) DEFAULT NULL, `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`message_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb
解决 : 保证消息一定会发送出去 , 每一个消息都可以做好日志记录 (给数据库保存每一个消息的详细信息) , 定期扫描数据库将师表的消息再次发送一遍
讯享网try { rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo); }catch (Exception e){ //TODO 将没法成功发送的消息进行重试发送 }
300.1.2 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚 未持久化完成,宕机。


解决 : publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
300.1.3 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
解决 : 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
300.2 消息重复
300.2.1 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息 重新由unack变为ready,并发送给其他消费者
手动ack失败
解决一 : 将收到消息后的处理做成幂等的(增加各种判断 , 比如除了已锁定库存的状态之外不进行锁定库存操作之类的) ** 解决二 : rabbitMQ的每一个消息都有redelivered字段,可以获取是否 是被重新投递过来的,而不是第一次投递过来的
Boolean redelivered = message.getMessageProperties().getRedelivered();
300.3 消息积压
- 上线更多的消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
300.Ω 总结-注册
300.Ω.1 @GetMapping("/sms/sendCode")-发送验证码
讯享网@GetMapping("/sms/sendCode") @ResponseBody public R sendCode(@RequestParam("phone") String phone){ String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); if(!StringUtils.isEmpty(redisCode)){ long l = Long.parseLong(redisCode.split("_")[1]); //接口防刷 if(System.currentTimeMillis() - l < 60000){ return R.error(BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.VAILD_SMS_CODE_EXCEPTION.getMessage()); } } String code = UUID.randomUUID().toString().substring(0, 5) + "_" +System.currentTimeMillis(); System.err.println("我这里直接在控制台mock,获得验证码了"+code); redisTemplate.opsForValue().set( AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone , //sms:code: code, 10, TimeUnit.MINUTES); //设置验证码10分钟内有效 return R.ok(); }
①接口防刷 : 从redis中获取上次用户注册对应的验证码 , 获取后面设置的毫秒数 , 与再次进入接口的毫秒数详见 , 如果小于60000 , 则相当于多次发送验证码 , 在后端控制接口防刷 , 如果在前端设置60s内无法再次发送则,页面刷新又开始读秒 , 是控制不住的 , 这里并没有引入第三方的发送短信的功能 , 而是在控制台打印了验证码 , mock了发送验证的功能
②生成验证码 , 并在后面加上_当前毫秒数 , 并且存进redis中
300.Ω.2 @PostMapping("/regist")-注册
@PostMapping("/regist")
public String regist(@Valid UserRegisterVo vo , BindingResult result , RedirectAttributes redirectAttributes){
System.out.println("注册!");
System.out.println(result);
if(result.hasErrors()){
//校验出错,转发到注册页
Map<String, String> errorsMap = result.getFieldErrors().stream().collect(Collectors.toMap(
k -> k.getField(),
v -> v.getDefaultMessage()
));
redirectAttributes.addFlashAttribute("errors",errorsMap);
return "redirect:http://auth.gulimall.com/reg.html";
}
//注册 调用远程服务进行注册
String code = vo.getCode();
String s = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if(!StringUtils.isEmpty(s)){
if(code.equals(s.split("_")[0])){
//删除验证码,令牌机制
redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//验证码通过 注册 添加数据库
System.err.println("调用远程服务,将用户信息保存到数据库");
R r = memberFeignService.register(vo);
if(r.getCode() == 0){
return "redirect:http://auth.gulimall.com/login.html";
}else{
HashMap<String, String> errors = new HashMap<>();
errors.put("msg","error");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
}else{
HashMap<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
//校验出错 , 转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
}else{
HashMap<String, String> errors = new HashMap<>();
errors.put("code","验证码错误");
redirectAttributes.addFlashAttribute("errors",errors);
//校验出错 , 转发到注册页
return "redirect:http://auth.gulimall.com/reg.html";
}
//注册成功回到首页 , 回到登录页
//return "redirect:/login.html";
}
① 利用JSR-303 校验注册的表达数据是否符合格式
②校验验证码是否正确 , 如果正确就删除验证码的令牌(我感觉没有必要删除 , 如果用户服务保存用户信息的时候做了 , 此手机号已被注册的判断的话 , 就让他自动过期就好了 ) , 调用远程服务将用户信息保存在数据库
③保存用户信息 => 判断用户手机号是否已经存在 =>判断用户名是否存在 =>利用MD5加盐算法加密密码
controller
讯享网 / * 注册 */ @PostMapping("/regist") public R register(@RequestBody UserRegisterVo vo){ try { memberService.register(vo); }catch (PhoneExistException p){ return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(), BizCodeEnume.PHONE_EXIST_EXCEPTION.getMessage()); }catch (UserNameExistException u){ return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(), BizCodeEnume.USER_EXIST_EXCEPTION.getMessage()); } return R.ok(); }
serviceImpl
/ * 注册 * @param vo */ @Override public void register(UserRegisterVo vo) { MemberEntity entity = new MemberEntity(); //设置默认等级 entity.setLevelId(1L); //检查用户名和手机号是否唯一 checkPhoneUnique(vo.getPhone()); checkUsernameUnique(vo.getUserName()); entity.setMobile(vo.getPhone()); entity.setUsername(vo.getUserName()); entity.setNickname(vo.getUserName()); //密码 加盐 加密 存储 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String encode = passwordEncoder.encode(vo.getPassword()); entity.setPassword(encode); memberDao.insert(entity); }
300.Ω.3 @GetMapping("/login.html")-登录页
讯享网 @GetMapping("/login.html")
public String loginPage(HttpSession session){
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute == null){
//没登录
return "login";
}else{
return "redirect:http://gulimall.com";
}
}
①判断session是否存在
300.Ω.4 @PostMapping("/login")-登录
@PostMapping("/login")
//账号 15941148735 密码 rrrrrrr
public String login(UserLoginVo vo , RedirectAttributes redirectAttributes , HttpSession session ){
R login = memberFeignService.login(vo);
if(login.getCode() == 0){
//成功
String userName = "leifengyang mock";
//SpringData
System.err.println("将用户信息存储值redis session");
session.setAttribute(AuthServerConstant.LOGIN_USER,userName);
return "redirect:http://gulimall.com";
}else {
HashMap<String, String> errors = new HashMap<>();
errors.put("msg","账号或密码错误");
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
①调用远程用户服务的登录方法
controller
讯享网/ * 登录 */ @PostMapping("/login") public R login(@RequestBody UserLoginVo vo){ MemberEntity entity = memberService.login(vo); if(entity != null){ return R.ok(); }else { return R.error(BizCodeEnume.LOGINACCOUNT_PASSWORD_INVALID_EXCEPTION.getCode(), BizCodeEnume.LOGINACCOUNT_PASSWORD_INVALID_EXCEPTION.getMessage() ); } }
serviceImpl
/ * 登录 * @param vo * @return */ @Override public MemberEntity login(UserLoginVo vo) { String loginAccount = vo.getLoginAccount(); String password = vo.getPassword(); MemberEntity entity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginAccount). or().eq("mobile", loginAccount)); if(entity == null){ //登录失败 return null; }else{ String MD5Password = entity.getPassword(); BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); //密码匹配 boolean matches = passwordEncoder.matches(password, MD5Password); //password : 页面传过来的 password //MD5Password : 从数据库中取得的 加盐后的password // if(matches){ return entity; }else{ return null; } } }
②利用Spring家工具类BCryptPasswordEncoder来对页面传来的password和从数据库中取出的MD5password进行匹配
300.Ω.4 GulimallSessionConfig-自定义SpringSession
讯享网@EnableRedisHttpSession @Configuration public class GulimallSessionConfig { / * 子域共享session,扩大作用域 * @return */ @Bean public CookieSerializer cookieSerializer(){ DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); cookieSerializer.setDomainName("gulimall.com"); cookieSerializer.setCookieName("GULISESSION"); return cookieSerializer; } / * 序列化 * @return */ @Bean public RedisSerializer<Object> springSessionDefaultRedisSerializer(){ return new GenericJackson2JsonRedisSerializer(); } }
①扩大Session的作用于 , 凡是参与用户信息的服务都需要方大Session的作用于 , 由于auth.gulimall.com内设置的session的作用于为 auth.gulimall.com 其他的服务不可用 , 所以需要将session的作用于方大
②存储在redis中的信息默认是jdk序列化的 , 变为JSON序列化
300.α 总结-购物车
300.α.1 登录拦截器
public class CartInterceptor implements HandlerInterceptor { public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserInfoTo userInfoTo = new UserInfoTo(); HttpSession session = request.getSession(); String userName = (String) session.getAttribute(AuthServerConstant.LOGIN_USER); if(userName != null && !userName.equals("")){ //登录了 userInfoTo.setUserId(userName); threadLocal.set(userInfoTo); return true; }else{ //没登录 return false; } } }
300.α.2 获取购物车信息
controller
讯享网 /
* 获取购物车信息
* @param model
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(Model model){
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
cartServiceImpl.getCart()
这里getUserId 的作用主要是判断是否是临时购物车 , 但是实际上 , 临时购物车的功能很挫 , 包括京东自己都砍掉了
/ * 获取购物车信息 * @return */ @Override public Cart getCart() { Cart cart = new Cart(); UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); if(userInfoTo.getUserId() != null){ //登录了 String cartKey = CART_PREFIX + userInfoTo.getUserId(); List<CartItem> cartItems = getCartItems(cartKey); cart.setItems(cartItems); return cart; } return null; }
cartServiceImpl.getCartItems()
讯享网 private List<CartItem> getCartItems(String cartKey){ BoundHashOperations<String, Object, Object> hashOperations = redisTemplate.boundHashOps(cartKey); List<Object> values = hashOperations.values(); if(values != null && values.size()>0){ List<CartItem> collect = values.stream().map(obj -> { String str = (String) obj; CartItem cartItem = JSON.parseObject(str, CartItem.class); return cartItem; }).collect(Collectors.toList()); return collect; } return null; }
300.α.3 添加购物车
controller
写了两个映射 , 当添加购物车方法执行完毕后 , 为了防止刷新页面就重复添加 , 重定向到另一个页面 , 只做查询购物车数据操作
/
*
*RedirectAttributes redirectAttributes
* redirectAttributes.addFlashAttribute(); 将数据放在session里面可以再页面中取出,但是只能取一次
* redirectAttributes.addAttribute(); 将数据放在url后面
* 添加购物侧
* @return
*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes redirectAttributes
){
cartService.addToCart(skuId,num);
//将数据自动放到url后面
redirectAttributes.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
/
* 跳转到购物车添加成功页
* @param skuId
* @param model
* @return
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
//重定向到成功页面,再次查询购物车数据即可
CartItem cartItem = cartService.getCartItem(skuId);
model.addAttribute("item",cartItem);
return "success";
}
讯享网return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
//重定向到下一个页面 , 刷新下一个页面没有添加商品的操作 , 只有展示的商品详细信息的操作
cartServiceImpl.addToCart()
@Override public CartItem addToCart(Long skuId, Integer num) { //返回绑定对redis中hash类型的操作. BoundHashOperations<String, Object, Object> cartOps = getCartOps(); //判断购物车现在是否有此商品 String skuInfoJSON = (String) cartOps.get(skuId.toString()); if(StringUtils.isEmpty(skuInfoJSON)){ CartItem cartItem = new CartItem(); //购物车无此商品 //添加商品到购物车 getSkuInfoTask CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> { R skuInfo = productFeignService.getSkuInfo(skuId); SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {}); cartItem.setCheck(true); cartItem.setCount(num); cartItem.setImage(data.getSkuDefaultImg()); cartItem.setSkuId(skuId); cartItem.setPrice(data.getPrice()); }, executor); //远程查询sk销售属性u组合信息 getSkuSaleAttrTask CompletableFuture<Void> getSkuSaleAttrTask = CompletableFuture.runAsync(() -> { List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId); cartItem.setSkuAttr(skuSaleAttrValues); }, executor); //阻塞等待任务全部完成 CompletableFuture<Void> task = CompletableFuture.allOf(getSkuInfoTask, getSkuSaleAttrTask); try { task.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } //将添加的商品放入对应用户的redis hash 中 String cartItemJSON = JSON.toJSONString(cartItem); cartOps.put(skuId,cartItemJSON); return cartItem; } else { //购物车有此商品,修改数量 CartItem cartItem = JSON.parseObject(skuInfoJSON, CartItem.class); cartItem.setCount(cartItem.getCount()+num); cartOps.put(skuId.toString(),JSON.toJSONString(cartItem)); return cartItem; } }
cartServiceImpl.getCartOps()
返回绑定对redis中hash类型的操作.
讯享网/ * 获取到要操作的购物车 * @return */ private BoundHashOperations<String, Object, Object> getCartOps() { UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); String cartKey = ""; if(userInfoTo.getUserId() != null){ cartKey = CART_PREFIX + userInfoTo.getUserId(); } BoundHashOperations<String, Object, Object> stringObjectObjectBoundHashOperations = redisTemplate.boundHashOps(cartKey); return stringObjectObjectBoundHashOperations; }
300.α.4 改变购物车选中状态
controller
/
* 改变购物车项数量
* @param skuId
* @param num
* @return
*/
@GetMapping("/countItem")
public String countItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num
){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
cartServiceImpl.checkItem()
讯享网/ * 改变购物项的选择状态 * @param skuId * @param check * @return */ @Override public void checkItem(Long skuId, Integer check) { BoundHashOperations<String, Object, Object> cartOps = getCartOps(); CartItem cartItem = getCartItem(skuId); cartItem.setCheck(check==1?true:false); String cartItemJSON = JSON.toJSONString(cartItem); cartOps.put(skuId.toString(),cartItemJSON); }
300.α.5 改变购物项数量
controller
/
* 改变购物车项数量
* @param skuId
* @param num
* @return
*/
@GetMapping("/countItem")
public String countItem(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num
){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
cartServiceImpl.changeItemCount()
讯享网/ * 改变购物车项数量 * @param skuId * @param num * @return */ @Override public void changeItemCount(Long skuId, Integer num) { CartItem cartItem = getCartItem(skuId); cartItem.setCount(num); BoundHashOperations<String, Object, Object> cartOps = getCartOps(); cartOps.put(skuId.toString(),JSON.toJSONString(cartItem)); }
300.α.6 删除购物项
controller
/
* 删除购物车购物项
* @param skuId
* @return
*/
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}
cartServiceImpl.deleteItem()
讯享网/ * 删除购物车购物项 * @param skuId * @return */ @Override public void deleteItem(Long skuId) { BoundHashOperations<String, Object, Object> cartOps = getCartOps(); cartOps.delete(skuId.toString()); }
300.α.7 获取当前用户购物车所有购物项
controller
/ * 获取当前用户购物车所有购物项 * @return */ @ResponseBody @GetMapping("/currentUserCartItems") public List<CartItem> getCurrentUserCartItem(){ return cartService.getUserCartItems(); }
cartServiceImpl.getUserCartItems()
讯享网/ * 获取当前用户购物车所有购物项 * @return */ @Override public List<CartItem> getUserCartItems() { UserInfoTo userInfoTo = CartInterceptor.threadLocal.get(); if(userInfoTo.getUserId() == null){ return null; }else { String cartKey = CART_PREFIX + userInfoTo.getUserId(); List<CartItem> cartItems = getCartItems(cartKey); //获取所有被选中的购物项 List<CartItem> collect = cartItems.stream(). filter(cartItem -> cartItem.getCheck()). map(cartItem -> { //远程去数据库查询最新商品价格 BigDecimal price = productFeignService.getPrice(cartItem.getSkuId()); cartItem.setPrice(price); return cartItem; }). collect(Collectors.toList()); return collect; } }
cartServiceImpl.getCartItems()
private List<CartItem> getCartItems(String cartKey){ BoundHashOperations<String, Object, Object> hashOperations = redisTemplate.boundHashOps(cartKey); List<Object> values = hashOperations.values(); if(values != null && values.size()>0){ List<CartItem> collect = values.stream().map(obj -> { String str = (String) obj; CartItem cartItem = JSON.parseObject(str, CartItem.class); return cartItem; }).collect(Collectors.toList()); return collect; } return null; }
300.β 总结-订单
300.β.1 登录拦截器
LoginUserInterceptor.java
讯享网@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<String> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/status/", uri);
if(match){
return true;
}
String userName = (String) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if(userName != null && !userName.equals("")){
loginUser.set(userName);
return true;
}else {
//没去登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
300.β.2 给feign请求添加头信息
@Configuration public class GuliFeignConfig { @Bean("requestInterceptor") public RequestInterceptor requestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { //RequestContextHolder 拿到刚进来的这个请求 ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); if(attributes != null){ HttpServletRequest request = attributes.getRequest(); //原请求 if(request != null){ //同步请求头数据 , Cookie String cookie = request.getHeader("Cookie"); //给新请同步了原请求的cookie template.header("Cookie",cookie); } } } }; } }
300.β.3 返回订单确认页需要的数据
讯享网/ * 返回订单确认页需要的数据 * * @return */ @Transactional @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo orderConfirmVo = new OrderConfirmVo(); String userName = LoginUserInterceptor.loginUser.get(); RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); //getAddressListTask CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //远程查询所有的收货地址列表 RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> addresses = memberFeignService.getAddressByMemberId(1L); orderConfirmVo.setAddress(addresses); }, executor); //getCartItemsTask CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //远程查询购物车所有选中的购物项 RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem(); orderConfirmVo.setItems(currentUserCartItems); }, executor).thenRunAsync(() -> { //查询商品库存信息 List<OrderItemVo> items = orderConfirmVo.getItems(); List<Long> skuIds = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList()); R r = wareFeignService.getSkuHasStock(skuIds); List<SkuHasStockVo> skuHasStockVos = r.getData(new TypeReference<List<SkuHasStockVo>>() { }); if (skuHasStockVos != null && skuHasStockVos.size() > 0) { Map<Long, Boolean> skuHasStockMap = skuHasStockVos.stream().collect(Collectors.toMap( k -> k.getSkuId(), v -> v.getHasStock())); orderConfirmVo.setSkuHasStockMap(skuHasStockMap); } }); //查询用户积分 orderConfirmVo.setIntegration(1); //TODO : 防重令牌 String token = UUID.randomUUID().toString().replace("-",""); //给redis 端保存令牌 (server端) redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES); //给网页端 保存令牌(client端) orderConfirmVo.setOrderToken(token); //阻塞等待全部异步任务完成 CompletableFuture.allOf(getAddressFuture, cartFuture).get(); //其他数据自动计算 return orderConfirmVo; }
让我们看看这个方法做了多少事?
①可以看到远程调用购物车的服务并没有传入参数 , 而是从session中获取的用户信息再去查询购物车 , 远程的getCurrentUserCartItem()方式是通过拦截器的LocalThread来获取用户信息的,但是由于远程调用并不像正常调用 , 无法从原请求中获取头信息, 所以导致session为空 , 因此需要对feign的远程调用的template加头信息 ,详见263.2
②采用了异步编排的方式进行数据获取 , 详见 263.3
③对于防止订单重复提交的令牌设置
//TODO : 防重令牌 String token = UUID.randomUUID().toString().replace("-",""); //给redis 端保存令牌 (server端) redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName,token,30, TimeUnit.MINUTES); //给网页端 保存令牌(client端) orderConfirmVo.setOrderToken(token);
300.β.4 重头戏-下单
controller
讯享网/ * 下单功能 */ @PostMapping("/submitOrder") public String submitOrder(OrderSubmitVo vo , Model model){ //验证令牌 //验证价格 //扣减击飞 //锁定库存 //保存订单 SubmitOrderResponseVo responseVo = orderService.submitOrder(vo); if(responseVo.getCode() == 0){ //下单成功 -> 支付页 model.addAttribute("submitOrderResp",responseVo); return "pay"; }else{ //下单失败 -> 重新确认 String msg = "下单失败"; switch (responseVo.getCode()){ case 1 : msg += "订单信息过期,请刷新后再次提交" ;break; case 2 : msg += "订单商品价格发送变化,请确认后再次提交" ;break; case 3 : msg += "库存锁定失败,商品库存不足" ;break; } return "redirect:http://order.gulimall.com/toTrade"; } }
orderServiceImpl.submitOrder()
/ * 下单功能 * * 验证令牌 * 验证价格 * 锁定库存 * 保存订单 */ @Transactional @Override public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) { confirmVoThreadLocal.set(vo); SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo(); String userName = LoginUserInterceptor.loginUser.get(); responseVo.setCode(0); / * 验证令牌 */ String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; String orderToken = vo.getOrderToken(); //0 : 验证令牌失败 //1 : 验证令牌成功 , 删除令牌 Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName), orderToken ); if(result == 0L){ //令牌验证失败 responseVo.setCode(1); return responseVo; }else{ //令牌验证成功 / * 创建订单 */ OrderCreateTo order = createOrder(); / * 验价 */ BigDecimal payAmount = order.getOrder().getPayAmount(); BigDecimal payPrice = vo.getPayPrice(); if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){ //验证价格成功 / * 保存订单 */ saveOrder(order); / * 锁定库存 */ WareSkuLockVo lockVo = new WareSkuLockVo(); lockVo.setOrderSn(order.getOrder().getOrderSn()); List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> { OrderItemVo itemVo = new OrderItemVo(); itemVo.setSkuId(item.getSkuId()); itemVo.setCount(item.getSkuQuantity()); itemVo.setTitle(item.getSkuName()); return itemVo; }).collect(Collectors.toList()); lockVo.setLocks(locks); R r = wareFeignService.orderLockStock(lockVo); if(r.getCode() == 0){ //锁定成功 OrderVo orderVo = new OrderVo(); BeanUtils.copyProperties(order.getOrder(),order); responseVo.setOrderVo(orderVo); //订单创建成功发送消息给MQ rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder()); return responseVo; }else{ throw new RuntimeException("无库存"); //return responseVo; } }else{ responseVo.setCode(2); return responseVo; } } }
orderServiceImpl.createOrder()
讯享网/ * 封装订单数据 * @return */ private OrderCreateTo createOrder(){ OrderCreateTo createTo = new OrderCreateTo(); //生成订单号 String orderSn = IdWorker.getTimeId(); //创建订单 OrderEntity orderEntity = buildOrder(orderSn); //获取到所有所有订单项信息 List<OrderItemEntity> orderItemEntities = buildOrderItems(orderSn); //计算价格相关 computePrice(orderEntity,orderItemEntities); createTo.setOrder(orderEntity); createTo.setOrderItems(orderItemEntities); return createTo; }
orderServiceImpl.buildOrderItems()
/ * 构建所有订单项数据 * @param * @param orderSn * @return */ private List<OrderItemEntity> buildOrderItems(String orderSn) { //最后确定每个购物项的价格 List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItem(); if(currentUserCartItems != null && currentUserCartItems.size()>0){ List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> { OrderItemEntity itemEntity = buildOrderItem(cartItem); itemEntity.setOrderSn(orderSn); return itemEntity; }).collect(Collectors.toList()); return itemEntities; } return null; }
orderServiceImpl.buildOrder()
讯享网private OrderEntity buildOrder(String orderSn) { String userName = LoginUserInterceptor.loginUser.get(); OrderEntity entity = new OrderEntity(); entity.setOrderSn(orderSn); entity.setMemberId(1L); OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get(); //获取收货地址信息 R fare = wareFeignService.getFare(orderSubmitVo.getAddrId()); FareResponseVo fareResponseVo = fare.getData(new TypeReference<FareResponseVo>() {}); //设置运费信息 entity.setFreightAmount(fareResponseVo.getFare()); //设置收货人信息 entity.setReceiverCity(fareResponseVo.getMemberAddressVo().getCity()); entity.setReceiverDetailAddress(fareResponseVo.getMemberAddressVo().getDetailAddress()); entity.setReceiverName(fareResponseVo.getMemberAddressVo().getName()); entity.setReceiverPhone(fareResponseVo.getMemberAddressVo().getPhone()); entity.setReceiverPostCode(fareResponseVo.getMemberAddressVo().getPostCode()); entity.setReceiverProvince(fareResponseVo.getMemberAddressVo().getProvince()); entity.setReceiverRegion(fareResponseVo.getMemberAddressVo().getRegion()); //设置订单的相关状态信息 entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode()); entity.setAutoConfirmDay(7); entity.setDeleteStatus(0); return entity; }
orderServiceImpl.computePrice()
/ * 计算价格相关 * @param orderEntity * @param orderItemEntities */ private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) { BigDecimal total = new BigDecimal("0.0"); BigDecimal coupon = new BigDecimal("0.0"); BigDecimal integration = new BigDecimal("0.0"); BigDecimal promotion = new BigDecimal("0.0"); BigDecimal gift = new BigDecimal("0.0"); BigDecimal growth = new BigDecimal("0.0"); for (OrderItemEntity entity : orderItemEntities) { coupon = coupon.add(entity.getCouponAmount()); integration = integration.add(entity.getIntegrationAmount()); promotion = promotion.add(entity.getPromotionAmount()); total = total.add(entity.getRealAmount()); gift = gift.add(new BigDecimal(entity.getGiftIntegration().toString())); growth = growth.add(new BigDecimal(entity.getGiftGrowth().toString())); } //订单价格相关 orderEntity.setTotalAmount(total); //应付总额 orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount())); orderEntity.setPromotionAmount(promotion); orderEntity.setIntegrationAmount(integration); orderEntity.setCouponAmount(coupon); //设置积分等信息 orderEntity.setIntegration(gift.intValue()); orderEntity.setGrowth(growth.intValue()); }
orderServiceImpl.saveOrder()
讯享网 / * 保存订单到数据库 * @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); }
orderServiceImpl.buildOrderItem()
/ * 构建某一个订单项 * @return * @param cartItem */ private OrderItemEntity buildOrderItem(OrderItemVo cartItem) { OrderItemEntity itemEntity = new OrderItemEntity(); / * 订单信息 : 订单号 */ / * 商品SPU信息 */ Long skuId = cartItem.getSkuId(); R r = productFeignService.getSpuInfoBySkuId(skuId); SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {}); itemEntity.setSpuId(data.getId()); itemEntity.setSpuBrand(data.getBrandId().toString()); itemEntity.setSpuName(data.getSpuName()); itemEntity.setCategoryId(data.getCatalogId()); / * 商品sku信息 */ itemEntity.setSkuId(cartItem.getSkuId()); itemEntity.setSkuName(cartItem.getTitle()); itemEntity.setSkuPic(cartItem.getImage()); itemEntity.setSkuPrice(cartItem.getPrice()); String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";"); itemEntity.setSkuAttrsVals(skuAttr); itemEntity.setSkuQuantity(cartItem.getCount()); / * 优惠券信息[不做] */ / * 积分信息 */ itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue()); itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())).intValue()); / * 订单项价格信息 */ itemEntity.setPromotionAmount(new BigDecimal("0")); itemEntity.setCouponAmount(new BigDecimal("0")); itemEntity.setIntegrationAmount(new BigDecimal("0")); //当前订单项的实际金额 BigDecimal origin = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString())); BigDecimal subtract = origin.subtract(itemEntity.getCouponAmount()). subtract(itemEntity.getPromotionAmount()). subtract(itemEntity.getIntegrationAmount()); itemEntity.setRealAmount(subtract); return itemEntity; }
①利用Lua脚本验证令牌
讯享网/ * 验证令牌 */ String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; String orderToken = vo.getOrderToken(); //0 : 验证令牌失败 //1 : 验证令牌成功 , 删除令牌 Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + userName), orderToken );
②创建订单 , 这里就是set 订单项属性 , 这里就不在赘述了
/ * 创建订单 */ OrderCreateTo order = createOrder();
③价格验证
这里 payPrice 是页面来的数据 , payAmount是最新的从数据库中获得的商品价格
讯享网/ * 验价 */ BigDecimal payAmount = order.getOrder().getPayAmount(); BigDecimal payPrice = vo.getPayPrice(); if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){ //...... }
④保存订单
将订单保存在数据库中
/ * 保存订单到数据库 * @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); }
⑤锁定库存
讯享网 / * 锁定库存 */ WareSkuLockVo lockVo = new WareSkuLockVo(); lockVo.setOrderSn(order.getOrder().getOrderSn()); List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> { OrderItemVo itemVo = new OrderItemVo(); itemVo.setSkuId(item.getSkuId()); itemVo.setCount(item.getSkuQuantity()); itemVo.setTitle(item.getSkuName()); return itemVo; }).collect(Collectors.toList()); lockVo.setLocks(locks); //远程锁定库存 R r = wareFeignService.orderLockStock(lockVo);
⑤.① WareSkuController =>远程锁定库存controller
/ * 锁定库存 * * @param vo * @return */ @GetMapping("/lock/order") public R orderLockStock(@RequestBody WareSkuLockVo vo) { try { Boolean stockResults = wareSkuService.orderLockStock(vo); return R.ok(); }catch (NoStockException e){ return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMessage()); } }
⑤.② WareSkuServiceImpl =>远程锁定库存ServiceImpl
讯享网/ * 锁定库存 * @param vo * @return */ @Transactional @Override public Boolean orderLockStock(WareSkuLockVo vo) { / * 保存库存工作详情 , 相当于 TCC 的 frozen , 用于追溯库存锁定状态 */ WareOrderTaskEntity taskEntity = new WareOrderTaskEntity(); taskEntity.setOrderSn(vo.getOrderSn()); wareOrderTaskService.save(taskEntity); //找到每个商品在哪个仓库都有库存 List<OrderItemVo> locks = vo.getLocks(); List<SkuWareHasStockVo> collect = locks.stream().map(item -> { SkuWareHasStockVo stock = new SkuWareHasStockVo(); Long skuId = item.getSkuId(); stock.setSkuId(skuId); stock.setNum(item.getCount()); //查询这个商品在哪里有库存 List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId); stock.setWareId(wareIds); return stock; }).collect(Collectors.toList()); Boolean allLock = true; / * 锁定库存 */ for (SkuWareHasStockVo hasStockVo : collect) { Boolean skuStocked = false; Long skuId = hasStockVo.getSkuId(); List<Long> wareIds = hasStockVo.getWareId(); if(wareIds == null || wareIds.size() == 0 ){ //没有任何仓库有这个商品的库存 throw new NoStockException(skuId); } //尝试每一个仓库 // 成功 : // 失败 : for (Long wareId : wareIds) { //成功返回1 , 否则返回0 Long l = wareSkuDao.lockSkuStock(skuId,wareId,hasStockVo.getNum()); if(l == 1){ skuStocked = true; //TODO : 告诉MQ库存锁定成功 WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, null, hasStockVo.getNum(), taskEntity.getId(), wareId, 1); wareOrderTaskDetailService.save(entity); //发送库存锁定成功的消息 StockLockedTo lockedTo = new StockLockedTo(); lockedTo.setId(taskEntity.getId()); StockDetailTo stockDetailTo = new StockDetailTo(); BeanUtils.copyProperties(entity,stockDetailTo); lockedTo.setStockDetailTo(stockDetailTo); rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo); break; }else { //当前仓库锁定失败,重试下一个仓库 } if(skuStocked == false){ //当前商品所有仓库都没有锁住 throw new NoStockException(skuId); } } } return true; }
自己看代码吧 我已经说不清楚了......
301-309 支付 略
302 内网穿透
304.SpringMCV 指定返回类型
/
*
*/
@GetMapping(value = "/payOrder" , produces = "text/html")
public String payOrder(){
//...
return pay;
}
支付成功后支付宝返回支付成功页面的html代码(pay) , 这既不是json , 也不是服务的template模板页面
306 远程传输对象
远程调用传输对象推荐用@ResponseBody
讯享网/ * 查询当前登录用户的订单分页信息 */ @PostMapping("/listWithItem") public R listWithItem(@ResponseBody Map<String, Object> params){ PageUtils page = orderService.queryPageWithItem(params); return R.ok().put("page", page); }
307 内网穿透到Nginx 请求Host头不匹配

通知结果异步回调
@PostMapping("/payed/notif") public String handleAlipayed(HttpServletRequest request){ //... return success; }
修改nginx 配置文件X
讯享网location /payed/nofif { proxy_set_header Host order.gulimall.com; proxy_pass http://gulimall }
修改nginx 配置文件Ω
server { listen 08; server_name gulimall.com *.guilimall.com 497n86m7k.52http.net; }
309 收单
①在支付页不动 , 等待订单过期 , 再支付 , 这个时候库存是已解锁状态

1.在支付页面一定时间不支付则不能支付.
2.在下单成功之后 , 队列收到消息时(30min) , 手动调用支付宝收单 ,让此次交易无法支付
讯享网 @Service public class OrderCloseListener { @Autowired private OrderService orderService; / * 延迟关单 * @param entity * @param channel * @param message * @throws IOException */ @RabbitListener(queues = "order.release.order.queue") public void listener(OrderEntity entity , Channel channel , Message message) throws IOException { System.out.println("收到过期的订单信息 , 准备关闭订单 " + entity.getOrderSn()); try { orderService.closeOrder(entity); //TODO : 手动调用支付宝收单 channel.basicAck(message.getMessageProperties().getDeliveryTag() , false); }catch (Exception e){ channel.basicReject(message.getMessageProperties().getDeliveryTag() , true); } } }
310.秒杀-高并发的商家不定期促销
秒杀具有瞬间高并发的特点 , 针对这一特点 , 必须要做到限流 + 异步 + 缓存 (页面静态化) + 独立部署(专门的秒杀服务).

312.秒杀-Spring定时任务和异步任务
- 定时任务
- @EnableScheduling 开启定时任务
- @Schedule 开启一个定时任务
- 异步任务 : 我们希望即使定时任务的执行耗时大于执行时间的不长 , 也不会阻塞
- @EnableAsync 开启异步任务功能
- @Async 给希望异步执行的方法上标注注解
@Slf4j @Component @EnableAsync @EnableScheduling public class HelloSchedule { @Async @Scheduled(cron = "* * * * * ?") public void hello() throws InterruptedException { log.info("hello...."); TimeUnit.SECONDS.sleep(3); } }
313秒杀- 时间日期处理
其他计算如法炮制.获取当天00:00 到 两天后的23:59
讯享网LocalDate now = LocalDate.now(); //2022-11-11 LocalDate plus1 = now.plusDays(1); //当天加一天 2022-11-12 LocalDate plus2 = now.plusDays(2); //当前天加两天 2022-11-13 LocalTime min = LocalTime.MIN; //00:00 LocalTime max = LocalTime.MAX; //23:59 //拼接 LocalDateTime start = LocalDateTime.of(now, min); //2022-11-11 00:00 LocalDateTime end = LocalDateTime.of(plus2, max); //2022-11-13 23:59
314.秒杀-秒杀商品上架
/ * 每天晚上3点执行 上架最近三天需要秒杀的商品 */ @Override public void uploadSeckillSkuLatest3Days() { R session = couponFeignService.getLatest3DaySession(); if(session.getCode() == 0){ List<SeckillSessionWithSkus> sessionData = session.getData(new TypeReference<List<SeckillSessionWithSkus>>() { }); //缓存活动信息 saveSessionInfos(sessionData); //缓存活动的关联商品信息 saveSessionSkuInfos(sessionData); } }
缓存活动信息 key: seckill:session:_ value:List<String> {1,2,3,4,5}
讯享网 / * 缓存活动信息 key: seckill:session:_ value:List<String> {1,2,3,4,5} * @param sessions */ private void saveSessionInfos(List<SeckillSessionWithSkus> sessions){ sessions.stream().forEach(session -> { Long startTime = session.getStartTime().getTime(); Long endTime = session.getEndTime().getTime(); String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime; List<String> collect = session.getRelationSkus().stream().map(item -> item.getId().toString()).collect(Collectors.toList()); //缓存活动信息 stringRedisTemplate.opsForList().leftPushAll(key,collect); }); }
缓存活动的关联商品信息 key:SKUKILL_CACHE_PREFIX field:skuId value:skuInfo
/ * 缓存活动的关联商品信息 key:SKUKILL_CACHE_PREFIX field:skuId value:skuInfo * @param sessions */ private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions){ sessions.stream().forEach(session -> { //准备hash操作 BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps("SKUKILL_CACHE_PREFIX"); session.getRelationSkus().stream().forEach(seckillSkuVo -> { //缓存商品详情 SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo(); // sku的基本数据 R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId()); if(skuInfo.getCode() == 0){ SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() { }); redisTo.setSkuInfo(info); } // sku秒杀数据 BeanUtils.copyProperties(seckillSkuVo,redisTo); //设置上当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //随机码 seckill?skuId=1&key=d35ga3f5 String token = UUID.randomUUID().toString().replace("-", ""); redisTo.setRandomCode(token); String s = JSON.toJSONString(seckillSkuVo); //使用库存作为分布式信号量 限流 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); //商品可以秒杀的数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); ops.put(seckillSkuVo.getSkuId().toString(),s); }); }); }
317.秒杀-幂等性保证

保证redis中存储的hash sku信息不重复添加.
①利用分布式锁控制秒杀商品的上架
讯享网@Slf4j @Component @EnableAsync @EnableScheduling public class SeckillScheduled { @Autowired private SecKillService secKillService; @Autowired private RedissonClient redissonClient; private final String upload_lock = "seckill:upload:lock"; //TODO : 幂等性处理 上架之后就不用再上架了 @Async @Scheduled(cron = "0 0 3 * * ?") //每天晚上凌晨3点执行 public void hello() throws InterruptedException { log.info("上架秒杀的商品信息......"); //重复上架无需处理 RLock lock = redissonClient.getLock(upload_lock); lock.lock(10, TimeUnit.SECONDS); try { secKillService.uploadSeckillSkuLatest3Days(); }finally { lock.unlock(); } } }
②校验key时候已经存在 , 防止重复提交秒杀活动
/ * 缓存活动信息 key: seckill:session:_ value:List<String> {1,2,3,4,5} * @param sessions */ private void saveSessionInfos(List<SeckillSessionWithSkus> sessions){ sessions.stream().forEach(session -> { Long startTime = session.getStartTime().getTime(); Long endTime = session.getEndTime().getTime(); String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime; Boolean hasKey = stringRedisTemplate.hasKey(key); if(!hasKey){ List<String> collect = session.getRelationSkus().stream().map(item -> item.getSkuId().toString()).collect(Collectors.toList()); //缓存活动信息 stringRedisTemplate.opsForList().leftPushAll(key,collect); } }); }
③校验key时候已经存在 , 防止重复提交hash->sku信息
讯享网 / * 缓存活动的关联商品信息 key:SKUKILL_CACHE_PREFIX field:skuId value:skuInfo * @param sessions */ private void saveSessionSkuInfos(List<SeckillSessionWithSkus> sessions){ sessions.stream().forEach(session -> { //准备hash操作 BoundHashOperations<String, Object, Object> ops = stringRedisTemplate.boundHashOps("SKUKILL_CACHE_PREFIX"); session.getRelationSkus().stream().forEach(seckillSkuVo -> { String token = UUID.randomUUID().toString().replace("-", ""); if(!ops.hasKey(seckillSkuVo.getPromotionId().toString() +"_"+seckillSkuVo.getSkuId().toString())){ //缓存商品详情 SecKillSkuRedisTo redisTo = new SecKillSkuRedisTo(); // sku的基本数据 R skuInfo = productFeignService.getSkuInfo(seckillSkuVo.getSkuId()); if(skuInfo.getCode() == 0){ SkuInfoVo info = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() { }); redisTo.setSkuInfo(info); } // sku秒杀数据 BeanUtils.copyProperties(seckillSkuVo,redisTo); //设置上当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //随机码 seckill?skuId=1&key=d35ga3f5 redisTo.setRandomCode(token); String s = JSON.toJSONString(seckillSkuVo); ops.put(seckillSkuVo.getPromotionId()+"_"+seckillSkuVo.getSkuId().toString(),s); //使用库存作为分布式信号量 限流 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); //商品可以秒杀的数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); } }

318 秒杀-查询秒杀商品
/ * 返回当前时间可以参与秒杀商品信息 * @return */ @Override public List<SecKillSkuRedisTo> getCurrentSeckillSkus() { //确定当前时间属于哪个秒杀场次 long time = new Date().getTime(); Set<String> keys = stringRedisTemplate.keys(SESSIONS_CACHE_PREFIX + "*"); for (String key : keys) { String replace = key.replace(SESSIONS_CACHE_PREFIX, ""); String[] s = replace.split("_"); long start = Long.parseLong(s[0]); long end = Long.parseLong(s[1]); if(time >= start && time <= end){ List<String> range = stringRedisTemplate.opsForList().range(key, -100, 100); BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); List<String> list = hashOps.multiGet(range); if(list != null){ List<SecKillSkuRedisTo> collect = list.stream().map(item -> { SecKillSkuRedisTo redis = JSON.parseObject((String) item, SecKillSkuRedisTo.class); return redis; }).collect(Collectors.toList()); return collect; } break; } } return null; }
319.秒杀-商品秒杀
讯享网/ * 获取商品秒杀信息 */ @Override public SecKillSkuRedisTo getSkuSeckillInfo(Long skuId) { //找到所有需要参与秒杀的商品的key BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); Set<String> keys = hashOps.keys(); if(keys != null && keys.size() > 0){ String regx = "\\d" + skuId; for (String key : keys) { if(Pattern.matches(regx,key)){ String json = hashOps.get(key); SecKillSkuRedisTo skuRedisTo = JSON.parseObject(json, SecKillSkuRedisTo.class); //随机码 , 只有当前时间在秒杀场次时间范围内则存储随机码 long current = new Date().getTime(); Long startTime = skuRedisTo.getStartTime(); Long endTime = skuRedisTo.getEndTime(); if(!(current >= startTime && current <= endTime)){ skuRedisTo.setRandomCode(null); } return skuRedisTo; } } } return null; }
320.秒杀-秒杀系统设计


321.秒杀-登录检查
加登录拦截器 和 其他服务一样.
322.秒杀-秒杀流程
流程一


流程二

无数据库交互 , 无远程调用.
①在登录拦截器完成了登录判断
②检验合法性 =>时间是否在秒杀活动范围内 =>校验商品随机码和商品Id
③验证用户购买限制
④生成订单号
/ * kill 秒杀 * @param killId * @param key * @param num * @return */ @Override public String kill(String killId, String key, Integer num) { String userName = LoginUserInterceptor.loginUser.get(); BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); String json = hashOps.get(killId); if(StringUtils.isEmpty(json)){ return null; }else { SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class); //校验合法性 Long startTime = redis.getStartTime(); Long endTime = redis.getEndTime(); long time = new Date().getTime(); long userKeyTTL = endTime - time; //校验时间的合法性 if(time >= startTime && time <= endTime){ //校验随机码和商品id String randomCode = redis.getRandomCode(); String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId(); if(randomCode.equals(key) && killId.equals(skuId)){ //验证购物数量是否合理 if(num <= redis.getSeckillLimit()){ //验证这个人是否已经购买过 , 幂等性 , 如果秒杀成功 就去站位 userId_SessionId_skuId String redisKey = userName + "_" + redis.getPromotionSessionId() + "_" + redis.getSkuId(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), userKeyTTL, TimeUnit.MILLISECONDS); if(aBoolean){ //占位成功说明没有买过 //获取信号量 tryAcquire => 非阻塞 acquire=>阻塞 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE); try { boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS); String timeId = IdWorker.getTimeId(); //TODO : 发送MQ return timeId; // 返回订单号 } catch (InterruptedException e) { e.printStackTrace(); } }else{ //说明已经买过了 return null; } } }else{ return null; } } } return null; } }
323.秒杀-生成订单号发送给MQ

异步下订单.
323.1 秒杀用队列元素
讯享网 / * 秒杀用队列 */ @Bean public Queue orderSeckillOrderQueue(){ return new Queue("order.seckill.order.queue",true,false,false); } / * 秒杀用绑定关系 * @return */ @Bean public Binding orderSeckillOrderQueueBinding(){ return new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.seckill.order", null); }
323.2 发送
String timeId = IdWorker.getTimeId(); //TODO : 发送MQ SeckillOrderTo orderTo = new SeckillOrderTo(); orderTo.setOrderSn(timeId); orderTo.setMemberId(1L); //mock userId orderTo.setNum(num); orderTo.setPromotionSessionId(redis.getPromotionSessionId()); orderTo.setSkuId(redis.getSkuId()); orderTo.setSeckillPrice(1); rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo); return timeId; // 返回订单号
讯享网/ 上架商品的时候每一个数据都有过期时间 * kill 秒杀 * @param killId * @param key * @param num * @return */ @Override public String kill(String killId, String key, Integer num) { String userName = LoginUserInterceptor.loginUser.get(); BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX); String json = hashOps.get(killId); if(StringUtils.isEmpty(json)){ return null; }else { SecKillSkuRedisTo redis = JSON.parseObject(json, SecKillSkuRedisTo.class); //校验合法性 Long startTime = redis.getStartTime(); Long endTime = redis.getEndTime(); long time = new Date().getTime(); long userKeyTTL = endTime - time; //校验时间的合法性 if(time >= startTime && time <= endTime){ //校验随机码和商品id String randomCode = redis.getRandomCode(); String skuId = redis.getPromotionSessionId() + "_" + redis.getSkuId(); if(randomCode.equals(key) && killId.equals(skuId)){ //验证购物数量是否合理 if(num <= redis.getSeckillLimit()){ //验证这个人是否已经购买过 , 幂等性 , 如果秒杀成功 就去站位 userId_SessionId_skuId String redisKey = userName + "_" + redis.getPromotionSessionId() + "_" + redis.getSkuId(); Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), userKeyTTL, TimeUnit.MILLISECONDS); if(aBoolean){ //占位成功说明没有买过 //获取信号量 tryAcquire => 非阻塞 acquire=>阻塞 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE); boolean b = semaphore.tryAcquire(num); if(b){ String timeId = IdWorker.getTimeId(); //TODO : 发送MQ SeckillOrderTo orderTo = new SeckillOrderTo(); orderTo.setOrderSn(timeId); orderTo.setMemberId(1L); //mock userId orderTo.setNum(num); orderTo.setPromotionSessionId(redis.getPromotionSessionId()); orderTo.setSkuId(redis.getSkuId()); orderTo.setSeckillPrice(1); rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",orderTo); return timeId; // 返回订单号 } return null; }else{ //说明已经买过了 return null; } } }else{ return null; } } } return null; }
323.3 监听
@Slf4j @RabbitListener(queues = {"order.seckill.order.queue"}) @Component public class OrderSeckillListener { @Autowired OrderService orderService; / * 创建秒杀订单 * @param seckillOrderTo * @param channel * @param message * @throws IOException */ @RabbitHandler public void listener(SeckillOrderTo seckillOrderTo , Channel channel , Message message) throws IOException { try { log.info("准备创建秒杀单的详细信息...."); orderService.createSeckillOrder(seckillOrderTo); channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); }catch (Exception e){ channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); } } }
323.4 保存订单
他并没有扣库存的动作?
讯享网/ * 创建秒杀订单 * @param seckillOrderTo */ @Override public void createSeckillOrder(SeckillOrderTo seckillOrderTo) { OrderEntity orderEntity = new OrderEntity(); orderEntity.setOrderSn(seckillOrderTo.getOrderSn()); orderEntity.setMemberId(seckillOrderTo.getMemberId()); orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode()); int payAmount = seckillOrderTo.getSeckillPrice() * seckillOrderTo.getNum(); orderEntity.setPayAmount(new BigDecimal(payAmount + "")); this.save(orderEntity); //保存订单项信息 OrderItemEntity itemEntity = new OrderItemEntity(); itemEntity.setOrderSn(seckillOrderTo.getOrderSn()); itemEntity.setRealAmount(new BigDecimal(payAmount + "")); //获取当前SKU的详细信息进行设置 itemEntity.setSkuQuantity(seckillOrderTo.getNum()); orderItemService.save(itemEntity); }
325.Sentinel-高并发方法论
限流&熔断&降级
327.Sentinel-整合SpringBoot
第一步 : 引入依赖
<!--sentinel--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
第二步 : 启动sentinel
讯享网java -jar

第三步 : 配置
spring: sentinel: transport: dashboard: localhost:8333 #控制台端口 port: 8719 #服务与控制台传输数据端口
331.Sentinel-Feign
331.1 调用方熔断保护
第一步 : 引入sentinel依赖
第二步 : 引入openFeign
第三步 : 配置
讯享网feign: sentinel: enabled: true
第四部:给feign客户端配置降级类
@FeignClient(value = "seckill-service",fallback = SeckillFeignServiceFallBack.class) public interface SeckillFeignService { @GetMapping("/sku/seckill/{skuId}") R getSkuSeckillInfo(@PathVariable("skuId") Long skuId); }
第五步 : 编写降级类
讯享网@Component @Slf4j public class SeckillFeignServiceFallBack implements SeckillFeignService { @Override public R getSkuSeckillInfo(Long skuId) { log.info("远程调用熔断"); return R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(),BizCodeEnume.TOO_MANY_REQUEST.getMessage()); } }
331.2 调用方控制台指定远程服务的降级策略

332.自定义受保护资源
332.1 抛出异常的方式自定义资源
try (Entry entry = SphU.entry("seckillSkus")){ //自定义资源名 //受保护的资源 }catch (BlockException e){ log.error("资源被限流",e.getMessage()); }
332.2 基于注解自定义资源
Sentinel支持通过@SentinelResource注解定义资源并配置blockHandler 和 fallback 函数来进行限流之后的处理.
blockHandler函数会在原方法被限流/降级/系统保护的时候调用 , 而fallback函数会针对所有类型的异常 , 请注意 blockHander 和 fallback函数的形式要求.
讯享网@Override @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "blockHandler",//降级方法名 fallback = xxxx.class ) public List<SecKillSkuRedisTo> getCurrentSeckillSkus(){ //...... } / * 返回当前时间可以参与秒杀商品信息降级 */ public List<SecKillSkuRedisTo> blockHandler(BlockException e){ log.error("getCurrentSeckillSkusResource被限流降级了"); return null; }
332.3 配置异常返回
无论那种方式一定要配置被限流后的默认返回.
url请求可以设置统一返回.
333.网关流控
好处 : 流控失败时都不需要走到服务 , 直接在网关拦截
Sentinel 提供了 Spring Cloud Gateway 的适配模块 , 可以提供两种资源维度的限流.
- route维度 : 即在Spring配置文件在配置的路由条目 , 资源名为对应的routeid
- 自定义API维度 : 用户可以利用Sentinel提供的API来自定义一些API分组
第一步 : 引入依赖
<!--sentinel gateway--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency>
第二步 : 启动控制台

路由名即为资源名


333.1 针对属性请求

333.2 API分组

333.3 自定义返回数据

可以在 GatewayCallbackManager 注册回调进行定制:
讯享网@Configuration public class SentinelGatewayConfig { public SentinelGatewayConfig(){ GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { R error = R.error(BizCodeEnume.TOO_MANY_REQUEST.getCode(), BizCodeEnume.TOO_MANY_REQUEST.getMessage()); String errJSON = JSON.toJSONString(error); Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errJSON), String.class); return body; } }); } }
335 Sleuth-链路追踪
335.1 基本概念
见P335
335.2 Spring整合Sleuth
第一步 : 服务
<!--sleuth--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>
提供者与消费者导入依赖 (common)
讯享网logging: level: org.springframework.cloud.openfeign: debug org.springframework.cloud.sleuth: debug
第三步 : 发起一次远程调用,观察控制台
DEBUG [user-service,f08573fff5,f08573fff5,false]
user-service:服务名
f08573fff5:是 TranceId,一条链路中,只有一个
TranceId f08573fff5:是 spanId,链路中的基本工作单元 id
false:表示是否将数据输出到其他服务,true 则会把信息输出到其他可视化的服务上观察
336.Sleuth-整合Zipkin 链路追踪
第一步 : docker安装zipkin服务器
docker run -d -p 9411:9411 openzipkin/zipkin
第二步 : 导入依赖
讯享网<!--zipkin--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引
第三步 : 添加配置
spring: application: name: user-service zipkin: base-url: http://192.168.56.10:9411/ # zipkin 服务器的地址 # 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称 discoveryClientEnabled: false 保存: type: web # 设置使用 http 的方式传输数据 sleuth: sampler: probability: 0.5 # 设置抽样采集率为 100%,默认为 0.1,即 10%
336.1 Zipkin 数据持久化
保存再es中
讯享网docker run --env STORAGE_TYPE=elasticsearch --env ES_HOSTS=192.168.56.10:9200 openzipkin/zipkin-dependencies

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/40055.html