Spring Boot集成mysql与Redis

原文:Spring Boot集成mysql与Redis

在项目搭建初期,我们便集成了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可用的操作,并且以重载方法取代泛型声明,可读性强。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值