20230615学习记录

1. 20230615学习记录

目标

  • 缓存更新的实现
  • redis的缓存问题
  • 布隆过滤器的使用

1.1. 缓存更新

昨天分析到数据库和缓存一致性问题,需要一种方案当数据库数据产生增删改时来及时更新缓存

canal基于订阅binlog的同步机制,可以同步主服务器修改操作,可以利用其进行更新缓存。

1.1.1. 解读官方canal可运行代码

public class SimpleCanalClientExample {
public static void main(String args[]) {
    // 创建链接,此处AddressUtils.getHostIp()改为自己canal服务的ip
    CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
                                                                                        11111), "example", "", "");
    int batchSize = 1000;
    int emptyCount = 0;
    try {
        connector.connect();//和canal服务建立连接
        connector.subscribe(".*\\..*");
        connector.rollback();
        int totalEmptyCount = 120;
        while (emptyCount < totalEmptyCount) {
            Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
            long batchId = message.getId();
            int size = message.getEntries().size();
            if (batchId == -1 || size == 0) {
                emptyCount++;
                System.out.println("empty count : " + emptyCount);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            } else {
                emptyCount = 0;
                // System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
                printEntry(message.getEntries());
            }

            connector.ack(batchId); // 提交确认
            // connector.rollback(batchId); // 处理失败, 回滚数据
        }

        System.out.println("empty too many times, exit");
    } finally {
        connector.disconnect();
    }
}

private static void printEntry(List<Entry> entrys) {
    for (Entry entry : entrys) {
        if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
            continue;
        }

        RowChange rowChage = null;
        try {
            rowChage = RowChange.parseFrom(entry.getStoreValue());
        } catch (Exception e) {
            throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                                       e);
        }

        EventType eventType = rowChage.getEventType();
        System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",
                                         entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                                         entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                                         eventType));

        for (RowData rowData : rowChage.getRowDatasList()) {
            if (eventType == EventType.DELETE) {
                printColumn(rowData.getBeforeColumnsList());
            } else if (eventType == EventType.INSERT) {
                printColumn(rowData.getAfterColumnsList());
            } else {
                System.out.println("-------&gt; before");
                printColumn(rowData.getBeforeColumnsList());
                System.out.println("-------&gt; after");
                printColumn(rowData.getAfterColumnsList());
            }
        }
    }
}

private static void printColumn(List<Column> columns) {
    for (Column column : columns) {
        System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
    }
}

}
  1. AddressUtils.getHostIp(),11111 ;redis连接信息

CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
                                                                                        11111), "example", "", "");
  1. 建立canal服务

connector.connect();//和canal服务建立连接
connector.subscribe(".*\\..*");//订阅数据库表,全部表
connector.rollback();//回滚到未进行ack的地方
  1. int totalEmptyCount 用于控制循环体循环次数

    while (emptyCount < totalEmptyCount) {} 循环体内是main函数的主要部分,循环读取同步状态

    if (batchId == -1 || size == 0) {//没有增删改数据}else{//有增删改数据} 判断是否有增删改数据,没有线程休眠会儿,可以用于控制更新缓存的时机

int totalEmptyCount = 120; //根据循环条件,可以得出,此变量用于控制循环体循环次数
while (emptyCount < totalEmptyCount) {
    Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
    long batchId = message.getId();
    int size = message.getEntries().size();
    if (batchId == -1 || size == 0) {
        //没有增删改数据
        emptyCount++;
        System.out.println("empty count : " + emptyCount);
        try {
            //线程休眠1s
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    } else {
        //有增删改数据
        emptyCount = 0;
        printEntry(message.getEntries());
    }

    connector.ack(batchId); // 提交确认
    // connector.rollback(batchId); // 处理失败, 回滚数据
}
  1. printEntry(message.getEntries()); 修改后打印数据,都在该方法中进行。

    其中在for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {} 内打印修改前和修改后的数据

for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
    if (eventType == CanalEntry.EventType.DELETE) {
        //删除
        printColumn(rowData.getBeforeColumnsList());
    } else if (eventType == CanalEntry.EventType.INSERT) {
        printColumn(rowData.getAfterColumnsList());
    } else {
        //监听到的操作类型为修改时,打印数据
        System.out.println("-------&gt; before");
        printColumn(rowData.getBeforeColumnsList()); //用于打印数据  由结果可以rowData
        System.out.println("-------&gt; after");
        printColumn(rowData.getAfterColumnsList());
    }
}
  1. rowData.getBeforeColumnsList() 得到修改前的数据

    rowData.getAfterColumnsList() 得到修改后的数据

    方法返回的是CanalEntry.Column类型的集合

    一个CanalEntry.Column包含了被修改的行数据的字段名(getName()),该字段数据值(getValue()),该字段是否修改(getUpdated())

private void printColumn(List<CanalEntry.Column> columns) {
    for (CanalEntry.Column column : columns) {
        System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
    }
}
  1. 修改,增加,删除操作的获取结果

修改

image-20230615121248119

添加

image-20230615115530294

删除

image-20230615115537901

1.1.2. 应用项目的修改

  1. 新建类,作更新缓存操作

    将可运行代码复制粘贴,main方法更改为自定义的方法

    image-20230615135026258

  2. 需要随着项目一起启动,且一直获取同步状态

    启动类实现CommandLineRunner,重写public void run(String... args)方法,在方法中调用缓存更新方法

@SpringBootApplication
@MapperScan("com.wnhz.bm.dao")
public class App implements CommandLineRunner {
    @Autowired
    private ReidsAutoUp reidsAutoUp;

    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }

    @Override
    public void run(String... args) throws Exception {
        reidsAutoUp.run();
    }
}

循环体作死循环

boolean isContinue = true; //一致循环监听
while (isContinue) {
    Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
    long batchId = message.getId();
    int size = message.getEntries().size();
    if (batchId == -1 || size == 0) {
        emptyCount++;
        System.out.println("empty count : " + emptyCount);
        try {

            Thread.sleep(500); //500ms
        } catch (InterruptedException e) {
        }
    } else {
        emptyCount = 0;
        printEntry(message.getEntries());
    }

    connector.ack(batchId); // 提交确认
    // connector.rollback(batchId); // 处理失败, 回滚数据
}
  1. 获取到修改前,修改后数据,用以更新缓存
for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
    if (eventType == CanalEntry.EventType.DELETE) {
        printColumn(rowData.getBeforeColumnsList());
    } else if (eventType == CanalEntry.EventType.INSERT) {
        printColumn(rowData.getAfterColumnsList());
    } else {
        //监听到的操作类型为修改时,打印数据
        System.out.println("-------&gt; before");
        printColumn(rowData.getBeforeColumnsList()); //用于打印数据  由结果可以rowData
        System.out.println("-------&gt; after");
        printColumn(rowData.getAfterColumnsList());
        updateRedis(rowData.getBeforeColumnsList(),rowData.getAfterColumnsList());
    }
}

/**
* 更新缓存操作
* @param beforColumns
* @param afterColumns
*/
private void updateRedis(List<CanalEntry.Column> beforColumns,List<CanalEntry.Column> afterColumns){
    String beforUsername = "";
    for (CanalEntry.Column column : beforColumns) {
        if("username".equals(column.getName())){
            beforUsername = column.getValue();
            break;
        }
    }

    for (CanalEntry.Column column : afterColumns) {
        if("username".equals(column.getName()) && column.getUpdated()){
            redisTemplate.opsForSet().remove("username",beforUsername);
            redisTemplate.opsForSet().add("username",column.getValue());
        }
    }
}

1.2. redis缓存雪崩

大量请求访问redis时,刚好缓存中数据大批量到过期时间,大量的请求到去请求数据库,导致数据库压力陡增,可能导致宕机。

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。

(热点数据:请求频繁的数据)

1.3. redis缓存击穿

大量请求访问redis的同一数据,刚好该数据到了过期时间,大量的请求到去请求数据库,导致数据库压力陡增,可能导致宕机。

解决方案:

  1. 设置热点数据永远不过期。(不是真正的永不过期)
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。

1.4. redis缓存穿透

当缓存和数据库没有数据时,当查询数据缓存不存在时,会继续查询数据量,当大量请求访问不存在的数据时,每次访问都要查询数据库,访问不存在数据请求的并发量一大数据库容易宕机;该问题容易被有心之人进行攻击。

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置较短的过期时间
  3. 使用布隆过滤器判断

1.4.1. 布隆过滤器

简介:

布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率查询时间远远超过一般的算法,缺点是有一定的误识别率和删除困难。

布隆过滤器的原理:当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能在

用自己理解的话解释一下:

  • 首先引出BitMap数据结构

    设想这样一个情况:我们想存储数据,并且有需求要判断某个值是否存在,我们会使用list、map存储;假设大量的数据要存到集合中会怎样,假设一个请求就要存储一亿个int类型的整数,就需要380多MB(4byte × 10 ^8),高并发情况下,需要很大的内存存储集合数据,不实际。

    BitMap结构是指,以位数来代表一个数字大小,比如1B,第7位被置了1,代表存了一个数字1,假设要存一个数字10,则在第十位置。

    那么1B就能用来存储0-7这八个数,一个int 4B 32位,可以存储32个值,存储大小直接减少到1/32,大大减少

    BitMap结构能存储的数量庞大,但是用位数代表值的做法有个弊端,要开辟的存储空间与最大数的数值成正比,假如要存储1万个的数据,但是数据的最大值是100万,意味着要101万bit空间来存储这1万个数据。怎么办?

    用算法,将数据面值减小

  • 布隆过滤器

    存储的结构利用的BitMap,对一个数据进行了K个hash函数计算,得到K个值,这k个值表示一个数,在这k个值的位数上置1、这样也可以保证装下面值大的数据,而且想判断一个数据是否存在,只需要进行一下hash计算,看对应结果的位数上是否置1,只要有一个0就表示这个数据存在;

    怎么理解布隆过滤器对于判断为存在的结果误识别概率(通过布隆过滤器计算判断不存在的结果肯定正确,但是存在的结果不一定正确)

    上述所说,hash得到的结果肯定能够对应上数据,所以只要对应结果的位数上有一个0,该hash结果对应的数据不存在是100%的;但是数据变多的情况下,置1的位数会变多,可能不同的位数组成了一个数据的hash结果,但是这个数据其实并不存在

    假设x的hash结果为 1、3、7,那么在第一位,第三位、第七位置一;查找x是否存在是,hash得到1、3、7,检查第一位,第三位、第七位是否都置1,其中1个为0,就可以判定x不存在。

    假设x的hash结果为1、3、7,y的hash结果为2、4、8,x和y都存在了,此时要找一个c,它的hash结果为1、2、7,它实际没有存入,但是x和y的hash结果包含了c的hash结果,导致查找的时候误判c存在了。

参考:https://developer.huawei.com/consumer/cn/forum/topic/0201117114074403582

1.4.2. 布隆过滤器应用

还是hutool提供了工具类

1.4.2.1. BitMapBloomFilter的使用
// 初始化
BitMapBloomFilter filter = new BitMapBloomFilter(10);
filter.add("123");
filter.add("abc");
filter.add("ddd");

// 查找  返回类型boolean
filter.contains("abc")
1.4.2.2. 项目中应用

查询不存在的数据时,先用布隆过滤器过滤查询不存在用户的请求,防止缓存穿透;此处布隆过滤器充当类似白名单功能,布隆过滤器中有的才放行

  • 缓存预热时,同时也预热布隆过滤器;
  • 访问先查布隆过滤器,没有,直接返回;有,继续查询缓存;
  • 缓存有,取出,返回数据;
  • 缓存没有,继续查询数据库,查询出来后回写更新缓存,返回数据
1.4.2.3. 自定义布隆过滤器工具类
public class WhiteListUtil {
    private static BitMapBloomFilter bf = new BitMapBloomFilter(10);

    public static void add(String str){
        bf.add(str);
    }

    public static boolean isExist(String str){
        return bf.contains(str);
    }
}
1.4.2.4. 布隆过滤器数据装载

在预热缓存的时候同时装载

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
    log.debug("缓存预热开始");
    //缓存预热
    //先清空缓存
    redisTemplate.delete("username");

    //查询数据库,将用户名全部存入key="username"的set类型value中
    List<User> allUsers = userService.findAll();
    allUsers.forEach(u -> {
        redisTemplate.opsForSet().add("username",u.getUsername());
        //布隆过滤器装载
        WhiteListUtil.add(u.getUsername());
    });
    log.debug("缓存预热结束");
}
1.4.2.5. controller层,先查布隆过滤器
@GetMapping("/findByUsername")
public RespData findByUsername(String username){
    if(WhiteListUtil.isExist(username)){
        //布隆过滤器中存在,继续查询缓存
        log.debug("布隆过滤器中存在,继续查询缓存");
        String result = userService.findByUsername(username);
        return RespData.respData(ResultCode.BOOK_QUERY_SUCCESS,result);
    }else {
        //不存在直接返回
        log.debug("布隆过滤器中不存在");
        return RespData.respData(ResultCode.BOOK_QUERY_ERROR.getCode(),"用户不存在",null);
    }
}
1.4.2.6. service层查缓存和数据库
@Override
public String findByUsername(String username) {
    log.debug("通过了布隆过滤器,查询缓存");
    //先查缓存有没有
    Boolean isExist = redisTemplate.opsForSet().isMember("username", username);
    if(isExist){
        //缓存中有,取出来返回
        log.debug("缓存中有");
        Set<String> usernames = redisTemplate.opsForSet().members("username");
        return usernames.stream().filter(s -> s.equals(username)).collect(Collectors.toList()).get(0);
    }else {
        //缓存中不存在
        log.debug("缓存没有,查询数据库");
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername,username);
        User user = userDao.selectOne(queryWrapper);
        //回写缓存
        log.debug("回写缓存");
        redisTemplate.opsForSet().add("username",user.getUsername());
        return user.getUsername();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值