zoukankan      html  css  js  c++  java
  • Spring Boot 学习之缓存和 NoSQL 篇(四)

    该系列并非完全原创,官方文档作者

    一、前言

    当系统的访问量增大时,相应的数据库的性能就逐渐下降。但是,大多数请求都是在重复的获取相同的数据,如果使用缓存,将结果数据放入其中可以很大程度上减轻数据库的负担,提升系统的响应速度。

    本篇将介绍 Spring Boot 中缓存和 NoSQL 的使用。上篇文章《Spring Boot 入门之持久层篇(三)》

    二、整合EhCache 缓存

    Spring Boot 针对不同的缓存技术实现了不同的封装,提供了以下几个注解实现声明式缓存:

    @EnableCaching	开启缓存功能,放在配置类或启动类上
    @CacheConfig	缓存配置,设置缓存名称
    @Cacheable	执行方法前先查询缓存是否有数据。有则直接返回缓存数据;否则查询数据再将数据放入缓存
    @CachePut	执行新增或更新方法后,将数据放入缓存中
    @CacheEvict	清除缓存
    @Caching	将多个缓存操作重新组合到一个方法中

    1、添加依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
    </dependency>

    2、添加配置

    在 src/main/resources 目录下创建 ehcache.xml 文件,内容如下:

    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="ehcache.xsd">
        <cache name="question" 
               eternal="false"
               maxEntriesLocalHeap="0" 
               timeToIdleSeconds="50">
        </cache>
    </ehcache>

    这里的name可以多个,与@CacheConfig的cacheNames对应

    application.properties 添加

    spring.cache.type=ehcache
    spring.cache.ehcache.config=classpath:ehcache.xml
    

    3、添加缓存注解

    在前文基础之上进行修改添加

    Service层

    package com.phil.springboot.service.impl;
    
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheConfig;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.util.StringUtils;
    
    import com.phil.springboot.mappper.QuestionMapper;
    import com.phil.springboot.service.QuestionService;
    
    @Service
    @CacheConfig(cacheNames = "question")
    public class QuestionServiceImpl implements QuestionService {
    
    	@Autowired
    	private QuestionMapper questionMapper;
    	
    	@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
    	@Override
    	@Cacheable(key = "#params")
    	public List<Map<String, Object>> findByPage(Map<String, Object> params) {
    		return questionMapper.findByPage(params);
    	}
    	
    	@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
    	@Override
    	@Cacheable(key = "#params")
    	public Map<String, Object> findByProperty(Map<String, Object> params) {
    		return questionMapper.findByProperty(params);
    	}
    	
    	@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
    	@Override
    	@CachePut(key = "#params")
    	public Integer saveOrUpdate(Map<String, Object> params){
    		Integer i = 0;
    		if (StringUtils.isEmpty(params.get("id"))) {
    			i = questionMapper.save(params);
    		} else {
    			i = questionMapper.update(params);
    			i ++;
    		}
    		return i;
    	}
    	
    	@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
    	@CacheEvict(key = "#ids")
    	@Override
    	public Integer delete(String ids){
    		if(StringUtils.isEmpty(ids)){
    			return -1;
    		}
    		String[] strs = ids.trim().split(",");
    		Integer[] ids_ = new Integer[strs.length];
    		for(int i = 0; i < strs.length; i++){
    			ids_[i] = Integer.parseInt(strs[i]);
    		}
    		return questionMapper.delete(ids_);
    	}
    }

    控制层

    package com.phil.springboot.controller;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.phil.springboot.service.QuestionService;
    
    import io.swagger.annotations.Api;
    
    @Api(value = "问题Rest接口")
    @RestController
    @RequestMapping("api/question")
    public class QuestionController {
    	
    	private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    	@Autowired
    	private QuestionService questionService;
    	
    	@PostMapping("list")
    	public Map<String, Object> list(@RequestBody Map<String, Object> map) {
    		Map<String, Object> data = new HashMap<String, Object>();
    		List<Map<String, Object>> list;
    		try {
    			list = questionService.findByPage(map);
    			data.put("msg", list);
    			data.put("code", 200);
    		} catch (Exception e) {
    			data.put("msg", e.getMessage());
    			data.put("code", -1);
    		}
    		logger.debug("list {}" , data);
    		return data;
    	}
    	
    	@GetMapping("get/{id}")
    	public Map<String, Object> get(@PathVariable("id")Integer id) {
    		Map<String, Object> data = new HashMap<String, Object>();
    		Map<String, Object> params = new HashMap<String, Object>();
    		params.put("id", id);
    		Map<String, Object> map;
    		try {
    			map = questionService.findByProperty(params);
    			data.put("msg", map);
    			data.put("code", 200);
    		} catch (Exception e) {
    			data.put("msg", e.getMessage());
    			data.put("code", -1);
    		}
    		logger.debug("get {}" , data);
    		return data;
    	}
    	
    	@PostMapping("put")
    	public Map<String, Object> put(@RequestBody Map<String, Object> map) {
    		Map<String, Object> data = new HashMap<String, Object>();
    		Integer i = questionService.saveOrUpdate(map);
    		logger.debug("put status {}" , i);
    		if(i == 1){
    			data.put("msg", "新增成功");
    			data.put("code", 200);
    		} else if (i == 2) {
    			data.put("msg", "修改成功");
    			data.put("code", 200);
    		} else {
    			data.put("msg", "数据处理失败");
    			data.put("code", -1);
    		}
    		logger.debug("put {}" , data);
    		return data;
    	}
    
    	@PostMapping("delete")
    	public Map<String, Object> delete(@RequestBody String ids) {
    		Map<String, Object> data = new HashMap<String, Object>();
    		Integer i = questionService.delete(ids);
    		logger.debug("delete {}" , i);
    		if(i > 0){
    			data.put("msg", "删除成功");
    			data.put("code", 200);
    		} else {
    			data.put("msg", "删除失败");
    			data.put("code", -1);
    		}
    		logger.debug("delete {}" , data);
    		return data;
    	}
    }
    启动类

    添加 @EnableCaching 注解,开启缓存功能

    4、接口测试

    1)List :http://localhost:8081/api/question/list

    连续发起两次list请求

    2018-04-04 16:23:40.807 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.mappper.QuestionMapper.findByPage [159] -| ==>  Preparing: select id, number, description from question 
    2018-04-04 16:23:40.808 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.mappper.QuestionMapper.findByPage [159] -| ==> Parameters: 
    2018-04-04 16:23:40.810 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.mappper.QuestionMapper.findByPage [159] -| <==      Total: 15
    2018-04-04 16:23:40.811 |-DEBUG [http-nio-8081-exec-2] com.phil.springboot.controller.QuestionController [43] -| list {code=200, msg=[{id=24, description=问题三不需要描述了, number=3}, {id=25, description=问题四描述, number=4}, {id=27, description=问题四描述, number=4}, {id=29, description=问题二描述, number=2}, {id=30, description=问题三描述, number=3}, {id=31, description=问题四描述, number=4}, {id=32, description=问题40描述, number=40}, {id=33, description=问题63描述, number=63}, {id=36, description=问题87描述, number=87}, {id=39, description=新问题, number=6}, {id=40, description=新问题, number=6}, {id=41, description=新问题, number=8}, {id=42, description=新问题, number=118}, {id=43, description=新问题, number=119}, {id=44, description=新问题, number=119}]}
    2018-04-04 16:23:44.887 |-DEBUG [http-nio-8081-exec-3] com.phil.springboot.controller.QuestionController [43] -| list {code=200, msg=[{id=24, description=问题三不需要描述了, number=3}, {id=25, description=问题四描述, number=4}, {id=27, description=问题四描述, number=4}, {id=29, description=问题二描述, number=2}, {id=30, description=问题三描述, number=3}, {id=31, description=问题四描述, number=4}, {id=32, description=问题40描述, number=40}, {id=33, description=问题63描述, number=63}, {id=36, description=问题87描述, number=87}, {id=39, description=新问题, number=6}, {id=40, description=新问题, number=6}, {id=41, description=新问题, number=8}, {id=42, description=新问题, number=118}, {id=43, description=新问题, number=119}, {id=44, description=新问题, number=119}]}

    2)http://localhost:8081/api/question/get/24

    连续发起两次get请求

    2018-04-04 16:25:52.984 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.mappper.QuestionMapper.findByProperty [159] -| ==>  Preparing: select id, number, description from question WHERE id = ? 
    2018-04-04 16:25:52.985 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.mappper.QuestionMapper.findByProperty [159] -| ==> Parameters: 24(Integer)
    2018-04-04 16:25:52.986 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.mappper.QuestionMapper.findByProperty [159] -| <==      Total: 1
    2018-04-04 16:25:52.987 |-DEBUG [http-nio-8081-exec-6] com.phil.springboot.controller.QuestionController [61] -| get {code=200, msg={id=24, description=问题三不需要描述了, number=3}}
    2018-04-04 16:25:55.310 |-DEBUG [http-nio-8081-exec-7] com.phil.springboot.controller.QuestionController [61] -| get {code=200, msg={id=24, description=问题三不需要描述了, number=3}}

    3)http://localhost:8081/api/question/put

    {
      "description": "新问题",
      "number": 150
    }
    发起保存
    2018-04-04 16:27:28.300 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.mappper.QuestionMapper.save [159] -| ==>  Preparing: insert into question (number,description) values (?, ?) 
    2018-04-04 16:27:28.301 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.mappper.QuestionMapper.save [159] -| ==> Parameters: 150.0(Double), 新问题(String)
    2018-04-04 16:27:28.302 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.mappper.QuestionMapper.save [159] -| <==    Updates: 1
    2018-04-04 16:27:28.306 |-DEBUG [http-nio-8081-exec-10] com.phil.springboot.controller.QuestionController [79] -| save {code=200, msg=新增成功}

    4)http://localhost:8081/api/question/put

     {
          "id": 24,
          "description": "问题三三三不需要描述了",
          "number": 333
    }

    发起update

    2018-04-04 16:55:26.791 |-DEBUG [http-nio-8081-exec-4] com.phil.springboot.mappper.QuestionMapper.update [159] -| ==>  Preparing: update question set number = ?, description = ? where id = ? 
    2018-04-04 16:55:26.791 |-DEBUG [http-nio-8081-exec-4] com.phil.springboot.mappper.QuestionMapper.update [159] -| ==> Parameters: 333.0(Double), 问题三三三不需要描述了(String), 24.0(Double)
    2018-04-04 16:55:26.793 |-DEBUG [http-nio-8081-exec-4] com.phil.springboot.mappper.QuestionMapper.update [159] -| <==    Updates: 1

    没有日志打印,但返回修改后的对象数据,说明缓存中的数据已经同步。(增加了一个新的Key)

    目前发现个bug,gson会把int long自动转换为double

    三、整合Redis 缓存

    1、添加依赖

    <!-- Redis 依赖 -->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    2、安装Redis并配置

    Windows 64 位 下载地址

    再加上以下脚本文件

    startup.bat

    redis-server.exe redis.windows.conf  

    service-install.bat

    redis-server.exe --service-install redis.windows.conf --loglevel verbose  
    

    uninstall-service.bat

    redis-server --service-uninstall  

    查询Redis所有Key的命令

    redis 127.0.0.1:6379> KEYS *

    application-local.properties添加redis的配置(查看RedisProperties.class源码,部分已经默认)

    #spring.redis.host=localhost
    spring.redis.password=
    #spring.redis.port=6379
    spring.redis.timeout=3000

    把原来ehcache.xml rename为ehcache.xml--,启动类的@EnableCaching去除就可以完全使用Redis缓存。

    3、创建配置类

    package com.phil.springboot.config;
    
    import java.time.Duration;
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.cache.RedisCacheWriter;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    
    @Configuration
    @EnableConfigurationProperties(RedisProperties.class)
    @EnableCaching
    public class RedisCacheManagerConfig {
    
    //	@Bean
    //	@ConditionalOnMissingBean(name = "redisTemplate")
    //	public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    //			throws UnknownHostException {
    //		RedisTemplate<?, ?> template = new RedisTemplate<>();
    //		template.setConnectionFactory(redisConnectionFactory);
    //		return template;
    //	}
    
    	@Bean
    //	public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
    	public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    		/**1.x写法 
    		RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
    		//cacheManager.setDefaultExpiration(3000); // =Sets the default expire time
    		 */
    		//2.x写法
    		//question信息缓存配置
    	    RedisCacheConfiguration questionCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)).disableCachingNullValues().prefixKeysWith("question");
    	    Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
    	    redisCacheConfigurationMap.put("question", questionCacheConfiguration);
    	    
    	    //初始化一个RedisCacheWriter
    	    RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    	    
    	    //设置CacheManager的值序列化方式为JdkSerializationRedisSerializer,但其实RedisCacheConfiguration默认就是使用StringRedisSerializer序列化key,JdkSerializationRedisSerializer序列化value,所以以下注释代码为默认实现
    	    //ClassLoader loader = this.getClass().getClassLoader();
    	    //JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(loader);
    	    //RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jdkSerializer);
    	    //RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);   
    	    RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
    	    //设置默认超过期时间是30秒
    	    defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
    	    //初始化RedisCacheManager
    	    RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig, redisCacheConfigurationMap);
    		return cacheManager;
    	}
    	
    }

    在项目中可以直接引用RedisTemplate 和 StringRedisTemplate 两个模板进行数据操作,或者自定义封装API

    4、接口测试

    略,同上(可通过查询Redis所有Key的命令发现缓存的Key变化)

    5、Redis测试

    package com.phil.springboot.redis;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class RedisTest {
    
    	@Autowired
    	private StringRedisTemplate stringRedisTemplate;
    
    	@Test
    	public void testSet() {
    		String key = "name";
    		String value = "zhangsan";
    		stringRedisTemplate.opsForValue().set(key, value);
    	}
    
    	@Test
    	public void testGet() {
    		String key = "name";
    		String value = stringRedisTemplate.opsForValue().get(key);
    		System.out.println(value);
    	}
    
    	@Test
    	public void testDelete() {
    		String key = "name";
    		stringRedisTemplate.delete(key);
    	}
    }

    四、整合Redis 集群

    在windows上搭建redis集群

    1、添加依赖

    在3.1基础之上,在pom.xml继续添加

    <dependency>
    	<groupId>redis.clients</groupId>
    	<artifactId>jedis</artifactId>
    </dependency>

    2、配置文件

    在application-local.properties添加以下配置

    spring.redis.pool.max-idle=8
    spring.redis.pool.max-wait=-1
    spring.redis.cluster.nodes=127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384
    spring.redis.commandTimeout=5000

    3、配置类

    package com.phil.springboot.config;
    
    import java.time.Duration;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.cache.RedisCacheWriter;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    
    import redis.clients.jedis.HostAndPort;
    import redis.clients.jedis.JedisCluster;
    import redis.clients.jedis.JedisPoolConfig;
    
    @Configuration
    @EnableConfigurationProperties(RedisProperties.class)
    @EnableCaching
    public class RedisCacheManagerConfig {
    
    	@Value("${spring.redis.cluster.nodes}")
    	private String clusterNodes;
    
    	@Value("${spring.redis.pool.max-idle}")
    	private int maxIdle;
    
    	@Value("${spring.redis.pool.max-wait}")
    	private int maxWait;
    
    	@Value("${spring.redis.commandTimeout}")
    	private int commandTimeout;
    
    	// @Bean
    	// @ConditionalOnMissingBean(name = "redisTemplate")
    	// public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory
    	// redisConnectionFactory)
    	// throws UnknownHostException {
    	// RedisTemplate<?, ?> template = new RedisTemplate<>();
    	// template.setConnectionFactory(redisConnectionFactory);
    	// return template;
    	// }
    
    	@Bean
    	public JedisCluster getJedisCluster() {
    		String[] c_nodes = clusterNodes.split(",");
    		Set<HostAndPort> nodes = new HashSet<>();
    		// 分割集群节点
    		for (String node : c_nodes) {
    			String[] h = node.split(":");
    			nodes.add(new HostAndPort(h[0].trim(), Integer.parseInt(h[1].trim())));
    			System.err.println("h[1] = " + h[1].trim());
    		}
    		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    		jedisPoolConfig.setMaxIdle(maxIdle); //默认是8
    		jedisPoolConfig.setMaxWaitMillis(maxWait);//默认是-1
    		return new JedisCluster(nodes, commandTimeout, jedisPoolConfig);
    	}
    
    	@Bean
    	// public CacheManager cacheManager(RedisTemplate<?, ?> redisTemplate) {
    	public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    		/**
    		 * 1.x写法 RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
    		 * //cacheManager.setDefaultExpiration(3000); // =Sets the default expire time
    		 */
    		// 2.x写法
    		// question信息缓存配置
    		// RedisClusterConnection redisClusterConnection = new
    		// JedisClusterConnection(getJedisCluster());
    		RedisCacheConfiguration questionCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
    				.entryTtl(Duration.ofMinutes(30)).disableCachingNullValues().prefixKeysWith("question");
    		Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
    		redisCacheConfigurationMap.put("question", questionCacheConfiguration);
    		System.err.println("question 缓存 启动");
    		// 初始化一个RedisCacheWriter
    		RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    		// 设置CacheManager的值序列化方式为JdkSerializationRedisSerializer,但其实RedisCacheConfiguration默认就是使用StringRedisSerializer序列化key,JdkSerializationRedisSerializer序列化value,所以以下注释代码为默认实现
    		// ClassLoader loader = this.getClass().getClassLoader();
    		// JdkSerializationRedisSerializer jdkSerializer = new
    		// JdkSerializationRedisSerializer(loader);
    		// RedisSerializationContext.SerializationPair<Object> pair =
    		// RedisSerializationContext.SerializationPair.fromSerializer(jdkSerializer);
    		// RedisCacheConfiguration
    		// defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
    		RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
    		// 设置默认超过期时间是30秒
    		defaultCacheConfig.entryTtl(Duration.ofSeconds(30));
    		// 初始化RedisCacheManager
    		RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig,
    				redisCacheConfigurationMap);
    		return cacheManager;
    	}
    }

    4、接口测试



    本文为Phil Jing原创文章,未经博主允许不得转载,如有问题请直接回复或者加群。
  • 相关阅读:
    nginx upstream permission denied错误解决
    基于Mariadb 10.6.4在CentOS 7环境下配置Galera Cluster集群
    K8s 开始
    RTSP H264/HEVC 流 Wasm 播放
    Netty编码示例(RPC & WbeSocket & Tomcat)
    Netty异步任务调度与异步线程池
    Netty编解码器&TCP粘包拆包
    Netty核心模块组件
    Neety编码示例(群聊系统&⼼跳检测&WebSocket⻓连接)
    Netty高性能架构设计
  • 原文地址:https://www.cnblogs.com/phil_jing/p/15615870.html
Copyright © 2011-2022 走看看