1.无限极树优化
1.1.为什么要优化
每次都要从数据库查询一次.
使用的地方&问题:
1)后台管理
课程类型列表要使用
课程类型树,在后面添加课程时会反复使用。通过下拉选择对应的类型
就算每个人使用时只查询一次,如果人比较多.也要对数据库进行频繁操作.
2)课程主页
如果一亿并发,也会频繁访问数据库。
100w redis? ===页面静态化
1.2.优化方案
1)后台管理
缓存:用内存查询替换数据库磁盘查询.
经常查询,很少修改
2)课程主页
不能用缓存,还是会高并发访问缓存中数据.
页面静态化:以不发请求静态页面代替要发请求静态页面或者动态页面.没有对后台数据获取.
1)每次访问都是html静态页面,不能访问数据
2)只要数据发生改变,就要重新生成新的静态页面替换原来的静态页面.
页面基本不会变,除了数据
1.3.实现
.搭建缓存服务-hrm-common-service-2090
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hrm-common-parent</artifactId>
<groupId>com.penny</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hrm-common-service-2090</artifactId>
<dependencies>
<!--作为中间依赖包,提供公共代码供feign和service调用-->
<dependency>
<groupId>com.penny</groupId>
<artifactId>hrm-common-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Eureka 客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--配置中心支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--fastdfs包-->
<dependency>
<groupId>cn.bestwu</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27</version>
</dependency>
<!--redis客户端-jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
</project>
配置-redis.properties
redis.host=127.0.0.1
redis.port=6379
redis.password=admin
redis.timeout=1000
RedisUtils工具类
/**
* 获取连接池对象
*/
public enum RedisUtils {
INSTANCE;
static JedisPool jedisPool = null;
static {
//1 创建连接池配置对象
JedisPoolConfig config = new JedisPoolConfig();
//2 进行配置-四个配置
config.setMaxIdle(1);//最小连接数
config.setMaxTotal(11);//最大连接数
config.setMaxWaitMillis(10 * 1000L);//最长等待时间
config.setTestOnBorrow(true);//测试连接时是否畅通
//3 通过配置对象创建连接池对象
Properties properties = null;
try {
properties = new Properties();
properties.load(RedisUtils.class.getClassLoader().getResourceAsStream("redis.properties"));
} catch (IOException e) {
e.printStackTrace();
}
String host = properties.getProperty("redis.host");
String port = properties.getProperty("redis.port");
String password = properties.getProperty("redis.password");
String timeout = properties.getProperty("redis.timeout");
System.out.println(host);
System.out.println(port);
System.out.println(password);
System.out.println(timeout);
jedisPool = new JedisPool(config, host, Integer.valueOf(port),Integer.valueOf(timeout), password);
}
//获取连接
public Jedis getSource() {
return jedisPool.getResource();
}
//关闭资源
public void closeSource(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
/**
* 设置字符值
*
* @param key
* @param
*/
public void del(String key) {
Jedis jedis = getSource();
jedis.del(key);
closeSource(jedis);
}
/**
* 设置字符值
*
* @param key
* @param value
*/
public void set(String key, String value) {
Jedis jedis = getSource();
jedis.set(key, value);
closeSource(jedis);
}
/**
* 设置
* @param key
* @param value
*/
public void set(byte[] key, byte[] value) {
Jedis jedis = getSource();
jedis.set(key, value);
closeSource(jedis);
}
/**
*
* @param key
* @return
*/
public byte[] get(byte[] key) {
Jedis jedis = getSource();
try {
return jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeSource(jedis);
}
return null;
}
/**
* 设置字符值
*
* @param key
*/
public String get(String key) {
Jedis jedis = getSource();
try {
return jedis.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeSource(jedis);
}
return null;
}
}
Contorler
@RestController
/**
* 增/修改 set
* 删除 del
* 查询 get
*/
@RequestMapping("/redis")
public class RedisController {
@PostMapping
public AjaxResult add(
@RequestParam(value = "key",required = true) String key,
@RequestParam(value = "value",required = true) String value)
{
try
{
RedisUtils.INSTANCE.set(key,value);
return AjaxResult.me();
}catch (Exception e){
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("添加失败");
}
}
@DeleteMapping
public AjaxResult del(@RequestParam(value = "key",required = true) String key)
{
try
{
RedisUtils.INSTANCE.del(key);
return AjaxResult.me();
}catch (Exception e){
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("删除失败");
}
}
@GetMapping
public String get(@RequestParam(value = "key",required = true) String key)
{
try
{
return RedisUtils.INSTANCE.get(key);
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
1.4.把服务通过client暴露出去 hrm-common-client
Pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hrm-common-parent</artifactId>
<groupId>com.penny</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hrm-common-client</artifactId>
<dependencies>
<!--作为中间依赖包,防止feign报错-->
<dependency>
<groupId>com.penny</groupId>
<artifactId>hrm-common-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--给调用的模块来转换json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<!--客户端feign支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
Client
package com.penny.client;
import com.penny.util.AjaxResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
//服务调用兼托底方法
@FeignClient(value = "HRM-COMMON", fallbackFactory = RedisClientFallbackFactory.class )//服务提供
@RequestMapping("/redis")
public interface RedisClient {
@PostMapping
AjaxResult add(@RequestParam(value = "key",required = true) String key,@RequestParam(value = "value",required = true) String value);
@DeleteMapping
AjaxResult del(@RequestParam(value = "key",required = true) String key);
@GetMapping
String get(@RequestParam(value = "key",required = true) String key);
}
Factory
package com.penny.client;
import com.penny.util.AjaxResult;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class RedisClientFallbackFactory implements FallbackFactory<RedisClient> {
@Override
public RedisClient create(Throwable throwable) {
return new RedisClient() {
@Override
public AjaxResult add(String key, String value) {
return AjaxResult.me().setSuccess(false).setMessage("redis调用失败");
}
@Override
public AjaxResult del(String key) {
return AjaxResult.me().setSuccess(false).setMessage("redis调用失败");
}
@Override
public String get(String key) {
return "redis调用失败";
}
};
}
}
通过feign完成服务间调用
package com.penny;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class CourseService2020App {
public static void main(String[] args) {
SpringApplication.run(CourseService2020App.class,args);
}
}
缓存逻辑实现
cache
@Component
public class CourseTypeCache {
@Autowired
private RedisClient redisClient;
private static final String TYPETREEDATA_IN_REDIS = "typetreedata_in_redis";
/**
* 从redis获取数据
* @return
*/
public List<CourseType> getCourseTypes() {
String redisData = redisClient.get(TYPETREEDATA_IN_REDIS);
return JSONArray.parseArray(redisData,CourseType.class);
}
/**
* 设置数据到redis
* @param courseTypesDb
*/
public void setCourseTypes(List<CourseType> courseTypesDb) {
String jsonStr = JSONArray.toJSONString(courseTypesDb);
redisClient.set(TYPETREEDATA_IN_REDIS,jsonStr);
}
}
Service
package com.penny.service.impl;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.penny.cache.ICourseTypeCache;
import com.penny.domain.CourseType;
import com.penny.mapper.CourseTypeMapper;
import com.penny.service.ICourseTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* 课程目录 服务实现类
* </p>
*
* @author Penny
* @since 2020-02-17
*/
@Service
public class CourseTypeServiceImpl extends ServiceImpl<CourseTypeMapper, CourseType> implements ICourseTypeService {
@Autowired
private CourseTypeMapper courseTypeMapper;
//和mapper差不多,mapper操作的是数据库的数据,而换成操作的是redis数据
@Autowired
private ICourseTypeCache courseTypeCache;
@Override
public List<CourseType> treeData(long pid) {
//如果换成中有就直接返回
List<CourseType>courseTypes = courseTypeCache.getTreeData();
if (courseTypes!=null && courseTypes.size()>0){
System.out.println("cache ....");
return courseTypes;
}else{
System.out.println("db....");
//否则先查询数据库,放入换成后再返回
//递归思想 每一个节点都会发一天sql
//return treeDataRecursion(pid);
//循环方案 一条sql
List<CourseType> courseTypesOfDb = treeDataLoop(pid);
courseTypeCache.setTreeData(courseTypesOfDb);
return courseTypesOfDb;
}
}
//循环方案-一条sql自己组织关系
private List<CourseType> treeDataLoop(Long pid) {
//1查询所有的节点
List<CourseType> allNodes = courseTypeMapper.selectList(null);
//为了查询父亲方便,我建立所有节点的一个key(Id)-vue(Node)集合 n
Map<Long,CourseType> allNodeDto = new HashMap<>();
for (CourseType courseType : allNodes) {
allNodeDto.put(courseType.getId(),courseType);
}
//2组织关系
List<CourseType> result = new ArrayList<>();
for (CourseType courseType : allNodes) { //n
//2.1 如果是一级节点之间放入result
if (courseType.getPid().intValue()==0) {
System.out.println("**********此处放入result**********");
result.add(courseType);
for (CourseType type : result) {
System.out.println(type);
}
}else{
//2.2 不是一级节点要建立父子关系-把自己作为父亲一个儿子添加进去
//2.2.1 获取父亲
Long pidTmp = courseType.getPid(); //不能发sql
System.out.println("pidTmp为:"+pidTmp);
//方案1:遍历获取父亲,效率低下 两层for n*N
/*
for (CourseType courseType1 : allNodes) {
if (courseType1.getId() == pidTmp) {
//获取到了父亲了。。。。
}
}*/
//方案2:提前建立id和node直接关系,直接获取 //2n
CourseType parent = allNodeDto.get(pidTmp);
System.out.println("**********此处放入parent**********");
//2.2.2 给父亲添加儿子(自己)
parent.getChildren().add(courseType);
System.out.println("parent为:"+parent);
for (CourseType type : result) {
System.out.println(type);
}
}
}
return result;
}
/**
* 通过父id查询儿子,有儿子就设置为自己的儿子,没有就返回
* 自己调用自己
* //返回条件
* @param pid
* @return
*/
private List<CourseType> treeDataRecursion(Long pid) {
//发一条sql
List<CourseType> children = courseTypeMapper
.selectList(new EntityWrapper<CourseType>().eq("pid", pid));
//返回条件
if (children==null || children.size()<1){
return null;
}
for (CourseType child : children) {
//自己调用自己
List<CourseType> cTmp = treeDataRecursion(child.getId());
child.setChildren(cTmp);
}
return children;
}
//重写增删改方法-同步缓存
@Override
public boolean insert(CourseType entity) {
courseTypeMapper.insert(entity);
courseTypeCache.setTreeData(treeDataLoop(0L));
return true;
}
@Override
public boolean deleteById(Serializable id) {
courseTypeMapper.deleteById(id);
courseTypeCache.setTreeData(treeDataLoop(0L));
return true;
}
@Override
public boolean updateById(CourseType entity) {
courseTypeMapper.updateById(entity);
courseTypeCache.setTreeData(treeDataLoop(0L));
return true;
}
}
2.缓存穿透,缓存击穿,缓存雪崩解决方案:
可详见:缓存穿透,缓存击穿,缓存雪崩解决方案
缓存穿透:
高并发访问数据库中不存在数据,放入缓存的数据也没有,击穿缓存每次都要查询数据库.
如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存击穿:
一个key过期,又来高并发访问. 直接高并发访问数据库.
1)让热点数据永远不过期.
2)加互斥锁,互斥锁参考代码如下 我在访问数据库是你就不要访问
缓存雪崩:
一堆key同时过期
设置过期时间不一致就ok. 热点数据永远不过期