这个太重要了,后期P140—P172的什么性能压测、缓存优化都是基于这块进行的
将"pms_category"表中所有数据封装到List0
;
找到List0
中parent_cid为0的List1
,这些就是1级分类;
遍历一级分类集合List1
里面的所有CategoryEntity,记住它们的id,在List0
里面找parent_id为一级分类id的就是二级分类;
遍历二级分类的集合List2
里面的所有CategoryEntity,记住它们的id,在List0
里面找parent_id为二级分类id的就是三级分类;
总之全程只查询了"pms_category"表,没有涉及到其他表。找到三级分类后将它们封装返回就是了。
访问gulimall.com,本地根据在windows的hosts文件配置里找到gulimall.com映射的是192.168.56.106,于是转发到虚拟机,虚拟机会交给nginx,nginx有一处配置专门监听gulimall.com,监听到以后根据配置转发到88端口的gulimall-gateway,网关根据断言转发到gulimall-product
1.中间件的影响
前台请求先经过nginx,然后nginx交给gulimall-Gateway,然后才能到达具体的服务,中间两个中间件nginx、Gateway会不会影响性能?
2.做了哪些压测?
先压测没有Gateway的情况下的吞吐量、响应时间;
然后压测有了Gateway的情况下的吞吐量、响应时间;
然后压测有了nginx,有了Gateway的情况下的吞吐量、响应时间;
访问首页gulimall.com,首页会访问数据库查询数据库的三级目录,模板引擎需要将查询到的数据转交给thymeleaf然后渲染到页面,你的业务代码都会导致响应会慢很多。比如你查询三级分类时查询数据库要尽量一次拿到pms_category的所有数据,而不是一级分类查一下数据库、二级分类查一下数据库、三级分类再查一下数据库。
先压测localhost:10000,因为它没有使用中间件直接访问到首页,所以响应也挺快的;
然后压测gulimall.com,因为它有了nginx,有了Gateway,查看它的响应时间;
3.优化的方向:
1.优化中间件:①让中间件每秒的吞吐量先上去,②然后让中间件之间的传输效率提高(买更好的网线、买更好的网卡、使用更高效率的传输协议等)
2.测试时JvisualVM发现伊甸园区内存只有32M,超小,所以垃圾回收次数非常多,所以如果伊甸园区调整的大一些就gc的时间就减少很多,那么吞吐量也就上去了。而且老年代也很小,导致几乎就要爆满了。给gulimall-product设置一Xmx1024m -xms1024m 一Xmn512m
(内存最大占用1024M,初值也时1024M,相当于内存大小固定好了就是1024M,Xmn就是伊甸园区,给伊甸园区调大到512M)
3.业务代码也很影响性能。①查询数据库次数问题。②静态资源问题——因为我们把静态资源放到IDEA中的微服务中,所以首页需要的css、js样式也得找tomcat要,所以tomcat还得处理这些静态资源请求,导致吞吐量变少。③模板的缓存问题,你开发时经常在yml中有个配置就是thymeleaf.cache: false
关闭thymeleaf缓存便于调试,到了实际上线后一定要开启缓存。④优化日志级别,以前是debug,现在改为error,也就是只打印错误日志。⑤优化数据库,在查询三级目录时经常查询parent_cid,由于parent_cid不是主键没有索引导致查询起来其实很慢,你如果查询id那种主键、有索引的就会很快很快,所以给parent_cid加上索引(索引类型就是普通索引,不用选成主键索引),那么查询速度就会快很多
4.使用缓存(见p151—p172)
以前我们是动态请求、静态请求都是先找nginx,然后找Gateway,然后找具体的微服务。
所以我们可以把静态资源上传到nginx上面,这样静态请求只需要找到nginx就看拿到对应的资源了。
分割线 |
在Redis这里别觉得代码多,代码一点都不多,最多的代码就是早就被你烂熟于心了的从数据查询三级目录的那片代码。
所以别被吓到,我担保p151—p172这块没有任何一个代码会让你看查过30秒都看不懂
1.哪些数据适合放入锾存?
2.本地缓存与分布式缓存对比
3.使用redis优化三级目录的代码
public Map<String, List<Catelog2Vo>> getCatalogJson2() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.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;
}
//从数据库查询数据(查询数据库之前再查一遍redis,防止在执行getDataFromDB()的前0.0001秒时正好有人把三级目录数据存入redis)
private Map<String, List<Catelog2Vo>> getDataFromDB() {
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("Redis中还是没有数据,查询了数据库。。。。。");
List<CategoryEntity> selectList = baseMapper.selectList(null);
//查出所有一级分类
List<CategoryEntity> level1Category = getParent_cid(selectList, 0L);
//2、封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
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());
//1、找当前二级分类的三级分类封装vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Catalog3Vo> collect = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catelog2Vo.Catalog3Vo catelog3Vo = new Catelog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
//3、将查到的数据再放入缓存,将对象转为JSON在缓存中
String jsonString = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
return parent_cid;
}
4.整合redis后压力测试出内存泄漏问题
就在首页查询三级目录整合了Redis后,使用JMeter大并发测试时,出现堆外内存溢出
异常OutOfDirectMemoryError
解决办法就是修改“gulimall-product”的“pom.xml”文件,更换为Jedis
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
前面我们将查询三级分类数据的查询进行了优化,将查询结果放入到Redis中,当再次获取到相同数据的时候,直接从缓存中读取,没有则到数据库中查询,并将查询结果放入到Redis缓存中
1.缓存穿透:
2.缓存雪崩:
3.缓存击穿
简单来说:缓存穿透是指查询一个永不存在的数据;缓存雪崩是值大面积key同时失效问题;缓存击穿是指高频key失效问题;
1.锁时序问题:
2.加锁代码
//从数据库查询并封装分类数据
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {
//为什么是this,看视频
synchronized (this) {
//得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isNotEmpty(catalogJSON)) {
//缓存不为null,直接返回,
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库");
List<CategoryEntity> selectList = baseMapper.selectList(null);
//1、查出所有一级分类,
List<CategoryEntity> category = getParent_cid(selectList, 0L);
//2、封装数据
Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、查到这个一级分类下的所有二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catalog2Vo> catalog2Vos = new ArrayList<>();
if (categoryEntities != null && categoryEntities.size() != 0) {
catalog2Vos = categoryEntities.stream().map(l2 -> {
Catalog2Vo catalog2Vo = new Catalog2Vo(
l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
if (level3Catalog != null && !level3Catalog.isEmpty()) {
//2、封装成指定格式
collectlevel3 = level3Catalog.stream().map(l3 -> {
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
l3.getCatId().toString(), l3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
}
catalog2Vo.setCatalog3List(collectlevel3);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
//3、将查到的数据再放入缓存,将对象转为JSON在缓存中
String jsonString = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
return parent_cid;
}
}
3.本地锁在分布式情况下存在的问题
把gulimall-product复制四份,然后让JMeter大并发去访问gulimall.com,我们发现在分布式下的四个服务分别存在着四个缓存未命中的情况,也就意味着会有四次查询数据库的操作,显然我们的synchronize锁未能实现限制其他服务实例进入临界区,也就印证了在分布式情况下,本地锁只能针对于当前的服务生效。
在Redisson出现之前我们使用的就是去Redis中占坑的方式去获得分布式锁,我们占坑的方法
lock=setIfAbsent(key,value)
就是如果这个key不存在的话就设置key-value,而且返回true;如果存在了就设置不了key-value,返回false
加了分布式锁解决缓存击穿,但是分布式锁存在很多需要考虑的因素:
public Map<String, List<Catalog2Vo>> getCatalogJson() {
/**
* 1、空结果缓存,解决缓存穿透
* 2、设置过期时间(加随机值),解决缓存雪崩
* 3、加锁,解决缓存击穿
*/
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
//缓存中没有,查询数据库
System.out.println("缓存不命中。。。。将要查询数据库。。。。");
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
return catalogJsonFromDb;
}
System.out.println("缓存命中。。。。直接返回。。。。");
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});//转为我们指定的对象
return result;
}
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
String token = UUID.randomUUID().toString();
//1、加锁和设置过程时间弄成原子性操做,这条有四个参数的setIfAbsent()方法就是原子的操做
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 100, TimeUnit.SECONDS);
if (lock) {
//lock=true说明加锁成功,那就从数据库中获取数据
System.out.println("获取分布式锁成功");
Map<String, List<Catalog2Vo>> dataFromDB;
try {
dataFromDB = getDataFromDB();
} finally {
// 先比对uuid再删锁(这不是原子性操做,所以被注释掉了)
// String lock1 = stringRedisTemplate.opsForValue().get("lock");
// if (token.equals(lock1)) {
// stringRedisTemplate.delete("lock");
// }
String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
Long lock1 = stringRedisTemplate.execute(luaScript, Arrays.asList("lock"), token);
}
return dataFromDB;
} else {
System.out.println("获取分布式锁失败,等待重试");
//加锁失败。。。重试 。休眠300ms重试
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();
}
}
private Map<String, List<Catalog2Vo>> getDataFromDB() {
//得到锁以后应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isNotEmpty(catalogJSON)) {
//缓存不为null,直接返回,
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
System.out.println(Thread.currentThread().getName() + "查询了数据库");
List<CategoryEntity> selectList = baseMapper.selectList(null);
List<CategoryEntity> category = getParent_cid(selectList, 0L);
//2、封装数据
Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、查到这个一级分类下的所有二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catalog2Vo> catalog2Vos = new ArrayList<>();
if (categoryEntities != null && categoryEntities.size() != 0) {
catalog2Vos = categoryEntities.stream().map(l2 -> {
Catalog2Vo catalog2Vo = new Catalog2Vo(
l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
if (level3Catalog != null && !level3Catalog.isEmpty()) {
//2、封装成指定格式
collectlevel3 = level3Catalog.stream().map(l3 -> {
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
l3.getCatId().toString(), l3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
}
catalog2Vo.setCatalog3List(collectlevel3);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
//3、将查到的数据放到缓存,将对象转为json放到缓存中
String s = JSON.toJSONString(parent_cid);
stringRedisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return parent_cid;
}
1.SpringBoot整合Redisson
①导入pom
②写一个配置类,配置类中指明Redis的地址,返回Redisson
@Configuration
public class MyRedisConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.137.14:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
③直接使用
RLock lock = redisson.getLock("my-lock");
lock.lock();
lock.unlock();
2.Redisson的特性
设想一种情况,一个请求线程在执行业务方法的时候,突然发生了中断,此时没有来得及执行释放锁操作,那么同时等待的另外一个线程是否会发生死锁。
在A服务在获取锁后,突然中断它的运行;等待的B服务会很快就拿到锁,不会因为A没有释放锁而被卡死。通这是因为在Redisson中会为每个锁加上“leaseTime”,默认是30秒,如果A服务宕机,到了时间就会自动释放锁。如果A服务没有宕机,而且30秒不够用,Redisson会自动给它续期。当然,人家默认的自动解锁时间是30秒,如果你改为10秒,那么10秒后立刻释放锁,不会给锁续期,但是这种自定义解锁场景也很常用,你可以自定义300秒,如果一个业务300秒都没有执行完肯定就有问题,而且我们还可以拿它评估一下业务的最大执行用时。
小结:redisson的lock具有如下特点
3.Redisson的读写锁
写+读:要等写完才能读
写+写:等前一个写完后一个才能写
读+读:相当于无锁,大家都能读
读+写:有读锁,写必须等待
读写锁适合经常读、很少写的情况,因为读的时候相当于无锁。
4.Redisson的闭锁
走完五个人就锁门,这就是闭锁
5.Redisson的信号量
车库停车,3个停车位,获取到信号量才能进去停车。
以上演示的Redisson的读写锁、闭锁、信号量都是分布式下也适用的情况。
1.如果没有使用Redisson
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
String token = UUID.randomUUID().toString();
//1、占分布式锁。去redis占坑,
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 100, TimeUnit.SECONDS);
if (lock) {
//lock=true说明加锁成功,那就从数据库中获取数据
System.out.println("获取分布式锁成功");
Map<String, List<Catalog2Vo>> dataFromDB;
try {
dataFromDB = getDataFromDB();
} finally {
String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
Long lock1 = stringRedisTemplate.execute(luaScript, Arrays.asList("lock"), token);
}
return dataFromDB;
} else {
System.out.println("获取分布式锁失败,等待重试");
//加锁失败。。。重试 。休眠300ms重试
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();
}
}
2.如果使用Redisson
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//注意锁的粒度问题
RReadWriteLock lock = redissonClient.getReadWriteLock("catalogJson-lock");
lock.lock();
Map<String, List<Catalogs2Vo>> dataFromDb = null;
try {
dataFromDb = getCatalogJsonFromDB();
} finally {
lock.unlock();
}
return dataFromDb;
}
3.使用Redsson时应该注意锁的粒度问题
给锁起名字要注意,不能都起一样的名字,一样的名字代表同一把锁,获取三级分类数据、获取品牌、获取属性锁到同一把锁里面,那就导致粒度很粗。假如访问三级分类是高并发的请求,访问品牌是低并发的,他俩如果同一把锁那么高并发的锁住导致低并发的也访问不到。
4.使用了Redsson还存在问题
如何保证缓存和数据库中的数据一致?
①双写模式(改了数据库顺带着改了缓存)
②失效模式(改了数据库顺带删了缓存)
③双写模式/失效模式+读写锁
④使用Canal(MySQL中一有什么变化就会同步到缓存中来)
各自的弊端:
①双写模式:A要把a改为1,然后B要把a改为2,最后数据库a应该是2。但是A把a改为1本来顺带改一下缓存结果卡顿了,导致B把a改为2顺带先改了缓存,然后卡顿的A改了缓存导致缓存中a是1但数据库中的a是2。高并发下缓存不一致出现了,这就又又又得加锁解决。
②失效模式:(A和B是写操作,改了数据库就要删缓存;C是读操作,如果缓存中读不到就得去数据库读然后写到缓存中)现在有这么一个场景:A要把a改为1,然后B要把a改为2,然后C要读取a,本来C读到的a应该是2。但是A要把a改为1然顺带删了缓存;然后B要把a改为2结果B卡顿住了;C进来读取缓存发现缓存没有数据就读数据库读到了a=1,因为缓存中没有数据所以C要把读到的a写到缓存上,但是C写缓存之前也卡顿了一下;结果现在B变流畅了,它把数据库a改为2,顺带要删缓存,结果发现缓存中还没有数据所以就不删了;现在C开始了,它把读到的a=1写到缓存中。又要加锁。
③双写模式/失效模式+读写锁:这个没什么问题,但是代码太复杂了吧。
④使用Canal:Canal是第三方的,使用起来非常方便,而且也没什么问题,但是又加了一个中间件,还得自定义一些功能,我们这个小项目就不用了。
我们系统的一致性解决方案:
实时性、一致性要求高的那就去数据库中查;
实时性、一致性要求不高的那就放到缓存中,如果害怕出现脏数据,那就给缓存加上过期时间
,然后使用双写模式/失效模式+读写锁
,代码很复杂
基于以上分析,我们发现用了Redisson依旧要考虑复杂的系统的一致性问题,所以SpringCache应用而生
@Cacheable:触发将数据保存到缓存的操作
@CacheEvict:触发将数据从缓存中删除的操作
@CachePut:在不影响方法执行的情况下更新缓存。
@Caching:组合以上多个操作
@CacheConfig: 在类级别上共享一些公共的与缓存相关的设置。
//这是以前编写的前台访问/index时获取一级目录的方法,我们只需要在上面添加@Cacheable注解就表示如果缓存中有就不用执行下面的方法,缓存中没有就执行下面的方法查出数据并且放入缓存
@Cacheable({
"catagory"})
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys......");
long l = System.currentTimeMillis();
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
@Cacheable注解里面有些默认配置不合理,我们要自定义
#在yml中指定过期时间
spring.cache.redis.time-to-live=3600000
//因为spel动态取值,所有需要额外加''表示字符串
@Cacheable(value = {
"catagory"},key = "'Level1Categorys'")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys......");
long l = System.currentTimeMillis();
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
@Cacheable(value = {
"catagory"},key = "#root.method.name") //用方法名做key
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys......");
long l = System.currentTimeMillis();
return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
}
@Cacheable注解里面有些默认配置不合理,我们要自定义
@CacheEvict
、@Cacheput
、@chaching
注解的演示说明:
getLevel1Categorys()是从数据库中读取一级分类数据,getCatalogJson()是从数据库中读取三级分类数据,;
updateCascade()是更新数据库中的三级分类数据。一旦数据库中三级分类数据被更新,那么那么一级目录的数据和三级目录的数据都变了,所以需要清除getLevel1Categorys()和getCatalogJson()里面的缓存数据。
1.清除缓存的方法一
@Caching(evict = {
@CacheEvict(value = "category",key = "'getLevel1Categorys'"),
@CacheEvict(value = "category",key = "'getCatalogJson'")
})
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationDao.updateCategory(category.getCatId(), category.getName());
//同时修改缓存中的数据,
}
@Cacheable(value = {
"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys........");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
return categoryEntities;
}
@Cacheable(value = {
"catagory"},key = "#root.method.name")
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
System.out.println(Thread.currentThread().getName() + "查询了数据库");
List<CategoryEntity> selectList = baseMapper.selectList(null);
List<CategoryEntity> category = getParent_cid(selectList, 0L);
//2、封装数据
Map<String, List<Catalog2Vo>> parent_cid = category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、查到这个一级分类下的所有二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
//2、封装上面的结果
List<Catalog2Vo> catalog2Vos = new ArrayList<>();
if (categoryEntities != null && categoryEntities.size() != 0) {
catalog2Vos = categoryEntities.stream().map(l2 -> {
Catalog2Vo catalog2Vo = new Catalog2Vo(
l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
//1、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catalog = getParent_cid(selectList, l2.getCatId());
List<Catalog2Vo.Catalog3Vo> collectlevel3 = new ArrayList<>();
if (level3Catalog != null && !level3Catalog.isEmpty()) {
//2、封装成指定格式
collectlevel3 = level3Catalog.stream().map(l3 -> {
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l3.getParentCid().toString(),
l3.getCatId().toString(), l3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
}
catalog2Vo.setCatalog3List(collectlevel3);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return parent_cid;
}
1.清除缓存的方法二
@CacheEvict(value = "category",allEntries = true) //删除某个分区下的所有数据
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationDao.updateCategory(category.getCatId(), category.getName());
//同时修改缓存中的数据,
}
spring.cache.type=redis
#设置超时时间,默认是毫秒
spring.cache.redis.time-to-live=3600000
#设置Key的前缀,如果指定了前缀,则使用我们定义的前缀,否则使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
p155 缓存击穿、穿透、雪崩问题能用SpringCache解决掉吗?
(总说)先明确什么是读模式什么是写模式:
getLevel1Categorys()
是从数据库中读取一级分类数据,getCatalogJson()
是从数据库中读取三级分类数据,它们都是读模式,读模式就是从数据中读取数据,然后使用@Cacheable将数据放入缓存。1)、读模式
spring.cache.redis.cache-null-values=true
配置来实现@Cacheable(sync = true)
来解决击穿问题。2)、写模式:(缓存与数据库一致)
3)、总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了,过期了让它自己更新就可以了)
特殊数据:你还想加缓存,还想保证数据库和缓存的一致性,那就需要结合Redisson来使用
p136到p138
是搭建了首页;
p139和p140
是让我们借助nginx来通过域名访问这个首页(假如nginx肯定会使访问路线更加曲折,从而影响性能);
p141-p147
是通过Jmeter、JvisualVM来分析加入Gateway、nginx这些中间件带来的性能损失,
p148-p150
是进行性能优化—动静分离、JVM内存优化、代码优化
p151--p154
是进行性能优化—使用Redis
p155—p158
是要解决缓存击穿就需要加锁,而加本地锁不行,只能加分布式锁,但是加分布式锁又要考虑一堆分布式并发问题,于是就有了Redisson;
p159—p166
是给你介绍了Redisson分布式锁的用法,但是用上Redisson后还要考虑缓存和数据库一致性问题,于是SpringCache应用而生。
p167—p172
有了SpringCache,常规数据的缓存你可以不用Redisson,因为SpringCache已经考虑到缓存雪崩、击穿、穿透问题了,它里面可以加锁,可以设置过期时间等等。
从P173开始,完整的笔记全部参考这位博主写的笔记:谷粒商城-个人笔记(高级篇二)
分割线 |
@Data
public class SearchParam {
private String keyword;//页面传递过来的全文匹配关键字
//sort=saleCount_asc/desc销量
//sort=skuPrice_asc/desc价格
//sort=hotScore_asc/desc热度分
private String sort;//排序条件
//hasStock=0/1
private Integer hasStock;//是否只显示有货
//skuPrice=1_500
private String skuPrice;//价格区间查询
//brandId=2&brandId=3
private List<Long> brandId;//按照品牌进行查询,可以多选
//catelog3Id=1
private Long catalog3Id;//三级分类id
//attr=1_3G:4G:5G;attrs=2_骁龙
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1;//页码
}
@Data
public class SearchResult {
//查询到的商品信息
private List<SkuEsModel> products;
//分页信息
private Integer pageNum;//当前页码
private Long total;//总记录数
private Integer totalPages;//总页码
//所有涉及到的品牌
private List<BrandVo> brands;
//所有涉及到的分类
private List<CatalogVo> catalogs;
//所有涉及到的属性
private List<AttrVo> attrs;
//=========================================================================
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
private String brandImg;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
}
//注意:因为Attrs这个类在SkuEsModel这个类里面,属于嵌入式的,所以后期在DSL语句中查询Attrs里面的东西要用nested
@Data
public class SkuEsModel {
private Long skuId; //(SkuInfoEntity)中有
private Long spuId; //(SkuInfoEntity)中有
private String skuTitle; //(SkuInfoEntity)中有
private BigDecimal skuPrice; //(SkuInfoEntity)中有,但是名字不一样
private String skuImg; //(SkuInfoEntity)中有,但是名字不一样
private Long saleCount; //(SkuInfoEntity)中有
private Boolean hasStock;
private Long hotScore;//热度评分设置为0即可
private Long brandId; //(SkuInfoEntity)中有
private Long catalogId; //(SkuInfoEntity)中有
private String brandName; //{BrandEntity}中有
private String brandImg; //{BrandEntity}中有
private String catalogName;//【CategoryEntity】中有
private List<Attrs> attrs;//{
{ProductAttrValueEntity}}中有
@Data
public static class Attrs {
//{
{ProductAttrValueEntity}}中有
private Long attrId;
private String attrName;
private String attrValue;
}
}
完整查询参数 keyword=小米&catalog3Id=1&brandId=1&hasStock=0/1&skuPrice=400_1900&at trs=1_3G:4G:5G&attrs=2_骁龙845&attrs=4_高清屏&sort=saleCount_desc/asc
GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"2"
]
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 1000,
"lte": 7000
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "6"
}
}
}
]
}
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {
"skuTitle": {
}}, //用户搜索"华为",那么前台要高亮显示
"pre_tags": "",
"post_tags": ""
},
"aggs": {
"brandAgg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brandNameAgg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brandImgAgg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalogAgg":{
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalogNameAgg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attrs":{
"nested": {
"path": "attrs"
},
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
}
}
}
}
}
}
}
代码看不懂太正常了,回看Day03_谷粒商城(谷粒商城高级篇二)摘要
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
RestHighLevelClient restHighLevelClient;
//去es进行检索
@Override
public SearchResult search(SearchParam param) {
//动态构建出查询需要的DSL语句
SearchResult result = null;
//1、准备检索请求
SearchRequest searchRequest = buildSearchRequest(param);
try {
//2、执行检索请求
SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//分析响应数据封装我们需要的格式
result = buildSearchResult(response,param);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 准备检索请求
* 模糊匹配、过滤(按照属性、分类、品牌、价格区间、库存),排序,分页,高亮,聚合分析
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();//构建DSL语句的
/**
* 过滤(按照属性、分类、品牌、价格区间、库存)
*/
//1、构建bool-query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1.1 must-模糊匹配、
if (!StringUtils.isEmpty(param.getKeyword())){
boolQuery.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
}
//1.2.1 filter-按照三级分类id查询
if (null != param.getCatalog3Id()){
boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
}
//1.2.2 filter-按照品牌id查询
if (null != param.getBrandId() && param.getBrandId().size()>0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
}
//1.2.3 filter-按照是否有库存进行查询
if (null != param.getHasStock() ) {
boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
}
//1.2.4 filter-按照区间进行查询 1_500/_500/500_
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(param.getSkuPrice())) {
String[] prices = param.getSkuPrice().split("_");
if (prices.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
}else {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
} else if (prices.length == 2) {
//_6000会截取成["","6000"]
if (!prices[0].isEmpty()) {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
}
boolQuery.filter(rangeQueryBuilder);
}
//1.2.5 filter-按照属性进行查询
List<String> attrs = param.getAttrs();
if (null != attrs && attrs.size() > 0) {
//attrs=1_5寸:8寸&2_16G:8G
attrs.forEach(attr->{
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
String[] attrSplit = attr.split("_");
queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));//检索的属性的id
String[] attrValues = attrSplit[1].split(":");
queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));//检索的属性的值
//每一个必须都得生成一个nested查询
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
boolQuery.filter(nestedQueryBuilder);
});
}
//把以前所有的条件都拿来进行封装
sourceBuilder.query(boolQuery);
/**
* 排序,分页,高亮,
*/
//2.1 排序 eg:sort=saleCount_desc/asc
if (!StringUtils.isEmpty(param.getSort())) {
String[] sortSplit = param.getSort().split("_");
sourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
}
//2.2、分页
sourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
sourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);
//2.3 高亮highlight
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
sourceBuilder.highlighter(highlightBuilder);
}
/**
* 聚合分析
*/
//5. 聚合
//5.1 按照品牌聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(50);
//品牌聚合的子聚合
TermsAggregationBuilder brand_name_agg = AggregationBuilders.terms("brand_name_agg").field("brandName").size(1);
TermsAggregationBuilder brand_img_agg = AggregationBuilders.terms("brand_img_agg").field("brandImg");
brand_agg.subAggregation(brand_name_agg);
brand_agg.subAggregation(brand_img_agg);
sourceBuilder.aggregation(brand_agg);
//5.2 按照catalog聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
TermsAggregationBuilder catalog_name_agg = AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1);
catalog_agg.subAggregation(catalog_name_agg);
sourceBuilder.aggregation(catalog_agg);
//5.3 按照attrs聚合
NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attr_agg", "attrs");
//按照attrId聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
//按照attrId聚合之后再按照attrName和attrValue聚合
TermsAggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1);
TermsAggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50);
attr_id_agg.subAggregation(attr_name_agg);
attr_id_agg.subAggregation(attr_value_agg);
nestedAggregationBuilder.subAggregation(attr_id_agg);
sourceBuilder.aggregation(nestedAggregationBuilder);
String s = sourceBuilder.toString();
System.out.println("构建的DSL"+s);
SearchRequest request = new SearchRequest(new String[]{
EsConstant.PRODUCT_INDEX}, sourceBuilder);
return request;
}
代码看不懂太正常了,回看Day03_谷粒商城(谷粒商城高级篇二)摘要
//5、分页信息-页码
result.setPageNum(param.getPageNum());
//5、1分页信息、总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//5、2分页信息-总页码-计算
int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
(int)total / EsConstant.PRODUCT_PAGESIZE : ((int)total / EsConstant.PRODUCT_PAGESIZE + 1);
result.setTotalPages(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
SearchResult result = new SearchResult();
//1、返回的所有查询到的商品
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(param.getKeyword())) {
//拿到高亮信息显示标题
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String skuTitleValue = skuTitle.getFragments()[0].string();
esModel.setSkuTitle(skuTitleValue);
}
esModels.add(esModel);
}
}
result.setProduct(esModels);
//4、当前商品涉及到的所有分类信息
//获取到分类的聚合
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
ParsedLongTerms catalogAgg = response.getAggregations().get("catalog_agg");
for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
//得到分类id
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
//得到分类名
ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalogName);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
//2、当前商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
//获取属性信息的聚合
ParsedNested attrsAgg = response.getAggregations().get("attr_agg");
ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//1、得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
attrVo.setAttrId(attrId);
//2、得到属性的名字
ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(attrName);
//3、得到属性的所有值(可能有多个值)
ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
//3、当前商品涉及到的所有品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
//获取到品牌的聚合
ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
//1、得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
brandVo.setBrandId(brandId);
//2、得到品牌的名字
ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString(); //品牌名只有一种情况,所以get(0)就可以
brandVo.setBrandName(brandName);
//3、得到品牌的图片
ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();//品牌的默认图片只有一种情况,所以get(0)就可以
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
完整代码如下:
private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {
SearchResult result = new SearchResult();
//1、返回的所有查询到的商品
SearchHits hits = response.getHits();
List<SkuEsModel> esModels = new ArrayList<>();
if (null != hits.getHits() && hits.getHits().length>0){
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
esModel.setSkuTitle(skuTitle.fragments()[0].string());
}
esModels.add(esModel);
}
}
result.setProducts(esModels);
//2、当前所有商品涉及到的所有属性
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();
//1、得到属性的id;
long attrId = bucket.getKeyAsNumber().longValue();
//2、得到属性的名字
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
//3、得到属性的所有值
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.setAttrName(attrName);
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
//3、当前所有品牌涉及到的所有属性
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();
//1、得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
//2、得到品牌的名
String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
//3、得到品牌的图片
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);
//4、当前商品所涉及的分类信息
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);
//===========以上从聚合信息获取到=============
//5、分页信息-页码
result.setPageNum(param.getPageNum());
//6、分页信息-总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//7、分页信息-总页码-计算
int totalPages = total%EsConstant.PRODUCT_PAGESIZE == 0 ?(int) total/EsConstant.PRODUCT_PAGESIZE:((int)total/EsConstant.PRODUCT_PAGESIZE+1);
result.setTotalPages(totalPages);
return result;
}
@GetMapping("/list.html")
public String listPage(SearchParam searchParam, Model model) {
SearchResult result = mallSearchService.search(searchParam);
System.out.println("===================="+result);
model.addAttribute("result", result);
return "list";
}
前台把查询的条件封装到SearchParam里面,后台根据SearchParam查询ElastiSearch,后台写的Java代码事实上就是动态的DSL语句,用DSL语句查询ElasticSearch,把查询到的结果从DSL语句中提取出来,封装到SearchResult里面返回给前台。
做法很简单,我们之前前台给后台传回去的SearchParam不变,但是后台返回给前台的SearchResult里面再添加一个新的字段List
,在NavVo里面有navName,navValue,link这三个字段;
然后还是一如既往,后台根据SearchParam查询ElastiSearch,查询结果封装到SearchResult里面返回给前台,唯一变了的就是在封装结果时,要对List
也要进一步封装。
假如你点击了一个属性是“高清屏”,那么前台传给后台就有attrs=4_高清屏
,对于NavVo里的navValue其实就是"高清屏
";对于NavVo里的navName其实就是根据attrId调用gulimall-product查询属性表得到attr_name,attrId不就是4嘛;对于NavVo里的link其实就是没点面包屑之前的url,点了面包屑不就是在原先url基础上多拼装了一个attrs=4_高清屏
嘛,所以你从前端拿到现在的url(也就是点了面包屑以后的url)然后切割一下就行了。
需要注意的三个点:
①因为远程调用gulimall-product所以可以在被调用的gulimall-product的那个方法上添加缓存@cacheable(value = "attr",key = " 'attrInfo'+#root.args[0]")
②如何从前端拿到现在的url?
③通过以上方法拿到的前端的url是被URL编码的结果&attrs=%257B%2522request%255Fid%2522%253A%25
,不是你想要的url,所以你需要先解码。
分割线 |
P193—P202是对多线程知识的讲解,和项目独立
前台传回来的只有skuid,然后查询对应的表得到对应的封装信息
spu是华为手机,sku是华为手机中具体的那个亮黑色128G的手机 ,spu和sku肯定在同一个三级分类下,它们的三级分类都是手机。
根据spuid和catalogid找到当前sku下的所有spu属性及其对应的值
返回给前台的Vo
@Data
public class SkuItemVo {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info;
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images;
//3、获取spu的销售属性组合
List<SkuItemSaleAttrVo> saleAttr;
//4、获取spu的介绍
SpuInfoDescEntity desc;
//5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> groupAttrs;
}
@Data
public class SkuItemSaleAttrVo {
private Long attrId;
private String attrName;
private String attrValues;
}
@ToString
@Data
public class SpuItemAttrGroupVo {
private String groupName;
private List<Attr> attrs;
}
分割线 |
总说:
完成用户在注册页面的发送验证码的操做:前台发送
/sms/sendcode
的请求给后台的gulimall-auth-server,然后gulimall-auth-server会先验证一下验证码是否在60秒前发送过(接口防刷),如果没有就使用OpenFeign远程调用gulimall-thrid-party的sendCode方法完成第三方服务的发送验证码功能。
关于接口防刷
gulimall-auth-server如何校验验证码是否在60秒前发送过?当前台带着手机号发送
/sms/sendcode
的请求给后台,后台先到redis中根据key为(“sms:code:”+phone)尝试获取这段redis信息,如果获取不到,后台会在redis中存储(key为"sms:code:"+phone,value为"验证码_当前时间",过期时间是10分钟)的一段信息,然后远程调用发送验证码方法;假如60秒内前台带着该手机号再次发送/sms/sendcode
的请求给后台,后台先到redis中根据key为(“sms:code:”+phone)尝试获取这段redis信息,如果能够获取到这段信息就判断时间差是否小于60s,如果是就不进行发送验证码操做。
p211—p214
是完成用户在注册页面的发送验证码的功能,并且添加接口防刷;这里只实现了获取验证码,没有做验证码校验
p215和p216和p217和p218
的这些操作是完成注册功能:我们之前把验证码发给用户了,用户会填好验证码和注册信息封装到UserRegistVo后发送给gulimall-auth-server,然后gulimall-auth-server会进行JSR303校验(校验密码格式、手机号格式 )和Redis中验证码校验,校验通过会利用OpenFeign远程调用gulimall-member的regist()方法来进行会员注册,会员注册肯定有失败有成功,对于那些注册失败我们使用异常机制
LoginController类的逻辑如下:
首先进行JSR303校验,若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面;
若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话远程调用会员服务注册;
会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面。
编写UserRegistVo类,代码如下:
@Data
public class UserRegistVo {
@NotEmpty(message = "用户名必须提交")
@Length(min = 6, max = 18, message = "用户名必须是6-18位字符")
private String userName;
@NotEmpty(message = "密码必须填写")
@Length(min = 6, max = 18, message = "密码必须是6-18位字符")
private String password;
@NotEmpty(message = "手机号必须填写")
@Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
private String phone;
@NotEmpty(message = "验证码必须填写")
private String code;
}
编写LoginController类,下面的注释一定一定要好好看!!!
/**
* 下面的代码可以说相当重要,regist()方法一共有三个参数,UserRegistVo是封装前台传过来的数据,BindingResult封装JSR303校验错误信息
* RedirectAttributes是重定向携带数据。转发的时候session共享数据,重定向的时候如何共享数据呢?
* 使用RedirectAttributes,它利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉。
*/
@PostMapping("/regist")
public String regist(@Valid UserRegistVo vo, BindingResult result,
RedirectAttributes redirectAttributes){
if (result.hasErrors()){
//如果校验不通过,则封装校验结果,将错误信息封装到redirectAttributes中
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors",errors);
//使用return "reg"; 转发会出现重复提交的问题,不要以转发的方式
//使用 return "forward:/reg.html"; 会出现问题:Request method 'POST' not supported的问题(原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的))
//使用重定向 解决重复提交的问题。但面临着数据不能携带的问题,就用RedirectAttributes。
return "redirect:http://auth.gulimall.com/reg.html";
}
//1、校验验证码
String code = vo.getCode();
String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
if (!StringUtils.isEmpty(s)) {
if (code.equals(s.split("_")[0])) {
//验证码通过,删除缓存中的验证码;令牌机制
stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
//真正注册调用远程服务注册
R r = memberFeignService.regist(vo);
if (r.getCode() == 0) {
//成功
return "redirect:http://auth.gulimall.com/login.html";
} else {
Map<String, String> errors = new HashMap<>();
errors.put("msg", r.getData(new TypeReference<String>() {
}));
redirectAttributes.addFlashAttribute("errors", errors);
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
} else {
Map<String, String> errors = new HashMap<>();
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";//校验出错重定向到注册页
}
//注册成功回到登录页
return "redirect:http://auth.gulimall.com/login.html";
}
远程调用会员服务,会员服务干了什么?会先从ums_member_level表查询用户默认等级;然后会查询ums_member表的phone的数量是否大于0,如果大于0说明手机号已经存在,返回错误信息;查询ums_member表的username的数量是否大于0,如果大于0说明用户名已经存在,返回错误信息;如果ums_member表中手机号数量问0、用户名数据为0,那就把(用户等级、用户名、手机号、密码)一并存入ums_member表,其中密码加密适用了BCrypt加密方式
给用户密码加密的三种方式对比:MD5加密、盐值加密、BCrypt加密
可逆加密:知道了加密算法后通过密文可以推算出原来的明文
不可逆加密:即使知道了加密算法通过密文也不可以推算出原来的明文
①MD5加密:知道了密文可以推算出原来的明文,网上随处可找MD5破解
String s = DigestUtils.md5Hex("123456");
System.out.println(s);//e10adc3949ba59abbe56e057f20f883e
②MD5加盐(盐值加密)
可以给随机盐也可以给指定盐值,反正就是对“密码+盐值”
进行MD5加密,你只能把盐值保存起来然后下一次对“密码+盐值”
进行再加密然后比对密文是否一致来判断用户名密码正确与否
String s = Md5Crypt.md5Crypt("123456".getBytes(),"$1$qqqqqqqq"); //$1$qqqqqqqq就是你指定的盐值
System.out.println(s); //$1$qqqqqqqq$AZofg3QwurbxV3KEOzwuI1
③BCrypt加密
Spring家的BCrypt加密,即使明文一样,每次加密的密文都不一样,但是你可以匹配明文和密文,人家就会告诉你这两个匹配与否
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");//$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");
System.out.println(matches);//true
本届内容就是完成测试
总说:
前台发送 /login到后台的gulimall-auth-server模块中,然后gulimall-auth-server会使用OpenFeign远程调用gulimall-member的login()方法,在该方法中根据用户名查询ums_member表拿到MemberEntity然后进行用户名密码比对完成登录,登录成功重定向到首页,登陆失败重定向到登录页
1.微博登陆的流程
2.有两个地址很重要
(1)是“ 在登录页引导用户至授权页”的地址:
这一步是前台完成的,前台html中的url要写成
Get
https://api.weibo.com/oauth2/authorize?client_id=1917008757&response_type=code&redirect_uri=http://gulimall.com/oauth2.0/weibo/success
client_id
:是你创建网站应用时的app key,
redirect_uri
是用户使用微博登录后重定向到哪里去。
我们指定redirect_uri=http://gulimall.com/oauth2.0/weibo/success
也就是说用户用户使用微博登录后,相当于发送 /oauth2.0/weibo/success
到后台的gulimall-auth-server
模块中,那么gulimall-auth-server
会使用code换取token
,这就涉及到换取token的url:
(2)是换取token的url
这一步是后台完成的,后台发送这样的url才能获取到token
POST
https://api.weibo.com/oauth2/access_token?client_id=1917008757&client_secret=94d9cc62c60d5f9f3d0c62389593024f&grant_type=authorization_code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success&code=CODE
client_id
: 创建网站应用时的app key;
client_secret
: 创建网站应用时的app secret
redirect_uri
: 认证完成后的跳转链接(需要和平台高级设置一致);
code
:换取令牌的认证码
后台发送这么个请求就可以根据用户授权返回的code换取token(换回来的不仅仅是token,还有uid用户id、expires_in令牌的过期时间等等,这些被封装到SocialUser中),拿到SocialUser中的token就可以向微博官方发送别的请求换取用户信息
3.微博登陆的具体流程
4.编码总说:
①前台带着code发送
/oauth2.0/weibo/success
请求到后台的gulimall-auth-server
模块中,然后gulimall-auth-server
会先使用code换取SocialUser,然后拿着SocialUser到OpenFeign远程调用gulimall-member
的oauth2Login()
方法,在该方法中会先用SocialUser的uid查询数据库来判断用户是否是第一次用微博登录,如果是第一次的话我们就得给该用户注册(拿着token到微博里面查询该用户的基本信息,然后insert到咱们的数据库里面);如果该用户之前已经用微博登陆过,那就到数据库中更新一下token。
如果一切顺利,gulimall-member
就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到RedirectAttributes然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html
②前台用户用微博登录后我们会拿到用户的code,后台用code到微博里面换取token这样才能用token访问到用户基本信息;用户每登陆一次访问微博的token就会变一次,所以当用户下次用微博登陆时我们需要到数据库更新一下token
5.代码(可看不可不看)
用code换取的用户信息封装到SocialUser中
@Data
public class SocialUser {
private String access_token;
private String remind_in;
private long expires_in;
private String uid;
private String isRealName;
}
gulimall-auth-server的Controller
@Controller
public class OauthController {
@Autowired
private MemberFeignService memberFeignService;
@RequestMapping("/oauth2.0/weibo/success")
public String authorize(String code, RedirectAttributes attributes) throws Exception {
//使用code换取token
Map<String, String> query = new HashMap<>();
query.put("client_id", "2144***074");
query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
query.put("grant_type", "authorization_code");
query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
query.put("code", code);
//发送post请求换取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
Map<String, String> errors = new HashMap<>();
if (response.getStatusLine().getStatusCode() == 200) {
//调用member远程接口进行oauth登录
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
});
R login = memberFeignService.login(socialUser);
//远程调用成功,返回首页并携带用户信息
if (login.getCode() == 0) {
String jsonString = JSON.toJSONString(login.get("memberEntity"));
MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() {
});
attributes.addFlashAttribute("user", memberResponseVo);
//MemberResponseVo和MemberEntity里的字段一模一样,就是封装着用户的基本信息
return "redirect:http://gulimall.com";
}else {
//否则返回登录页
errors.put("msg", "登录失败,请重试");
attributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}else {
errors.put("msg", "获得第三方授权失败,请重试");
attributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
被远程调用的gulimall-member的登录方法:
@Override
public MemberEntity login(SocialUser socialUser) {
//根据 uid 判断当前用户是否以前用社交平台登录过系统
MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", socialUser.getUid()));
if (!StringUtils.isEmpty(memberEntity)) {
// 说明这个用户之前已经注册过
MemberEntity update = new MemberEntity();
update.setId(memberEntity.getId());
update.setAccessToken(socialUser.getAccess_token());
update.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.updateById(update);
memberEntity.setAccessToken(socialUser.getAccess_token());
memberEntity.setExpiresIn(socialUser.getExpires_in());
return memberEntity;
} else {
// 未找到则注册,说明用户是第一次用微博登录
MemberEntity register = new MemberEntity();
try {
//拿着token到微博里面查询该用户的基本信息
Map<String, String> query = new HashMap<>();
query.put("access_token", socialUser.getAccess_token());
query.put("uid", socialUser.getUid());
HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), query);
if (response.getStatusLine().getStatusCode() == 200) {
String json = EntityUtils.toString(response.getEntity());
JSONObject jsonObject = JSON.parseObject(json);
String name = jsonObject.getString("name");
String gender = jsonObject.getString("gender");
// ......
register.setNickname(name);
register.setGender("m".equals(gender) ? 1 : 0);
// .....
}
} catch (Exception e) {
}
register.setSocialUid(socialUser.getUid());
register.setAccessToken(socialUser.getAccess_token());
register.setExpiresIn(socialUser.getExpires_in());
this.baseMapper.insert(register);
return register;
}
}
多台服务器都有会员服务,你在A服务器上把用户信息保存到内存上了,下次如果落在B服务器上,即使浏览器带着cookie来了,由于B服务器内存肯定没有存储用户信息,这也是问题。
session复制
用户登录后,A服务器得到session后,把session也复制到别的机器上,显然这种处理很不好
客户端存储
把session存储到浏览器上,肯定相当不安全
hash一致性
根据用户,到指定的机器上登录。但是远程调用还是不好解决
redis统一存储
最终的选择方案,把session放到redis中,这样每个微服务都可以获取到session
浏览器会在
auth.gulimall.com
里面登录成功,auth.gulimall.com
会将登陆成功的用户的从数据库查到的用户相关信息存到session里面,而且存session时不是存到自己的内存里面而是存到redis里面,然后auth.gulimall.com
给浏览器发cookie,而且发的cookie的作用域不能仅仅是auth.gulimall.com
而是要放大服务到.gulimall.com
,此时浏览器访问其它任何服务都会带上这个cookie。
如果你把redis里面的session清空,那就是把登陆过的用户信息清空,虽然前台的浏览器访问后台时携带了cookie信息,但是到redis里面查不到用户信息,所以你就得重新登陆。而且我们设置了redis里面的session默认30分钟过期,也就是30分钟后redis里面的用户信息就没有了
①修改sprinsession的存储类型是redis(这很重要·,以后存到session中就是存到redis中)
spring:
session:
store-type: redis
②增加一个配置类,由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化,并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
③修改gulimall-auth-server的Controller
以前的逻辑是:
这是微博登陆的代码,前台带着code发送
/oauth2.0/weibo/success
请求到后台的gulimall-auth-server
模块中,然后gulimall-auth-server
会先使用code换取SocialUser,然后拿着SocialUser到OpenFeign远程调用gulimall-member
的oauth2Login()
方法,在该方法中如果是第一次的话我们就得给该用户注册;如果该用户之前已经用微博登陆过,那就到数据库中更新一下token。
如果一切顺利,gulimall-member
就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到RedirectAttributes然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html
现在的逻辑就是:
gulimall-member
就会带着MemberEntity(封装着用户的所有信息)返回到gulimall-auth-server,然后gulimall-auth-server会把MemberEntity设置到SpringSession中然后重定向到http://gulimall.com;如果不顺利就把error信息返回到gulimall-auth-server,然后gulimall-auth-server会把error信息封装到RedirectAttributes然后重定向到http://auth.gulimall.com/login.html
@Controller
public class OauthController {
@Autowired
private MemberFeignService memberFeignService;
@RequestMapping("/oauth2.0/weibo/success")
public String authorize(String code, RedirectAttributes attributes) throws Exception {
//使用code换取token
Map<String, String> query = new HashMap<>();
query.put("client_id", "2144***074");
query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
query.put("grant_type", "authorization_code");
query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
query.put("code", code);
//发送post请求换取token
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
Map<String, String> errors = new HashMap<>();
if (response.getStatusLine().getStatusCode() == 200) {
//调用member远程接口进行oauth登录
String json = EntityUtils.toString(response.getEntity());
SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
});
=========================================================================================================
R r = memberFeignService.oauth2Login(socialUser);
//远程调用成功,返回首页并携带用户信息
if (r.getCode() == 0) {
// MemberResponseVO和MemberEntity的字段一模一样
MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {
});
log.info("登陆成功:用户信息"+loginUser.toString());
//有了下面这行代码,一来redis中可以看到你存的session,二来浏览器中session的作用域也被放大到gulimall.com
session.setAttribute("loginUser", loginUser);
=========================================================================================================
return "redirect:http://gulimall.com";
} else {
//2.2 否则返回登录页
errors.put("msg", "登录失败,请重试");
attributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.catmall.com/login.html ";
}
}else {
errors.put("msg", "获得第三方授权失败,请重试");
attributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}
springsession只能把auth.gulimall.com作用域放大到gulimall.com,解决了同域名的共享session问题,但要是访问同样是尚硅谷的atguigu.com怎么办呢?这种不同的域名也想共享session该怎么做呢?
你在新浪微博里面注册登录了,同时就要保证在新浪体育、新浪新闻里面全都可以拿到session数据
两个域名不一样的服务端client1和client2,还有一个负责登录的ssoserver,还有一个浏览器,它们四个之间的故事
先说明一下这个路径的含义:
http://ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees
的含义就是让你访问http://ssoserver.com:8080/login.html
登陆页面,而redirect_url=http:I/client1.com:8081/employees
的含义是当你完成登陆后会重定向到http:I/client1.com:8081/employees
的位置
第1-11步的解析:只有登陆了才能查看员工信息。一开始浏览器访问client1.com的员工信息
http:I/client1.com:8081/employees
,client1会根据这个url有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client1.com:8081/employees
,ssoserver.com会判断是否登陆过,没有登陆过就展示这个登陆页面,用户会输入账号密码进行登录,提交登陆请求http:/ssoserver.com:8080/doLogin?usermame,password,redirect_url
给ssoserver.com,那么ssoserver.com会保存用户状态到redis,同时ssoserver.com会命令重定向到http: /lclient1.com:8081/employees?token=dadadadsdeuieu
(浏览器访问路径),同时ssoserver.com会命令浏览器保存sso_token=dadadadsdeuieu
这样式的cookie。浏览器这次就可以访问员工信息了,他的访问路径是刚刚提到的http://lclient1.com:8081/employees?token=dadadadsdeuieu
比一开始访问员工信息的http:I/client1.com:8081/employees
多了token=dadadadsdeuieu
,这就回到第2步了,client1会根据有没有token参数判断是否登录,这次client1会觉得它登陆过了就可以访问员工信息了。
第12-19步解析:这次浏览器要访问客户端2的boss信息
http:I/client2.com:8081/boss
,client2会根据有没有token参数判断是否登录,由于没有token参数也就是没有登陆,服务端会命令浏览器重定向到ssoserver.com的登陆页面http:I/ssoserver.com:8080/login.html?redirect_url=http:I/client2.com:8081/boss
,ssoserver.com会判断是否登陆过,由于浏览器有sso_token=dadadadsdeuieu
这样式的cookie,而且从redis能查到,说明它之前在client1或者client2登陆过,ssoserver.com会命令重定向到http:/lclient2.com:8082/boss?token=dadadadsdeuieu
,所以浏览器就会访问http://lclient2.com:8082/boss?token=dadadadsdeuieu
,这就回到了第2步,client2会根据有没有token参数判断是否登录,登陆过就响应页面。
所以说,以后浏览器无论访问client1还是client2,由于浏览器中保存了cookie,所以ssoserver.com就会判定它登陆过,所以以后都不用登陆。
client1的代码:
@GetMapping(value = "/employees")
public String employees(Model model,
HttpSession session,
@RequestParam(value = "redisKey", required = false) String redisKey) {
if (!StringUtils.isEmpty(redisKey)) {
//redisKey非空(也就是token非空),说明去过server端登录过了
// 拿着token去服务器,在服务端从redis中查出来用户的username
RestTemplate restTemplate=new RestTemplate();
ResponseEntity<Object> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?redisKey="+ redisKey, Object.class);
Object loginUser = forEntity.getBody();
session.setAttribute("loginUser", loginUser);//设置到自己的session中
}
//尝试从自己的session中获取"loginUser"
Object loginUser = session.getAttribute("loginUser");
if (loginUser == null) {
//又没有token,session里又没有"loginUser",让它去登录页登录
return "redirect:" + "http://ssoserver.com:8080/login.html" + "?url=http://clientA.com/employees";
} else {
//自己的session里有"loginUser",即使没有token也说明登录过
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees"; //转到前端页面,前端会把数据拿出来展示
}
}
client2的代码:
代码一模一样,就是改一下访问路径@GetMapping(value = "/boss")
ssoserver的代码:
<body>
<form action="/doLogin" method="post">
<input type="hidden" name="url" th:value="${url}">
用户名:<input name="username" value="test"><br/>
密码:<input name="password" type="password" value="test">
<input type="submit" value="登录">
form>
body>
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ResponseBody
@GetMapping("/userInfo") //client1或client2会调用这个方法得到redis中的存储过的user信息
public Object userInfo(@RequestParam("redisKey") String redisKey){
// 拿着其他域名转发过来的token去redis里查
Object loginUser = stringRedisTemplate.opsForValue().get(redisKey);
return loginUser;
}
@GetMapping("/login.html") // 子系统都来这
public String loginPage(@RequestParam("url") String url,
Model model,
@CookieValue(value = "redisKey", required = false) String redisKey) {
//这是从浏览器中拿到的cookie,非空代表就登录过了
if (!StringUtils.isEmpty(redisKey)) {
//非空代表就登录过了
return "redirect:" + url + "?redisKey=" + redisKey;
}
model.addAttribute("url", url);
//没登录过才去登录页
return "login";
}
@PostMapping("/doLogin") //在前端输入用户名和密码后就会来到这里,进行server端统一认证
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletResponse response,
@RequestParam(value="url",required = false) String url){
//确认用户后,生成cookie,浏览器中存储,redis中也存储
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
//非空就简单认为登录正确
String redisKey = UUID.randomUUID().toString().replace("-", "");//用uuid代替token
Cookie cookie = new Cookie("redisKey", redisKey);
response.addCookie(cookie);//浏览器中存储cookie
stringRedisTemplate.opsForValue().set(redisKey, username+password+"...", 30, TimeUnit.MINUTES);//redis中存储
return "redirect:" + url + "?redisKey=" + redisKey;//重定向时候带着token
}
// 登录失败,再次登录
return "login";
}
}
演示
代码用的网友的,截屏用到老师的,网友喜欢自己起名字,把token改为redisKey什么的,不要计较细节上的不同
总说:
本节内容就是说明了用户购物车里的信息应该使用哪个数据库存储(MySQL还是Redis?),以及使用了Redis后是用List存储这些信息呢还是使用Hash存储这些信息?以及购物车VO、购物项VO的编写
在购物车的所有Controller执行之前,我们先执行一个拦截器。在拦截器里判断用户是否登录,从session中获取不到用户信息就说明他没有登录,没有登录的话就从浏览器中获取一下user-key,如果浏览器中没有user-key那就说明用户是第一次
没有登录
的状态下进入京东,我们就得创建一个cookie名字叫做user-key,而且设置cookie的作用域、过期时间,假如明天他来了,我们能从浏览器中获取到该用户的user-key。
一个用户进来我们执行的 “ 拦截器—Controller—Service—Dao ” 这一套流程让同一个线程执行,这就使用了ThreadLocal技术,ThreadLocal是同一个线程共享数据,这个线程里面的数据会共享,使用过程就是:
ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();//创建一个threadLocal
threadLocal.set(userInfoTo);//把要共享的数据设置进去
....
UserInfoTo userInfoTo = threadLocal.get();//后期就可以获取到这个共享的数据
UserInfoTo如下:
@ToString
@Data
public class UserInfoTo {
private Long userId;
private String userKey;
private boolean tempUser = false; //这个相当重要,我们会根据tempUser是true还是false来决定有没有执行postHandle()方法
}
登陆拦截器如下:
/**
* @Description: 在执行目标方法之前,判断用户的登录状态。并封装传递给目标请求
*/
public class CartInterceptor implements HandlerInterceptor {
//ThreadLocal同一个线程共享数据
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();
MemberResponseVO member = (MemberResponseVO) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (member != null){
//用户登录
userInfoTo.setUserId(member.getId());
}
//用户没有登陆:
Cookie[] cookies = request.getCookies();
if (cookies!=null && cookies.length >0){
//有临时用户信息
for (Cookie cookie : cookies) {
String name = cookie.getName();
if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}
//用户没有登陆,而且没有临时用户信息,一定保存一个临时用户
if (StringUtils.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//userInfoTo存到threadLocal中
threadLocal.set(userInfoTo);
return true;
}
/**
* 业务执行之后 分配临时用户,让浏览器保存
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
//如果没有临时用户,第一次访问购物车就添加临时用户
if (!userInfoTo.isTempUser()){
//持续的延长用户的过期时间
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}
请你假设一下以下三种情况在拦截器中会发生什么:
如果用户没有登录,而且浏览器中没有用户的临时信息:UserInfoTo中的userId是空的,但userKey不是空的
如果用户没有登录,但是浏览器中有用户的临时信息:UserInfoTo中的userId是空的,但userKey不是空的
如果用户已经登录,UserInfoTo中的userId不是空的,但userKey是空的
登录Controller如下:
@Controller
public class CartController {
/**
* 登录 session有
* 没登录,按照cookie里面带来的user-key来做
* 第一次,如果没有临时用户,帮忙创建一个临时用户
*/
@GetMapping("/cart.html")
public String cartListPage(){
//快速得到用户信息,id,user-key
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
System.out.println(userInfoTo);
return "cartList";
}
}
拦截器写了,Controller还有service什么的都没有写,在此之前,我们先打通整个页面(首页可以进入商品页,从商品页添加商品到购物车,然后点击购物车就可以进入购物车页),本节内容是前台代码。
getCartOps()方法里面逻辑:
因为是拦截器先执行的,所以先得到拦截器ThreadLocal的返回结果
UserInfoTo userInfoTo = threadLocal.get()
,如果userInfoTo.getUserId()
不为空表示账号用户,反之为临时用户 ,然后决定用临时购物车还是用户购物车。将用户购物车信息存到redis中,redis中肯定需要键值对,账号用户的购物车的redis中的key是gulimall:cart:1
(1是用户id,表示1号用户的购物车);临时用户的redis中的key是gulimall:cart:uuid
其中uuid就是我们拦截器里存下的user-key。 redisTemplate.boundHashOps(cartKey)是说以后所有对redis的增删改查都是针对redia中key为cartKey的增删改查。
addToCart()方法里面的逻辑:
添加新商品到购物车,第一步先看redis里面能不能查到skuid,查不到说明购物车里面之前没有添加过此商品,那就需要远程查询此商品的一系列信息;能查到说明购物车有此商品,将数据取出修改数量即可。
gulimall-cart的Controller:
/**
* 添加商品到购物车
* @return
*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
Model model) throws ExecutionException, InterruptedException {
CartItem cartItem = cartService.addToCart(skuId,num);
model.addAttribute("item",cartItem);
return "success";
}
gulimall-cart的ServiceImpl:
@Slf4j
@Service
public class CartServiceImpl implements CartService {
private final String CART_PREFIX = "gulimall:cart";
@Autowired
StringRedisTemplate redisTemplate;
@Autowired
ProductFeignService productFeignService;
@Autowired
ThreadPoolExecutor executor;
@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String res = (String) cartOps.get(skuId.toString());
// 1、添加新商品到购物车(购物车无此商品)
if (StringUtils.isEmpty(res)){
CartItem cartItem = new CartItem();
/**
* 异步查询
*/
CompletableFuture<Void> getSkuInfo = CompletableFuture.runAsync(() -> {
//1.1、远程查询要添加的商品信息
R skuInfo = productFeignService.getSkuInfo(skuId);
SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItem.setCheck(true);
cartItem.setCount(1);
cartItem.setImage(data.getSkuDefaultImg());
cartItem.setTitle(data.getSkuTitle());
cartItem.setSkuId(skuId);
cartItem.setPrice(data.getPrice());
},executor);
CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
//1.2、远程查询sku的组合信息
List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(values);
}, executor);
CompletableFuture.allOf(getSkuInfo,getSkuSaleAttrValues).get();
String jsonString = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),jsonString);
return cartItem;
}else {
//2、购物车有此商品,将数据取出修改数量即可
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount() + num);
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
return cartItem;
}
}
/**
* 获取我们要操作的购物车,临时购物车、用户购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
//得到用户信息 账号用户 、临时用户
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
//1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户 然后决定用临时购物车还是用户购物车
//放入缓存的key
String cartKey = "";
if (userInfoTo.getUserId() != null){
cartKey = CART_PREFIX + userInfoTo.getUserId();
}else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
//getCartItem()是完成上面的添加商品到购物车后从redis中获取商品信息
@Override
public CartItemVo getCartItem(Long skuId) {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String redisValue = (String) cartOps.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);
return cartItemVo;
}
}
存在的小问题
修改gulimall-cart的Controller:
/**
* 添加商品到购物车
*/
@GetMapping("/addToCart")
public String addToCart(@RequestParam("skuId") Long skuId,
@RequestParam("num") Integer num,
RedirectAttributes attributes) throws ExecutionException, InterruptedException {
cartService.addToCart(skuId,num);
attributes.addAttribute("skuId",skuId);
return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
}
/**
* 跳转到成功页
*/
@GetMapping("/addToCartSuccess.html")
public String addToCartSuccessPage(@RequestParam("skuId") Long skuId,Model model){
CartItem cartItem = cartService.getCartItem(skuId);//调用getCartItem()方法获取商品信息
model.addAttribute("item",cartItem);
return "success";
}
若用户未登录,则直接使用user-key获取购物车数据;否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
Controller
@GetMapping("/cart.html")
public String cartListPage(Model model) throws ExecutionException, InterruptedException {
//快速得到用户信息,id,user-key
// UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
Cart cart = cartService.getCart();
model.addAttribute("cart",cart);
return "cartList";
}
Service:
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
//1、登录
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo.getUserId() != null){
String cartKey = CART_PREFIX + userInfoTo.getUserId();
//1.1、如果临时购物车的数据还没有合并【合并购物车】
String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItem> tempsCartItems = getCartItems(tempCartKey);
if (tempsCartItems != null){
//临时购物车有数据,需要合并
for (CartItem item : tempsCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
//清除临时购物车的数据
clearCart(tempCartKey);
}
//1.2、获取登录后的购物车数据【包含合并过来的临时购物车的数据,和登录后的购物车数据 】
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}else {
//2、没登录
String cartKey = CART_PREFIX + userInfoTo.getUserKey();
//获取临时购物车的所有购物项
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}
return cart;
}
* 获取购物车里面的数据
private List<CartItemVo> getCartItems(String cartKey) {
//获取购物车里面的所有商品
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
List<Object> values = operations.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> {
String str = (String) obj;
CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class);
return cartItem;
}).collect(Collectors.toList());
return cartItemVoStream;
}
return null;
}
@Override
public void clearCart(String cartKey) {
redisTemplate.delete(cartKey);
}
Controller
@GetMapping("/checkItem")
public String checkItem(@RequestParam("skuId") Long skuId, @RequestParam("check") Integer check){
cartService.checkItem(skuId,check);
return "redirect:http://cart.gulimall.com/cart.html";
}
Service:
@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 jsonString = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),jsonString);
}
private BoundHashOperations<String, Object, Object> getCartOps() {
//得到用户信息 账号用户 、临时用户
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
//1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户 然后决定用临时购物车还是用户购物车
//放入缓存的key
String cartKey = "";
if (userInfoTo.getUserId() != null){
cartKey = CART_PREFIX + userInfoTo.getUserId();
}else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
@Override
public CartItemVo getCartItem(Long skuId) {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String redisValue = (String) cartOps.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class);
return cartItemVo;
}
Controller:
@GetMapping("/changeItemCount")
public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num){
cartService.changeItemCount(skuId,num);
return "redirect:http://cart.gulimall.com/cart.html";
}
Service:
@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));
}
Controller:
@GetMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId){
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}
Service:
@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
cartOps.delete(skuId.toString());
}