商品模块博客中的商品类型进行优化。主要是使用Redis进行商品类型后台缓存优化和模板Velocity进行商品类型主页面页面静态化
商品的品牌和类型,都是从数据库中全部获取出来的,每一次都要去数据库中查询一次,从而增加数据库的访问压力,反应时间就会长,造成用户体验差。
- 后台管理(Redis缓存解决)
使用缓存解决对数据库的频繁操作问题,用内存查询替换数据库磁盘查询.- 商城主页(页面静态化解决)
上面的缓存还不够优化,如果高并发,就会访问Redis缓存N多次,那么就会对缓存服务器造成压力。所以以不发请求静态页面代替要发请求静态页面或者动态页面。没有对后台数据获取.
JPA、mybatis的二级缓存,默认情况下不支持集群环境使用
缓存中数据是对数据库中数据的一种内存存放,如何保持两者的数据一致?换言之它们之间如何交互的
在redis中:序列化和json字符串(采纳)
- 存:
就是把数据库的数据存到Redis中(json字符串)- 取:
将Redis(json字符串)的数据转换成对象。
FastJson入门博客地址
将像Redis缓存这样的简单服务封装到一个公共服务模块中去,因为项目不止商品模块要用到缓存,其他的模块也会用到。
先创建公共的服务模块的父模块aigou_common_parent,以及它的公共接口子模块aigou_common_interface和服务子模块aigou_common_service_6699
feign的支持和velocity模板支持(在接下来的页面静态化需要)
负载均衡feign:服务内部之间调用使用负载均衡,可以在服务子模块aigou_common_service_6699中使用feign,但是为了规范,==将feign写在公共的服务模块的公共接口子模块中。==方便其他地方好用
cn.lyq
aigou_basic_util
1.0-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-openfeign
org.apache.velocity
velocity
1.7
/**
* @author lyq
* @date 2019/5/12 21:20
* 对Redis设置和获取的操作接口
*/
//feign客户端注解
@FeignClient(value = "COMMON-PROVIDER",fallback = RedisFall.class)
public interface RedisClient {
/**
* post参数接受:因为set存值有很多参数,用post方式提交
* @RequestParam:请求参数,从请求参数key中拿值设置到参数key中去
* @param key
* @param value
* 设置。将参数以key/value的形式存入Redis中。
*/
@RequestMapping(value = "/redis/set",method = RequestMethod.POST)
void setRedis(@RequestParam("key")String key,@RequestParam("value") String value);
/**
* @param key
* @return
* 获取json字符串。传入个参数key键拿到Redis的json字符串
*/
@RequestMapping(value = "/redis/get/{key}",method = RequestMethod.GET)
String getRedis(@PathVariable("key") String key);
}
/**
* @author lyq
* @date 2019/5/12 21:48
* redis的托底数据。想写啥写啥,暂时没有写
*/
@Component//配成一个组件
public class RedisFall implements FallbackFactory {
public RedisClient create(Throwable throwable) {
//要得到一个RedisClient对象,那么就new一个
return new RedisClient() {
public void setRedis(String key, String value) {
}
public String getRedis(String key) {
return null;
}
};
}
}
上面的公共接口子模块aigou_common_interface是不会暴露出去的,而要把服务子模块aigou_common_service_6699暴露出去。因为有接口要被使用
Redis的客户端jedis的支持、springboot支持、以及注册到注册中心支持
jedis操作Redis,类似于jdbc操作MySQL一个道理
cn.lyq
aigou_common_interface
1.0-SNAPSHOT
redis.clients
jedis
2.9.0
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
controller类要去实现feign的客户端接口RedisClient。因为要约束controller,要一致。它要对外暴露接口。
调用Redis的工具类RedisUtil,完成Redis的set和get方法
@RestController
@RequestMapping("/redis")
public class RedisController implements RedisClient {
@Override
@RequestMapping(value = "/set",method = RequestMethod.POST)
public void setRedis(@RequestParam("key") String key, @RequestParam("value") String value) {
//redis的set方法。调用工具类
RedisUtil.set(key,value);
}
@Override
@RequestMapping(value = "/get/{key}",method = RequestMethod.GET)
public String getRedis(@PathVariable("key") String key) {
//Redis的get方法。调用工具类
return RedisUtil.get(key);
}
}
server:
port: 6699
max-http-header-size: 4048576
spring:
application:
name: COMMON-PROVIDER #Redis的接口类会根据此名字扫描
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
由于只是在Redis的服务端使用得到,所以就将工具类放在自己的服务中aigou_common_service_6699
连接池的配置和API的封装
/**
* redis的工具类
*/
public class RedisUtil {
private static JedisPool jedisPool;
static {
//连接池的配置:
GenericObjectPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(20);
poolConfig.setMaxIdle(5);
poolConfig.setMaxWaitMillis(3000);
poolConfig.setTestOnBorrow(true);
//创建pool:
// (GenericObjectPoolConfig poolConfig, String host, int port, int timeout, String password)
jedisPool = new JedisPool(poolConfig,"127.0.0.1",6379,2000,"123456");
}
//设置。set方法 将值设置到Redis中
public static void set(String key,String value){
Jedis jedis =null;
try {
jedis= jedisPool.getResource();
jedis.set(key,value);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(jedis!=null){
jedis.close();
}
}
}
//获取json字符串。get方法
public static String get(String key){
Jedis jedis =null;
try {
jedis= jedisPool.getResource();
return jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if(jedis!=null){
jedis.close();
}
}
}
//测试Redis是否能用
public static void main(String[] args) {
RedisUtil.set("sex","1");
}
}
配置好了redis服务子模块aigou_common_service_6699,然后再来处理Redis的消费者。现在项目中的Redis的消费者就是商品分类和商品品牌。
使用feign来调用Redis的服务,那么消费者模块aigou_product_service子模块要注入Feign的接口,也就是要使用RedisClient接口。那么就要引入feign接口的依赖
fastjson:操作Redis数据的,能操作对象与json字符串之间的相互转换和list集合和json字符串之间的相互转换
cn.lyq
aigou_common_interface
1.0-SNAPSHOT
com.alibaba
fastjson
1.2.47
在商品模块的博客中已经完成了商品分类的树状图结构,但是没有将数据放入Redis缓存中去,每次都要去数据库查询,很影响性能,所以此处作为Redis的消费者去调用Redis的服务,实现Redis的缓存效果。
做商品类型,从数据库中查询数据之前,先在Redis中查询是否有数据,如果没有那么就要将从数据库中查询出的数据存入到Redis中去,再返回去;如果第一次进来的Redis中就有数据,那么就直接返回。
@Service
public class ProductTypeServiceImpl extends ServiceImpl implements IProductTypeService {
//注入mapper对象
@Autowired
private ProductTypeMapper productTypeMapper;
//注入Redis接口
@Autowired
private RedisClient redisClient;
//实现树状结构
@Override
public List treeData() {
//递归方法。查看商品类型数据库表得知pid=0为最上一级。返回前台的都是一级菜单
//return treeDataRecursion(0L);
//循环方法。
return treeDataLoop();
}
/**
* 步骤:循环查询
* 1:先查询出所有的数据
* 2:再组装父子结构
* @return
*/
public List treeDataLoop() {
//返回的一级菜单
List result = new ArrayList<>();
//1.先查询出所有的数据。selectList方法的参数mapper为null即可
List productTypes=null;
//List productTypes = productTypeMapper.selectList(null);
//数据从Redis中获取
//1.先通过key去Redis中获取json字符串
String productTypeJson = redisClient.getRedis(AiGouConstants.COMMON_PRODUCT_TYPE);
//1.1 Redis中没有这个key,那么就要从数据库去获取,并放入Redis中返回
if(StringUtils.isEmpty(productTypeJson)){
//Redis中没有数据
//就从数据库中查询
productTypes = productTypeMapper.selectList(null);
//然后将数据存入到Redis中。使用fastJson技术
//将list对象集合转换成json字符串
String jsonString = JSON.toJSONString(productTypes);
//将json字符串存入到Redis中。key/value
redisClient.setRedis(AiGouConstants.COMMON_PRODUCT_TYPE,jsonString );
System.out.println("==========from db==============");
}else{
//1.2 Redis中有就直接返回
//返回Redis。将json字符串转换为list集合
productTypes=JSON.parseArray(productTypeJson, ProductType.class);
System.out.println("==========from cache==============");
}
//定义一个map集合。参数是id和productType类
Map map=new HashMap<>();
//再循环查出来的对象,放到map集合中去
for (ProductType cur : productTypes) {
//将对象的id和对象放map集合中去
map.put(cur.getId(), cur);
}
//2.组装父子结构。遍历上面查询到的数据,拿到当前对象
for (ProductType current : productTypes) {
//找到一级菜单
if(current.getPid()==0){
//装到上面定义的result集合中去
result.add(current);
}else{
//否则就不是一级菜单,你是儿子。那么是哪个对象的儿子?
//先定义一个老子菜单
ProductType parent=null;
/*嵌套循环了,更加影响数据库性能,所以不用,用上面定义的map集合方式存放查询出来的数据
/再循环查出来的数据
for (ProductType cur : productTypes) {
//如果cur对象的id就是current对象的pid,那么cur对象就是老子。多找一下表中的id和pid的关系
if(cur.getId()==current.getPid()){
//cur对象就是老子
parent=cur;
}
}
*/
//和上面嵌套循环一个意思。如果map集合中的cur对象的id就是current对象的pid,那么cur对象就是老子。多找一下表中的id和pid的关系
parent = map.get(current.getPid());
//然后拿到老子的儿子
List children = parent.getChildren();
//然后将你自己加进去。你自己是你老子的儿子
children.add(current);
}
}
//返回装好的一级菜单
return result;
}
}
在最上面的操作已经配置好了
融断要配在消费者那一边,而aigou_common_service_6699作为Redis的服务者,商品子模块aigou_product_service作为Redis的消费者。所以融断配置在商品子模块aigou_product_service中。
保证Redis增删改数据的时候Redis同步操作。
商品分类的实现类ProductTypeServiceImpl继承了baomidou包的serviceImpl实现类,serviceImpl中有更新数据的update方法。那么就可以直接重写父类的update方法,完成自己的功能即可。
/**
*
* 商品目录 服务实现类
*
*
* @author lyqtest
* @since 2019-05-10
*/
@Service
public class ProductTypeServiceImpl extends ServiceImpl implements IProductTypeService {
//注入mapper对象
@Autowired
private ProductTypeMapper productTypeMapper;
//注入Redis接口
@Autowired
private RedisClient redisClient;
//实现树状结构
@Override
public List treeData() {
//递归方法。查看商品类型数据库表得知pid=0为最上一级。返回前台的都是一级菜单
//return treeDataRecursion(0L);
//循环方法。
return treeDataLoop();
}
//重写继承ServiceImpl中的更新方法。完成Redis的同步操作
@Override
public boolean update(ProductType entity, Wrapper wrapper) {
//先更新数据库
boolean update = super.update(entity, wrapper);
//再查询数据库,然后再调用下面定义的flushRedis()方法同步数据到Redis中去
flushRedis();
return update;
}
/**
* 步骤:循环查询
* 1:先查询出所有的数据
* 2:再组装父子结构
* @return
*/
public List treeDataLoop() {
//返回的一级菜单
List result = new ArrayList<>();
//1.先查询出所有的数据。selectList方法的参数mapper为null即可
List productTypes=null;
//List productTypes = productTypeMapper.selectList(null);
//数据从Redis中获取
//1.先通过key去Redis中获取json字符串
String productTypeJson = redisClient.getRedis(AiGouConstants.COMMON_PRODUCT_TYPE);
//1.1 Redis中没有这个key,那么就要从数据库去获取,并放入Redis中返回
if(StringUtils.isEmpty(productTypeJson)){
//Redis中没有数据
//调用下面定义的flushRedis()方法从数据库中获取
productTypes = flushRedis();
System.out.println("==========from db 从数据库来==============");
}else{
//1.2 Redis中有就直接返回
//返回Redis。将json字符串转换为list集合
productTypes=JSON.parseArray(productTypeJson, ProductType.class);
System.out.println("==========from cache 从缓存来==============");
}
//定义一个map集合。参数是id和productType类
Map map=new HashMap<>();
//再循环查出来的对象,放到map集合中去
for (ProductType cur : productTypes) {
//将对象的id和对象放map集合中去
map.put(cur.getId(), cur);
}
//2.组装父子结构。遍历上面查询到的数据,拿到当前对象
for (ProductType current : productTypes) {
//找到一级菜单
if(current.getPid()==0){
//装到上面定义的result集合中去
result.add(current);
}else{
//否则就不是一级菜单,你是儿子。那么是哪个对象的儿子?
//先定义一个老子菜单
ProductType parent=null;
//如果map集合中的cur对象的id就是current对象的pid,那么cur对象就是老子。多找一下表中的id和pid的关系
parent = map.get(current.getPid());
//然后拿到老子的儿子
List children = parent.getChildren();
//然后将你自己加进去。你自己是你老子的儿子
children.add(current);
}
}
//返回装好的一级菜单
return result;
}
//从数据库中查询数据
private List flushRedis() {
List productTypes;//就从数据库中查询
productTypes = productTypeMapper.selectList(null);
//然后将数据存入到Redis中。使用fastJson技术
//将list对象集合转换成json字符串
String jsonString = JSON.toJSONString(productTypes);
//将json字符串存入到Redis中。key/value
redisClient.setRedis(AiGouConstants.COMMON_PRODUCT_TYPE,jsonString );
return productTypes;
}
}
虽然在上面的操作中,做了商品分类的Redis中央缓存。
把动态页面使用静态页面替换:就是一个写死数据的html页面。将模板和需要的数据结合,生成一个静态的HTML页面。
虽然加载商品分类时使用到了Redis中央缓存,但是如果主页的访问人数很多,那么也会给Redis带来很大的压力,那么以不发请求的静态页面代替要发请求静态页面或者动态页面。没有从后台获取数据:对后台的压力就减小了,同时页面不用再动态的渲染,用户体验更好。
页面布局和数据不会经常变的,比如说首页。或者页面变化较小,比如详情页等等
完成今天的第二个功能,页面的静态化。由于页面静态化功能以后会在很多地方都需要调用,所以和Redis一样,将这个功能封装成一个服务,在之前的公共服务中增加静态化功能
velocity的工具类放在公共服务模块中的aigou_common_interface接口子模块中。
public class VelocityUtils {
private static Properties p = new Properties();
static {
p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, "");
p.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
p.setProperty(Velocity.INPUT_ENCODING, "UTF-8");
p.setProperty(Velocity.OUTPUT_ENCODING, "UTF-8");
}
/**
* 返回通过模板,将model中的数据替换后的内容
* @param model
* @param templateFilePathAndName
* @return
*/
public static String getContentByTemplate(Object model, String templateFilePathAndName){
try {
Velocity.init(p);
Template template = Velocity.getTemplate(templateFilePathAndName);
VelocityContext context = new VelocityContext();
context.put("model", model);
StringWriter writer = new StringWriter();
template.merge(context, writer);
String retContent = writer.toString();
writer.close();
return retContent;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 根据模板,静态化model到指定的文件 模板文件中通过访问model来访问设置的内容
*
* @param model
* 数据对象
* @param templateFilePathAndName
* 模板文件的物理路径
* @param targetFilePathAndName
* 目标输出文件的物理路径
*/
public static void staticByTemplate(Object model, String templateFilePathAndName, String targetFilePathAndName) {
try {
Velocity.init(p);
Template template = Velocity.getTemplate(templateFilePathAndName);
VelocityContext context = new VelocityContext();
context.put("model", model);
FileOutputStream fos = new FileOutputStream(targetFilePathAndName);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));// 设置写入的文件编码,解决中文问题
template.merge(context, writer);
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 静态化内容content到指定的文件
*
* @param content
* @param targetFilePathAndName
*/
public static void staticBySimple(Object content, String targetFilePathAndName) {
VelocityEngine ve = new VelocityEngine();
ve.init(p);
String template = "${content}";
VelocityContext context = new VelocityContext();
context.put("content", content);
StringWriter writer = new StringWriter();
ve.evaluate(context, writer, "", template);
try {
FileWriter fileWriter = new FileWriter(new File(targetFilePathAndName));
fileWriter.write(writer.toString());
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在公共基础模块aigou_basic_parent的工具类子模块aigou_basic_util中
@RestController
@RequestMapping("/common")
public class PageController implements PageClient {
@RequestMapping(value = "/page",method = RequestMethod.POST)
@Override
public void createPage(@RequestBody Map map) {
//获取参数:
//调用封装的工具类:
Object model=map.get(AiGouConstants.PAGE_MODEL);
String templateFilePathAndName=(String)map.get(AiGouConstants.PAGE_TEMPLATEFILEPATHANDNAME);
String targetFilePathAndName=(String)map.get(AiGouConstants.PAGE_TARGETFILEPATHANDNAME);
System.out.println("model:"+model);
System.out.println("templateFilePathAndName:"+templateFilePathAndName);
System.out.println("targetFilePathAndName:"+targetFilePathAndName);
VelocityUtils.staticByTemplate(model,templateFilePathAndName,targetFilePathAndName);
}
}
上面的操作中已经引入了
由于在上面的Redis中央缓存操作,在商品主页面访问数量非常大的时候,会更Redis造成压力,所以再结合使用静态化页面功能。
/**
*
* 商品目录 服务实现类
*
*
* @author lyqtest
* @since 2019-05-10
*/
@Service
public class ProductTypeServiceImpl extends ServiceImpl implements IProductTypeService {
//注入mapper对象
@Autowired
private ProductTypeMapper productTypeMapper;
//注入Redis接口
@Autowired
private RedisClient redisClient;
//注入静态化接口
@Autowired
private PageClient pageClient;
//同样是重写父类ServiceImpl的方法。这里解决静态化功能
@Override
public boolean updateById(ProductType entity) {
//根据数据库
boolean update = super.updateById(entity);
//同步redis:
List productTypes = flushRedis();
//根据模板,使用数据,生成一个静态页面:
List result=new ArrayList<>();
makeStructure(result,productTypes);
//先生成productType.html
Map map = new HashMap<>();
//设置三个参数:
//需要的数据:就是List
map.put(AiGouConstants.PAGE_MODEL,result);
map.put(AiGouConstants.PAGE_TEMPLATEFILEPATHANDNAME,"H:\\IDEA\\idea_workspace\\aigou_parent\\aigou_common_parent\\aigou_common_interface\\src\\main\\resources\\template\\product.type.vm");
map.put(AiGouConstants.PAGE_TARGETFILEPATHANDNAME,"H:\\IDEA\\idea_workspace\\aigou_parent\\aigou_common_parent\\aigou_common_interface\\src\\main\\resources\\template\\product.type.vm.html");
pageClient.createPage(map);
//再生成home.html
Map homeMap = new HashMap<>();
//设置三个参数:
Map staticRootMap=new HashMap<>();
//根路径:
staticRootMap.put("staticRoot","H:\\IDEA\\idea_workspace\\aigou_parent\\aigou_common_parent\\aigou_common_interface\\src\\main\\resources\\");
homeMap.put(AiGouConstants.PAGE_MODEL,staticRootMap);
homeMap.put(AiGouConstants.PAGE_TEMPLATEFILEPATHANDNAME,"H:\\IDEA\\idea_workspace\\aigou_parent\\aigou_common_parent\\aigou_common_interface\\src\\main\\resources\\template\\home.vm");
homeMap.put(AiGouConstants.PAGE_TARGETFILEPATHANDNAME,"H:\\IDEA\\idea_workspace\\aigou_parent\\aigou_common_parent\\aigou_common_interface\\src\\main\\resources\\template\\home.html");
pageClient.createPage(homeMap);
return update;
}
//实现树状结构
@Override
public List treeData() {
//递归方法。查看商品类型数据库表得知pid=0为最上一级。返回前台的都是一级菜单
//return treeDataRecursion(0L);
//循环方法。
return treeDataLoop();
}
//重写继承ServiceImpl中的更新方法。完成Redis的同步操作
@Override
public boolean update(ProductType entity, Wrapper wrapper) {
//先更新数据库
boolean update = super.update(entity, wrapper);
//再查询数据库,然后再调用下面定义的flushRedis()方法同步数据到Redis中去
flushRedis();
return update;
}
/**
* 步骤:循环查询
* 1:先查询出所有的数据
* 2:再组装父子结构
* @return
*/
public List treeDataLoop() {
//返回的一级菜单
List result = new ArrayList<>();
//1.先查询出所有的数据。selectList方法的参数mapper为null即可
List productTypes=null;
//List productTypes = productTypeMapper.selectList(null);
//数据从Redis中获取
//1.先通过key去Redis中获取json字符串
String productTypeJson = redisClient.getRedis(AiGouConstants.COMMON_PRODUCT_TYPE);
//1.1 Redis中没有这个key,那么就要从数据库去获取,并放入Redis中返回
if(StringUtils.isEmpty(productTypeJson)){
//Redis中没有数据
//调用下面定义的flushRedis()方法从数据库中获取
productTypes = flushRedis();
System.out.println("==========from db 从数据库来==============");
}else{
//1.2 Redis中有就直接返回
//返回Redis。将json字符串转换为list集合
productTypes=JSON.parseArray(productTypeJson, ProductType.class);
System.out.println("==========from cache 从缓存来==============");
}
//调用下面的makeStructure()方法,装入对应的菜单中
makeStructure(result, productTypes);
//返回装好的一级菜单
return result;
}
private void makeStructure(List result, List productTypes) {
//定义一个map集合。参数是id和productType类
Map map=new HashMap<>();
//再循环查出来的对象,放到map集合中去
for (ProductType cur : productTypes) {
//将对象的id和对象放map集合中去
map.put(cur.getId(), cur);
}
//2.组装父子结构。遍历上面查询到的数据,拿到当前对象
for (ProductType current : productTypes) {
//找到一级菜单
if(current.getPid()==0){
//装到上面定义的result集合中去
result.add(current);
}else{
//否则就不是一级菜单,你是儿子。那么是哪个对象的儿子?
//先定义一个老子菜单
ProductType parent=null;
/*嵌套循环了,更加影响数据库性能,所以不用,用上面定义的map集合方式存放查询出来的数据
/再循环查出来的数据
for (ProductType cur : productTypes) {
//如果cur对象的id就是current对象的pid,那么cur对象就是老子。多找一下表中的id和pid的关系
if(cur.getId()==current.getPid()){
//cur对象就是老子
parent=cur;
}
}
*/
//和上面嵌套循环一个意思。如果map集合中的cur对象的id就是current对象的pid,那么cur对象就是老子。多找一下表中的id和pid的关系
parent = map.get(current.getPid());
//然后拿到老子的儿子
List children = parent.getChildren();
//然后将你自己加进去。你自己是你老子的儿子
children.add(current);
}
}
}
//从数据库中查询数据
private List flushRedis() {
List productTypes;//就从数据库中查询
productTypes = productTypeMapper.selectList(null);
//然后将数据存入到Redis中。使用fastJson技术
//将list对象集合转换成json字符串
String jsonString = JSON.toJSONString(productTypes);
//将json字符串存入到Redis中。key/value
redisClient.setRedis(AiGouConstants.COMMON_PRODUCT_TYPE,jsonString );
return productTypes;
}
}