还在为验证参数存在而多次查询数据库而烦恼么?不妨看看这个思路

1.背景

        最近在做一个基于大数据量的应用,如果数据存在就不写入,反之写入数据。逻辑上来看其实比较简单,无非就是每次插入的时候,查询数据库这条数据是否存在,然后在根据是否存在处理后续的逻辑。但是也具有一个比较大的挑战,就是每次请求来了我都要去查询数据库,数据库负载比较高,而且数据量大的情况下效率也随之降低了。有人说这可以通过数据库建立索引提升查询效率,虽说是一种方案,但也会存在不利的一面,建立索引会加大额外的存储空间,还会影响数据插入效率,是对立的,那有没有一种更好的方法在不动底层数据库架构的情况下进行优化呢?

2.问题分析

        我们的目标是在尽量以最小的影响现有状态的情况下,提升程序的查询效率。肯定有人说那就直接上缓存,我觉得吧,这是对的但也不对,主要还是要看如何使用缓存,如果缓存使用方式正确确实会大大提高效率,反之亦然。这里推荐使用布隆过滤器,他可以结合Guava或者redis进行实现,确实可以较大的提升程序效率。

        布隆过滤器:是一个在大数据中常用的概率算法,其应用场景为判断一个元素是否已经存在数据库中了,这是一个有效的减少数据库IO的方法。如果要判断数据库是否存在某一个数据,一个一个比较是可行的办法,但是这样比较慢,特别是当数据库里面的数据很多的时候,甚至是大数据的时候更可怕,一个一个比较效率会非常低下。如果,应用不要求百分之百的正确性允许一定程度的误报(万分之一,或十万分之一)(这个误判率是可以设置的,Guava工具类已经实现布隆过滤器)误判率越小所需时间越长(性能差),那么布隆过滤器用于提升效率正合适。

3.布隆过滤器基本原理

        布隆过滤器的基本思想: 利用多个不同的哈希函数将一个元素映射到多个不同的位(或桶)上,并将这些位设置为1. 当查询一个元素是否在布隆过滤器中时,将这个元素进行哈希操作,得到多个哈希值,并检查这些哈希值对应的位是否都为1。如果有任意一位为0,则可以确定该元素不在布隆过滤器中;如果多有部位都为1,则不能确定该元素一定在布隆过滤器中。

        为什么会出现误报率呢?我认为是在进行哈希算法的时候,可能存在哈希冲突,将不同的元素被映射到了相同的位上。本来是不存在的,但是哈希映射出来是存在的,就产生了误报。

4.问题解决办法

我这里按照Guava实现一个布隆过滤器

第一步:导入需要的Guava包

<dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>  
    <version>31.0.1-jre</version> <!-- 使用你需要的版本 -->  
</dependency>

第二步:使用Guava布隆过滤器代码示例

package com.test.test;
 
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
/**
 * @author dp
 * @date 2024/9/13
 * 布隆过滤器误判率测试案例
 */
public class BloomFilterTest {
 
    //日志,用于执行记录时间
    private static final Log log = LogFactory.getLog(BloomFilterTest.class);
    
    //模拟布隆过滤器预计存储数据的大小,这里设置为100万
    private static int size = 1000000;
    //设置 期望的误判率
    private static double error = 0.01;
    /**
     *  创建布隆过滤器
     *  Integer:表示过滤器存储的是整数类型的数据
     *  Funnels.integerFunnel():表示过滤器只对整数类型进行判断
     */
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, error);
    //设置样本数据为100万
    private static int total = 1000000;
 
    public static void main(String[] args) {
        //1、在布隆过滤器中,插入样本数据
        for (int i = 0; i < total; i++) {
            //使用put()插入即可
            bloomFilter.put(i);
        }
        //2、定义一个误判数据量的计量值
        int count = 0;
        //3、误判测试前,打印下一句话,用于测试误判测试时间
        log.warn("误判开始");
        /**
         * 4、添加10万个布隆过滤器中不存在的数据,用于测试误判率
         *
         *    这里直接在total的基础上,再加10万的数据
         */
        for (int i = total; i < total+100000; i++) {
            //mightContain()判断数据是否存在于布隆过滤器中
            if (bloomFilter.mightContain(i)) {
                count++;
                //System.out.println("数据:" + i + "误判了");
            }
        }
        log.warn("误判结束");
        //打印误判数据及耗时
        System.out.println("总共误判数为:" + count);
    }
}
  • 在12ms内执行完成
  • 误判数为:947(测试总数为10万)
  • 真实误判率为:947 ÷ 10万 = 0.00947 ≈ 0.01 ,符合预期

总结:设置的误判率是能直接影响布隆过滤器最终的误判率的,误判率越低,耗时越多

问题:我们是不是把误判率设置为无限小,那么就不存在误判了呢?

这要双面来看,如果误判率越小,那么程序设置的Hash桶就会越大,一方面会占用更大的空间,另一方面在进行hash比对的时候也会消耗更多的时间。

5.反过来想一下

        是否使用布隆过滤器?以及使用布隆过滤器时设置误判率为多大?我的理解还是得看业务,如果业务不允许 哪怕一丁点的错误,我觉得损耗性能是值得的,比如:财务中,涉及到金额数据,我们不允许哪怕一分钱对不上,此时我们就不能使用这个布隆过滤器的方法。但是在另外一些场景下,即使存在误判,也不影响数据最终的结果,或者业务本身就可以允许一定的偏差,我觉得还是可以考虑使用布隆过滤器。总之,一切的技术都是为业务服务的,业务才是主导技术的应用,不能为了体现技术的高大尚而不考虑业务的场景。

<think>嗯,用户问的是这个方法执行一次会访问几次数据库。首先,我需要回顾之前的回答,看看之前提供的Java代码是怎样的。之前的代码里,用户通过JDBC执行了一个SQL查询,从scores表中根据学生ID查询kname和co字段,然后把这些结果放到一个Map里返回。 用户现在想知道这个方法执行一次会访问数据库多少次。我需要仔细看一下代码的结构。代码里有一个PreparedStatement,执行了一次查询,也就是执行了ps.executeQuery(),这应该只会向数据库发送一次请求,获取所有符合条件的记录。然后通过while循环遍历ResultSet,逐行读取数据,但这都是在已经获取到的结果集里操作,并不会再次访问数据库。所以在这个方法中,不管查询到多少科目成绩,都是通过一次数据库查询完成的,因此访问数据库的次数是一次。 但是,我需要确认是否有其他可能的数据库访问。比如,数据库连接是否在每次调用方法时都重新建立,但连接本身并不算一次查询,只是建立连接的开销。真正的查询次数还是看执行的SQL语句次数。在这个例子中,每次调用getScoresMap方法时,会创建一个新的连接,执行一次查询,然后关闭连接。因此,每次方法调用确实只执行一次SQL查询,也就是访问数据库一次。 用户可能关心的是性能问题,尤其是如果这个方法被频繁调用,可能会导致多次数据库连接的开销。这时候或许可以建议使用连接池来管理连接,减少每次建立连接的时间消耗。但就访问次数而言,每次方法调用确实只访问一次数据库。 另外,用户可能没有意识到,虽然访问次数是一次,但如果查询的结果集很大,可能会影响网络传输和内存使用,但这是另一个层面的问题。不过用户的问题明确是关于访问次数,所以只需要确认是每次方法调用一次数据库访问即可。 总结下来,这个方法的每次执行只会访问数据库一次,无论返回多少条记录,因为所有记录都是通过一次查询获取的。</think>--- ### 直接回答: **这个方法执行一次只会访问 1 次数据库**。 --- ### 关键分析: 1. **单次查询**: SQL 语句 `SELECT kname, co FROM scores WHERE sid = ?` 是一次性获取该学生的所有科目成绩的,代码中仅通过 `ps.executeQuery()` 执行了一次数据库查询。 2. **结果集遍历**: `while (rs.next())` 循环的作用是逐行读取**已从数据库返回的结果集**(数据已在内存中),不会触发新的数据库访问。 3. **资源开销**: 每次调用方法时会新建连接(实际生产环境建议用连接池优化),但连接本身不计算为“查询次数”。 --- ### 性能对比场景: | 实现方式 | 数据库访问次数 | 适用场景 | |-------------------------|---------------------|-----------------------| | 当前方案(单次查询) | 1 次 | 常规场景(推荐) | | 逐科目查询(错误示例) | N 次(科目数量) | 绝对禁止(性能灾难) | --- ### 如何验证? 在代码中添加日志观察 SQL 执行: ```java // 在 executeQuery() 前打印日志 System.out.println("Executing SQL: " + sql); ``` 你会看到:**无论学生有多少科目,日志只会输出一次 SQL 语句**。 --- ### 进一步优化建议: 1. **连接池**:用 HikariCP 等工具复用数据库连接。 2. **批量查询**:如需查多个学生,可通过 `WHERE sid IN (?,?)` 合并查询。 3. **缓存**:高频访问时可用 Redis 缓存成绩数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Scalzdp

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值