在项目搭建初期,我们便集成了mysql数据库,但是在现代的分布式架构中,往往还需要一种更高访问性能的键值数据库Redis。mysql结合redis解决了大多数软件系统的痛点,当然还有一些特殊的系统,需要保存大量的归档资料而用到分布式文件系统(比如GFS),需要同时满足数据强一致与高性能而自研了分布式数据库(比如OceanBase)。
本篇文章介绍我自己在Spring boot项目中,集成mysql与redis的过程,一些有关连接池的概念,遇到的一些坑,以及自己的想法。
tomcat连接池
tomcat提供了HTTP协议的web服务,如果tomcat的请求响应出现延迟,则必然会造成接口阻塞。创建连接是很消耗资源的,连接池能够很好地解决创建连接的问题,当请求结束时,不是真的物理上的关闭连接,而是将连接缓存起来,等待新的请求从而复用旧的连接。连接池这样的方式是对资源的一种抽象,客户端只要关心如何使用一个连接,而非如何获取一个连接,连接池返回的连接究竟是单例还是原型,取决于具体的实现。
tomcat连接池可选择的配置如下,每个配置条目将会覆盖tomcat对应的默认配置项:
server.tomcat.max-threads=211
server.tomcat.max-connections=10000
server.tomcat.accept-count=1000
max-threads 最大线程数。每一次HTTP请求到达Web服务,tomcat都会指派一个线程来处理该请求。最大线程数决定了Web服务容器可以同时处理多少个请求。maxThreads默认200。最大支持线程数受限于操作系统/CPU,过多的线程会导致系统频繁的上下文切换,会带来过多的内存消耗(JVM中默认情况下在创建新线程时会分配大小为1M的线程栈)。
max-connections 最大连接数。这个参数是指在同一时间,tomcat能够接受的最大连接数。对于旧版本Java的阻塞式IO,默认值等于最大线程数。对于Java 新的NIO模式,max-connections默认值是10000。如果设置为-1,则禁用max-connections功能,表示不限制tomcat容器的连接数。
accept-count 最大等待数。当所有的请求处理线程都在使用时,新的请求进入等待队列,当队列超过最大等待数时,任何的连接请求都将被拒绝。accept-count的默认值为100。max-connections和accept-count的关系为:当连接数达到最大值后,tomcat会继续接收连接,但不会超过accept-count的值。
tomcat的NIO模式“读取socket并交给Worker中的线程”这个过程是非阻塞的,当socket在等待下一个请求或等待释放时,并不会占用工作线程,因此Tomcat可以同时处理的socket数目远大于最大线程数,并发性能大大提高。
注意,测试并发效果时,测试工具不要选择浏览器,因为浏览器可能会做一些多余的动作影响测试结果,比如谷歌浏览器会对相同的请求参数队列化。
mysql与连接池
pom.xml添加mysql依赖之后,并在application.properties中配置了数据库源,在代码中就可以如下方式连接db
@Autowired
private JdbcTemplate db;
/**
* 插入讲话
* @param talkContent 讲话内容
*/
public boolean insertTalk(TalkContent talkContent){
return db.update("insert into talk_content(user_id,user_name,talk_content,talk_time) " +
"values(?, ?, ?, now())",
talkContent.getUserid(),
talkContent.getUsername(),
talkContent.getContent()
) > 0;
}
Spring boot2.x默认使用HikariCP作为数据库连接池,在未配置的情况下,连接池以无感知的方式使用缺省参数初始化,可以在application.properties中显式配置:
# hikari
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.maximum-pool-size=2
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
测试连接池效果
编写一个慢sql,以造成在数据库层面的延迟:
@Repository
public class TestRepository {
@Autowired
private JdbcTemplate db;
public void slowQuery(){
String sql = "select sleep(5)";
db.query(sql, (resultSet, i) -> 0);
}
}
编写测试接口:
@GetMapping("/test")
public String test() {
testRepository.slowQuery();
return "hello";
}
编写测试用例:(按配置,最大连接空闲是2,连接数是2)
public static void main(String[] args) {
long t = System.currentTimeMillis();
for (int i = 0; i < 5; i++){
new Thread(() -> {
String result = HttpRequest.sendGet("http://localhost:8080/test", "");
System.out.println(result + "-" + (System.currentTimeMillis() - t));
}).start();
}
}
运行输出:
hello-5202
hello-5202
hello-10309
hello-10309
hello-15422
并发请求到了数据库层面,变为了2对2对地串行化的请求,因为我们设置的最大连接数是2,证明连接池确实有效。(如果没有连接池,每个请求都会创建连接,会慢,但不会等待)
集成Redis
安装redis
因为我们已经有了公网访问的服务器,所以直接在服务器上安装即可,不需要本地安装。本地只要有IDEA,基本上就可以搞定一切了,Java开发环境的搭建,如今也像C#一样轻松。
下载地址:https://redis.io/download
类似于数据库的安装一样,步骤依次是:
- wget命令下载压缩文件
- tar命令将压缩文件解压,解压后放在期望的目录,最好重命名保持可读性
- 进入redis目录,输入make命令执行编译
- 安装 make install
- 启动 ./redis-server & ./redis.conf
设置认证
当redis的端口6379设置了公网访问权限,就可以在其他机器上访问redis了。但是这样不安全,redis默认开启了保护模式,只有取消保护模式,或者设置密码认证,网络请求才可以真正使用redis。
以下代码使用redis-cli程序,设置了密码并测试了redis的使用。
[root@sunday src]# ./redis-cli
127.0.0.1:6379> config set requirepass oO00OooO00Oo
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "oO00OooO00Oo"
127.0.0.1:6379> SET test 123
OK
127.0.0.1:6379> GET test
"123"
127.0.0.1:6379> Keys te*
1) "test"
127.0.0.1:6379>
这样一个单机的无主从的redis就部署好了。集群保证了redis读写性能的水平扩展,主从保证了redis读性能的水平扩展以及分区容忍性,目前还未考虑这么多。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置redis
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码
spring.redis.password=oO00OooO00Oo
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
这样代码中就可以直接使用redisTemplate了。
@Autowired
RedisTemplate<String, String> redisTemplate;
可以不用显示配置连接池的各项参数,因为Spring boot基于“约定即配置”的思想,也就是说,默认配置在大多数情况下会是理想的配置,这也是“最少惊讶原则”的体现,
Redis的使用
关于Lettuce
Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO,5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API。Lettuce的连接实例可以在多个线程间并发访问,是线程安全的。
Spring-date-redis中默认使用Lettuce作为底层驱动。可以直接使用Lettuce作为连接工具,也可以使用Spring对Lettuce的封装LettuceConnectionFactory来获取连接。我们通常使用RedisTemplate,一个对RedisConnectionFactory的封装。
测试LettuceConnectionFactory
LettuceConnectionFactory是RedisConnectionFactory接口的实例,如果使用Jedis,则为JedisConnectionFactory。RedisConnectionFactory是一个redis连接资源的工厂,封装了连接的获取方法,可以视作连接池。
以下代码测试RedisConnectionFactory的实现:
@GetMapping("/test")
public String test() {
//从connectionFactory获取redis连接
RedisConnection connection = connectionFactory.getConnection();
//连接的具体实现类
System.out.println(connection.getClass().getName());
//底层连接的具体实现类
System.out.println(connection.getNativeConnection().getClass().getName());
//多次创建连接,底层使用的是相同的连接
RedisConnection connection2 = connectionFactory.getConnection();
System.out.println(connection.getNativeConnection() == connection2.getNativeConnection());
return "hello";
}
输出:
org.springframework.data.redis.connection.lettuce.LettuceConnection
io.lettuce.core.RedisAsyncCommandsImpl
true
这个工厂在每次调用getConnection()时创建一个新的LettuceConnection。默认情况下,多个Lettuce连接共享一个线程安全的本地连接。共享的本地连接不会被关闭,如果shareNativeConnection为true,则连接池将只对阻塞操作和事务操作有效(意思是大多数情况用不到连接池),这些操作不应该共享连接。
可以直接基于RedisConnection上操作redis,获取连接-获取命令目录-执行命令
@GetMapping("/test")
public String test(String v) {
RedisConnection connection = connectionFactory.getConnection();
connection.stringCommands().set("test".getBytes(StandardCharsets.UTF_8), v.getBytes(StandardCharsets.UTF_8));
redisTemplate.opsForValue().set("test", v);
return "hello";
}
@GetMapping("/test2")
public String test2() {
RedisConnection connection = connectionFactory.getConnection();
String v = String.valueOf(connection.stringCommands().get("test".getBytes(StandardCharsets.UTF_8)));
return v;
}
使用RedisTemplate
redisTemplate是对连接资源的封装,就像jdbcTemplete是对datasource的封装一样,template代码简洁更易于使用(JDBC操作数据库查询需要一屏幕的代码)。
@Autowired
RedisTemplate<String, String> redisTemplate;
@GetMapping("/test")
public String test(String v) {
redisTemplate.opsForValue().set("test", v);
return "hello";
}
@GetMapping("/test2")
public String test2() {
return redisTemplate.opsForValue().get("test");
}
Templete再封装
有时Templete仍然不满足需求,我们可以对Templete进行封装,主要有以下几种情况或方法:
封装以支持多数据源
如果一个项目想要连接不同的数据库(一般情况不应该这么做,可能是架构有问题),并在依赖注入时要指名注入特定的数据库的模板,可以为每个Templete创建不同的bean。
@Configuration
public class DataConfig {
//配置数据源1号
@Bean(name = "primaryDataSource")
@Qualifier("primaryDataSource")
public DataSource primaryDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("");
dataSource.setUsername("");
dataSource.setPassword("");
//其他配置...
return dataSource;
}
//配置数据源2号
@Bean(name = "secondaryDataSource")
@Qualifier("secondaryDataSource")
public DataSource secondaryDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("");
dataSource.setUsername("");
dataSource.setPassword("");
//其他配置...
return dataSource;
}
//配置数据模板1号
@Bean(name = "primaryJdbcTemplate")
public JdbcTemplate primaryJdbcTemplate(
@Qualifier("primaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
//配置数据模板2号
@Bean(name = "secondaryJdbcTemplate")
public JdbcTemplate secondaryJdbcTemplate(
@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
可以将数据源写在配置文件中,写在代码中更方便做一些加密,动态配置等维护。
封装以修改Templete
比如在redis操作对象缓存时,可以把对象序列化后的字节数组存储到redis中,获取时再反序列化生成对象。这个操作发生在Templete内部,不需要用户手动处理。
@Configuration
public class RedisConfig {
//RedisConnectionFactory是已有的bean,会被级联注入
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//指定序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
Spring的@Component注解支持以类方式声明一个组件,@Bean以方法形式声明组件,很明显@Bean更为通用,可以为final类或者为具体的使用环境创建不同的组件实例。
封装以扩展Templete
假如不喜欢Templete的操作方法,想自定义CRUD方法名,希望加入日志和异常捕获,或其他的什么功能,可以创建新的类,以组合模式使用Templete,重新定义方法。比如:
@Component
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void set(String key, String value, long second){
redisTemplate.opsForValue().set(key, value, second, TimeUnit.SECONDS);
}
public Object read(String key){
return redisTemplate.opsForValue().get(key);
}
public void delete(String key){
redisTemplate.delete(key);
}
}
这种方式可能更加舒服,限制了对redis可用的操作,并且以重载方法取代泛型声明,可读性强。