今日任务
文章目录
-
课程类型的优化
-
无限极树的优化
-
课程类型的缓存
-
常见的缓存实现方案:
- 缓存的工作原理图
- 数据存储-json(转换)
- Fastjson
-
缓存和项目集成
-
与Redis的集成
-
与Redis的交互部分
* 测试:- 把服务通过client暴露出去
-
课程类型调用client
- 代码实现:
-
题外话
课程类型的优化
无限极树的优化
之前我们做无限极树用的是递归 但是这样每次都要去访问一次数据库 这对我们的数据库来说无敌是压力巨大的 所以我们需要做进一步的优化
方案一:发送一天SQL 将所以数据都查询出来 然后在通过循环将子级节点放入到父级节点中
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 1//循环方案-一条sql自己组织关系
2private List<CourseType> treeDataLoop(long pid) {
3 //1查询所有的节点
4 List<CourseType> allNodes = courseTypeMapper.selectList(null);
5
6 //为了查询父亲方便,我建立所有节点的一个key(Id)-vue(Node)集合 n
7 Map<Long,CourseType> allNodeDto = new HashMap<>();
8 for (CourseType courseType : allNodes) {
9 allNodeDto.put(courseType.getId(),courseType);
10 }
11 //2组织关系
12 List<CourseType> result = new ArrayList<>();
13 for (CourseType courseType : allNodes) { //n
14 //2.1 如果是一级节点之间放入result
15 if (courseType.getPid().intValue()==0) {
16 result.add(courseType);
17 }else{
18 //2.2 不是一级节点要建立父子关系-把自己作为父亲一个儿子添加进去
19 //2.2.1 获取父亲
20 Long pidTmp = courseType.getPid(); //不能发sql
21 //方案1:遍历获取父亲,效率低下 两层for n*N
22 /*
23 for (CourseType courseType1 : allNodes) {
24 if (courseType1.getId() == pidTmp) {
25 //获取到了父亲了。。。。
26 }
27 }*/
28 //方案2:提前建立id和node直接关系,直接获取 //2n
29 CourseType parent = allNodeDto.get(pidTmp);
30 //2.2.2 给父亲添加儿子(自己)
31 parent.getChildren().add(courseType);
32 }
33 }
34 return result;
35}
36
37
启动后会报一个错 空指针异常 因为在我们的实体类中 如果第一次没有创建一个空的集合 在parent.getChildren()获取的时候就获取不到 所以我们需要在domain里面 初始化儿子的时候就新建一个没有数据的集合
这个时候还有一个问题:
虽然就算我们只发送了一条sql 但是如果有几万个人同时访问 这样对数据库的压力也是巨大的 所以需要进一步的优化 —- 缓存
缓存:用内存查询替换数据库磁盘查询.
经常查询,很少修改
但是这种技术也只适合于后台的前端 不适合前台的前端 因为即使做了缓存 如果有几万个人同时访问页面 那么缓存也是抵不住压力的 这里就涉及到了一个新的技术 —–
页面静态化
课程类型的缓存
常见的缓存实现方案:
- 二级缓存 :jpa,Mybatis 默认情况下,
不支持集群环境使用
在没有集群的时候就可以使用它
- 中央缓存:redis/memcached 集群环境
缓存的工作原理图
数据存储-json(转换)
1
2
3
4
5
6
7
8
9
10
11 1queryCourseTypes{
2 List<CourseType> xxx = cache.queryCourseTypes(); // string--->Object
3 if(xxx ==null){
4 List<CourseType> yyy = maper.queryCourseTypes();
5 cache.setCourseTypes(yyy); //string object->string
6 return yyy;
7 }
8 return xxx;
9}
10
11
说明: 当我们去查询一个数据时 我们首先去缓存中查询cache.queryCourseTypes() , 如果缓存中的数据不为空 则直接返回 如果数据xxx为空 就去数据库查询该数据 然后先将数据存入到缓存中 然后返回数据
但是缓存是不支持直接存放对象的 所以我们需要将对象转换为json字符串进行存储
而要获取数据 就要 把json字符串转换为java对象进行返回
java对象和json字符串之间相互转换?
需要使用到json的框架 json-lib,jackson,gson,fastjson
这个给大家一篇参考文章 各种JSON比较(josn-lib,jackson,gson,fastjson)
这里我们使用的是Alibaba的
fastjson
Fastjson是一个Java语言编写的高性能的JSON处理器,由阿里巴巴公司开发。无依赖,不需要例外额外的jar,能够直接跑在JDK上。FastJson在复杂类型的Bean转换Json上会出现一些问题,可能会出现引用的类型,导致Json转换出错,需要制定引用。
FastJson采用独创的算法,将parse的速度提升到极致,超过所有json库
Fastjson
我们先来看一串数据
这分别是 1000 , 5000, 10000条数据下转换所用的时间 课件fastjson有多fast了
我们先来做一个测试
创建一个maven项目并添加依赖
1
2
3
4
5
6
7
8 1<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
2<dependency>
3 <groupId>com.alibaba</groupId>
4 <artifactId>fastjson</artifactId>
5 <version>1.2.58</version>
6</dependency>
7
8
Java对象转json字符串
输出:
可以看见第二排后面两条数据打印的是 引用地址 这也是为什么速度那么快的原因 这也是fastjson的优化之一
json字符串转Java对象
输出:
缓存和项目集成
除了课程模块要用缓存,其他模块也要用缓存,所有抽取缓存服务供其他模块使用,并且把像缓存这样的简单服务全部封装在一个公共服务模块里面
与Redis的集成
参考文章
NoSQL-Redis基本介绍
Redis客户端的基本使用和在java中使用
与Redis的交互部分
为了简化我们的代码 我的是将他放在之前的
hrm-common-service-2090中
首先还是先导入依赖 在原有的基础上再加上jedis
1
2
3
4
5
6
7
8
9 1<!-- redis客户端-jedis -->
2<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
3<dependency>
4 <groupId>redis.clients</groupId>
5 <artifactId>jedis</artifactId>
6 <version>2.9.0</version>
7</dependency>
8
9
编写配置类
redis.properties
1
2
3
4
5
6 1redis.host=127.0.0.1
2redis.port=6379
3redis.password=admin
4redis.timeout=1000
5
6
导入Redis工具类
RedisUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112 1/**
2 * 获取连接池对象
3 */
4public enum RedisUtils {
5 INSTANCE;
6 static JedisPool jedisPool = null;
7
8 static {
9 //1 创建连接池配置对象
10 JedisPoolConfig config = new JedisPoolConfig();
11 //2 进行配置-四个配置
12 config.setMaxIdle(1);//最小连接数
13 config.setMaxTotal(11);//最大连接数
14 config.setMaxWaitMillis(10 * 1000L);//最长等待时间
15 config.setTestOnBorrow(true);//测试连接时是否畅通
16 //3 通过配置对象创建连接池对象
17 Properties properties = null;
18 try {
19 properties = new Properties();
20 properties.load(RedisUtils.class.getClassLoader().getResourceAsStream("redis.properties"));
21 } catch (IOException e) {
22 e.printStackTrace();
23 }
24 String host = properties.getProperty("redis.host");
25 String port = properties.getProperty("redis.port");
26 String password = properties.getProperty("redis.password");
27 String timeout = properties.getProperty("redis.timeout");
28 jedisPool = new JedisPool(config, host, Integer.valueOf(port),Integer.valueOf(timeout), password);
29 }
30
31 //获取连接
32 public Jedis getSource() {
33 return jedisPool.getResource();
34 }
35
36 //关闭资源
37 public void closeSource(Jedis jedis) {
38 if (jedis != null) {
39 jedis.close();
40 }
41
42 }
43 /**
44 * 设置字符值
45 *
46 * @param key
47 * @param
48 */
49 public void del(String key) {
50 Jedis jedis = getSource();
51 jedis.del(key);
52 closeSource(jedis);
53 }
54 /**
55 * 设置字符值
56 *
57 * @param key
58 * @param value
59 */
60 public void set(String key, String value) {
61 Jedis jedis = getSource();
62 jedis.set(key, value);
63 closeSource(jedis);
64 }
65
66 /**
67 * 设置
68 * @param key
69 * @param value
70 */
71 public void set(byte[] key, byte[] value) {
72 Jedis jedis = getSource();
73 jedis.set(key, value);
74 closeSource(jedis);
75 }
76
77 /**
78 *
79 * @param key
80 * @return
81 */
82 public byte[] get(byte[] key) {
83 Jedis jedis = getSource();
84 try {
85 return jedis.get(key);
86 } catch (Exception e) {
87 e.printStackTrace();
88 } finally {
89 closeSource(jedis);
90 }
91 return null;
92
93 }
94
95 /**
96 * 设置字符值
97 * @param key
98 */
99 public String get(String key) {
100 Jedis jedis = getSource();
101 try {
102 return jedis.get(key);
103 } catch (Exception e) {
104 e.printStackTrace();
105 } finally {
106 closeSource(jedis);
107 }
108 return null;
109 }
110}
111
112
测试:
编写Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 1@RestController
2/**
3 * 增/修改 set
4 * 删除 del
5 * 查询 get
6 */
7@RequestMapping("/redis")
8public class RedisController {
9 @PostMapping
10 public AjaxResult add(
11 @RequestParam(value = "key",required = true) String key,
12 @RequestParam(value = "value",required = true) String value)
13 {
14 try
15 {
16 RedisUtils.INSTANCE.set(key,value);
17 return AjaxResult.me();
18 }catch (Exception e){
19 e.printStackTrace();
20 return AjaxResult.me().setSuccess(false).setMessage("添加失败");
21 }
22 }
23
24 @DeleteMapping
25 public AjaxResult del(@RequestParam(value = "key",required = true) String key)
26 {
27 try
28 {
29 RedisUtils.INSTANCE.del(key);
30 return AjaxResult.me();
31 }catch (Exception e){
32 e.printStackTrace();
33 return AjaxResult.me().setSuccess(false).setMessage("删除失败");
34 }
35 }
36
37 @GetMapping
38 public String get(@RequestParam(value = "key",required = true) String key)
39 {
40 try
41 {
42 return RedisUtils.INSTANCE.get(key);
43 }catch (Exception e){
44 e.printStackTrace();
45 }
46 return null;
47 }
48}
49
50
启动服务:
修改redis的配置文件 修改密码
并启动redis
我们可以通过Swagger测试一下:
获取
测试成功
刚刚我们是通过浏览器访问的 如果要在服务间进行一个调用 我们要使用到
feign
把服务通过client暴露出去
我们的client有可能会被多个模块使用 我们需要通过client把服务暴露出去(服务之间的调用 Feign)
创建一个子模块
hrm-common-client
导入相关依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 1<dependency>
2 <groupId>org.leryoo</groupId>
3 <artifactId>hrm-common-common</artifactId>
4 <version>1.0-SNAPSHOT</version>
5</dependency>
6
7<dependency>
8 <groupId>org.springframework.boot</groupId>
9 <artifactId>spring-boot-starter-web</artifactId>
10</dependency>
11<dependency>
12 <groupId>org.springframework.boot</groupId>
13 <artifactId>spring-boot-starter-test</artifactId>
14 <scope>test</scope>
15</dependency>
16
17
18<!-- 给调用的模块来转换json-->
19<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson 调用者需要转换-->
20<dependency>
21 <groupId>com.alibaba</groupId>
22 <artifactId>fastjson</artifactId>
23 <version>1.2.58</version>
24</dependency>
25<!--客户端feign支持-->
26<dependency>
27 <groupId>org.springframework.cloud</groupId>
28 <artifactId>spring-cloud-starter-openfeign</artifactId>
29</dependency>
30
31
这里需要注意的是 fastjson是给调用他的模块调用的 不是redis服务端调用的
在
hrm-common-client中创建 Redis的客户端
RedisClient.java
1
2
3
4
5
6
7
8
9
10
11
12 1@FeignClient(value = "HRM-COMMON", fallbackFactory = RedisClientFallbackFactory.class )//ζœεŠʽζδΎ›
2@RequestMapping("/redis")
3public interface RedisClient {
4 @PostMapping
5 AjaxResult add(@RequestParam(value = "key",required = true) String key,@RequestParam(value = "value",required = true) String value);
6 @DeleteMapping
7 AjaxResult del(@RequestParam(value = "key",required = true) String key);
8 @GetMapping
9 String get(@RequestParam(value = "key",required = true) String key);
10}
11
12
然后是熔断后的托底回调函数
RedisClientFallbackFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 1@Component
2public class RedisClientFallbackFactory implements FallbackFactory<RedisClient> {
3 @Override
4 public RedisClient create(Throwable throwable) {
5 return new RedisClient() {
6 @Override
7 public AjaxResult add(String key, String value) {
8 return AjaxResult.me().setSuccess(false).setMessage("redis调用失败");
9 }
10
11 @Override
12 public AjaxResult del(String key) {
13 return AjaxResult.me().setSuccess(false).setMessage("redis调用失败");
14 }
15
16 @Override
17 public String get(String key) {
18 return "redis调用失败";
19 }
20 };
21 }
22}
23
24
课程类型调用client
通过feign来完成服务间的调用
首先要在
course-service-2020的服务中先去引入client
1
2
3
4
5
6
7
8 1<!-- 通过feign调用client-->
2<dependency>
3 <groupId>cn.itsource</groupId>
4 <artifactId>hrm-common-client</artifactId>
5 <version>1.0-SNAPSHOT</version>
6</dependency>
7
8
本来还应该要导一个
但是依赖了
hrm-common-client 而那里面已经导入了 所以就不需要在导入了
最后需要去扫描Client 在启动类上加上
这样才能去扫描到Feign 只有去扫描了才会产生代理对象 最终通过代理对象完成一个调用
之后我们想要调用它就很简单了 直接将
RedisClient注入就可以了
整体流程为:先在调用者的服务里面引入client的依赖 ——> 启动类里面加上@EnableFeignClients注解——> 这样我们就能通过@EnableFeignClients 去扫描到加了@FeignClient的RedisClient类 ——> 扫描到了就会产生代理对象 ——> 通过代理对象就能远程访问名字叫value = "HRM-COMMON"的服务 并且访问路径
1 | 1` ```` ` ```` ` |
@RequestMapping("/redis")要和服务里的@RequestMapping("/redis")
1 | 1` |
一致
代码实现:
新建一个
cache的包 作用和
mapper一样 一个是从数据库查询数据 一个是从缓存中去查询数据
接口:ICourseTypeCache.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1public interface ICourseTypeCache {
2 /**
3 * 获取courseType的缓存
4 * @return
5 */
6 List<CourseType> getTreeData();
7
8 /**
9 * 设置courseType到缓存中
10 * @param courseTypesOfDb
11 */
12 void setTreeData(List<CourseType> courseTypesOfDb);
13}
14
15
实现类:
CourseTypeCacheImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 1@Component
2public class CourseTypeCacheImpl implements ICourseTypeCache {
3
4 //获取和设置都要使用
5 private final static String COURSETYPE_KEY_IN_CACHE = "courseType_Key_in_cache";
6 @Autowired
7 private RedisClient redisClient;
8 @Override
9 public List<CourseType> getTreeData() {
10 //获取的是字符串
11 String treeDataOfCache = redisClient.get(COURSETYPE_KEY_IN_CACHE);
12 //转换为java对象
13 return JSONArray.parseArray(treeDataOfCache,CourseType.class);
14 }
15
16 @Override
17 public void setTreeData(List<CourseType> courseTypesOfDb) {
18
19 if (courseTypesOfDb == null || courseTypesOfDb.size()<1){
20 //缓存穿透
21 redisClient.add(COURSETYPE_KEY_IN_CACHE,"[]");
22 }else{
23 //转换为json字符串
24 String jsonArrStr = JSONArray.toJSONString(courseTypesOfDb);
25 //设置获取
26 //缓存数据永远不过期 缓存击穿 缓存雪崩
27 redisClient.add(COURSETYPE_KEY_IN_CACHE,jsonArrStr);
28 }
29
30 }
31}
32
33
注意要加@Component注解
然后我们需要修改CourseTypeServiceImpl.java中的代码
查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1@Autowired
2private ICourseTypeCache courseTypeCache;
3
4@Override
5public List<CourseType> treeData(long pid) {
6
7 //如果缓冲中有就直接返回
8 List<CourseType>courseTypes = courseTypeCache.getTreeData();
9 if (courseTypes!=null && courseTypes.size()>0){
10 System.out.println("cache ....");
11 return courseTypes;
12 }else{
13 System.out.println("db....");
14 //否则先查询数据库,放入换成后再返回
15 List<CourseType> courseTypesOfDb = treeDataLoop(pid);
16 courseTypeCache.setTreeData(courseTypesOfDb);
17 return courseTypesOfDb;
18 }
19
20}
21
22
然后我们去
覆写默认的增删改的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1@Override
2public boolean insert(CourseType entity) {
3
4 courseTypeMapper.insert(entity);
5 courseTypeCache.setTreeData(treeDataLoop(0));
6 return true;
7}
8
9@Override
10public boolean deleteById(Serializable id) {
11 courseTypeMapper.deleteById(id);
12 courseTypeCache.setTreeData(treeDataLoop(0));
13 return true;
14}
15
16@Override
17public boolean updateById(CourseType entity) {
18 courseTypeMapper.updateById(entity);
19 courseTypeCache.setTreeData(treeDataLoop(0));
20 return true;
21}
22
23
说明:courseTypeCache.setTreeData(treeDataLoop(0));是查询所有的语句 当我们修改了数据库的值后 就查询所有的值 然后存入缓存中
题外话
- 为什么要用缓存? 经常查询而很少修改
- 为什么要用redis缓存? 如果项目中有集群的话就建议用redis缓存 因为二级缓存不知道集群
- 使用缓存好处? 减轻数据库压力 提高访问速度,增强用户体验
- 缓存数据很多的的情况怎么办? redis集群
主从复制-单主故障
哨兵模式-每个节点数据都是一样
redis-cluster: 单点故障,高并发,大量数据
- 缓存穿透,缓存击穿,缓存雪崩
参考文章 缓存穿透、缓存击穿、缓存雪崩区别和解决方案