canal探索及应用

news2024/11/23 16:50:09

认识canal

译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

canal应用场景

  1. 缓存/数据同步
  2. 任务下发
  3. 数据异构
  4. 数据采集

缓存/数据同步

在高并发环境下,程序需要应用大量的缓存来适应,当需数据需要更新时,就会出现缓存与数据库不一致的的情况,也就衍生出来很多问题

1.就redis而论,当数据库和redis同时存在该数据,恰巧一个查询到了,直接命中缓存,就不用经过数据库直接返回该数据,正常情况下是没有问题。如果这个数据刚好在更新,直接通过redis的key命中了缓存返回了未更新的数据是不是就出现错误,

     1.2.当然你也可以通过删除缓存,有意进行缓存击穿,直接在数据库查询,假设这个更新还未成功,查询就已经到了,是不是把旧数据重新拿了出来丢进了redis缓存,依旧会出现缓存不一致的情况,

     1.3.当然你还可以先写库,再删缓存,假设你写完库了,还未删除缓存,线程就宕机了,重启之后,缓存是不是还是与数据不一致

先删缓存再写数据

    延时双删策略:

     为有效应对1.2出现的缓存不一致性问题,采用延时双删策略

    1.先删缓存,致使查询击穿,直接查询数据库

    2.再写数据库,把数据库数据写为最新需要更新的数据

    3.休眠一段时间,致使查询线程读取到最新的数据回填到缓存(redis),注意这里会出现早于步骤2更新的查询,致使回填到缓存的数据依旧是旧数据

     4.再次删除缓存,不管缓存是否是最新的数据,再次删除,致使步骤3中出现的情况完全杜绝

 来达到最终一致性

先写数据再删缓存

  由上可以知道,这种情况是可以保证数据最终一致性的,但是会出现线程宕机等意外

 意外重试策略

为避免删除缓存失败的意外,我们要容忍一定次数的失败重试,以及最终失败的人工处理,哈哈哈

这里也可以做为canal的一种应用场景

   把canal伪装成mysql的一个从机,采集binlog日志,监听到mysql存在增删改的动作,讲情报发送给我们的处理程序,处理程序接收到情报执行删除缓存任务并且标识为成功,一旦标识不成功的话,也进行一定次数的重试,最终失败的话还是转人工手动处理

任务下发与数据采集

上面已经讲述的canal采集binlog日志,将情报传递给处理程序,就不做过多描述了哈

数据异构

在大型互联网架构中,为保证数据库的高可用性,经常会采用分库分表来解决性能问题,但是分库分表之后又会出现新的问题,我一个查询可能需要关联多张表,而这些表分布于不同的数据库中怎么办?

维度异构

某个用户的订单数据,散落在n个表中,某一天用户需要查看自己的订单数据怎么办?

我之前有个案列讲的是将用户id进行分库分表,按照雪花算法生成一串id,再由id去除以库的数量,取余,取余数等于要存入的分库序号里面,同样的再按照某种算法把数据存入分表,当查询这个用户的订单时,根据该用户id所在的维度,去对应的地方取数据

聚合异构

当要获取一个用户的详细信息时,该信息包含基本信息,银行卡号,身份证号,等等详细信息,而这些信息散落在不同分库中,根据这个用户的id需要查询关联到很多个库才能收集齐具体的数据,这就是聚合数据异构现象

canal也是实现数据异构的手段之一,它将你需要查询的数据按照某一个维度又重新聚合在一个数据库中,让你去查询

实战

mysql开启binlog

这里是我的mysql的配置,你们也可以根据自己的实际情况来修改,当然你得非常熟悉这些配置文件所包含的意义,盲目修改容易丢库丢数据,甚至搞崩服务

[client]
#password	= your_password
port		= 3306
socket		= /tmp/mysql.sock

[mysqld]
port		= 3306
socket		= /tmp/mysql.sock
datadir = /home/java/msql
default_storage_engine = InnoDB
performance_schema_max_table_instances = 400
table_definition_cache = 400
skip-external-locking
key_buffer_size = 256M
max_allowed_packet = 100G
table_open_cache = 1024
sort_buffer_size = 4M
net_buffer_length = 4K
read_buffer_size = 4M
read_rnd_buffer_size = 256K
myisam_sort_buffer_size = 64M
thread_cache_size = 128
query_cache_size = 128M
tmp_table_size = 128M
sql-mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
wait_timeout=31536000  
interactive_timeout=31536000
lower_case_table_names=1


explicit_defaults_for_timestamp = true
#skip-name-resolve
max_connections = 500
max_connect_errors = 100
open_files_limit = 65535

log-bin=mysql-bin
binlog_format=ROW
server-id = 1
expire_logs_days = 10
slow_query_log=1
slow-query-log-file=/home/java/msql/mysql-slow.log
long_query_time=3
#log_queries_not_using_indexes=on
early-plugin-load = ""


innodb_data_home_dir = /home/java/msql
innodb_data_file_path = ibdata1:10M:autoextend
innodb_log_group_home_dir = /home/java/msql
innodb_buffer_pool_size = 1024M
innodb_log_file_size = 512M
innodb_log_buffer_size = 128M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50
innodb_max_dirty_pages_pct = 90
innodb_read_io_threads = 12
innodb_write_io_threads = 12

[mysqldump]
quick
max_allowed_packet = 500M

[mysql]
no-auto-rehash

[myisamchk]
key_buffer_size = 256M
sort_buffer_size = 4M
read_buffer = 2M
write_buffer = 2M

[mysqlhotcopy]
interactive-timeout

 重载配置,重启服务后,查看是否成功开启binlog日志

show variables like 'log_bin'; 
show variables like 'binlog_format';
show master logs;

 

 

 bin_log开启成功了

 binlog的模式 STATEMENT,ROW,MIXED

STATEMENT模式

每一条会修改数据的sql语句会记录到binlog中。优点是并不需要记录每一条sql语句和每一行的数据变化,减少了binlog日志量,节约IO,提高性能。缺点是在某些情况下会导致master-slave中的数据不一致(如sleep()函数, last_insert_id(),以及user-defined functions(udf)等会出现问题)

ROW模式

不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了,修改成什么样了。而且不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题。缺点是会产生大量的日志,尤其是alter table的时候会让日志暴涨。

MIXED模式

以上两种模式的混合使用,一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog,MySQL会根据执行的SQL语句选择日志保存方式。
 

 开启了ROW模式

 

 这里是我的日志名称和大小

canal server搭建

下载https://github.com/alibaba/canal/releases

 

 下载完成后解压,你也可以上传到linux服务器上然后解压,实际上都是一个道理

然后在解压路径中找到\canal\conf\example

修改

instance.properties

#################################################
## mysql serverId , v1.0.26+ will autoGen
# canal.instance.mysql.slaveId=0

# enable gtid use true/false
canal.instance.gtidon=false

# 数据库地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名称
canal.instance.master.journal.name=mysql-bin.000001
# mysql主库连接时起始的binlog偏移量
canal.instance.master.position=156
# mysql主库连接时起始的binlog时间戳
canal.instance.master.timestamp=
canal.instance.master.gtid=

# rds oss binlog
canal.instance.rds.accesskey=
canal.instance.rds.secretkey=
canal.instance.rds.instanceId=

# table meta tsdb info
canal.instance.tsdb.enable=true
#canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb
#canal.instance.tsdb.dbUsername=canal
#canal.instance.tsdb.dbPassword=canal

#canal.instance.standby.address =
#canal.instance.standby.journal.name =
#canal.instance.standby.position =
#canal.instance.standby.timestamp =
#canal.instance.standby.gtid=

# username/password
# 在mysql服务器授权的账号密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
#canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==

# table regex .*..*表示监听所有表 也可以写具体的表名,用,隔开
canal.instance.filter.regex=.*\\..*
# table black regex  mysql 数据解析表的黑名单,多个表用,隔开
canal.instance.filter.black.regex=
# table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch
# table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch

# mq config
canal.mq.topic=example
# dynamic topic route by schema or table regex
#canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..*
canal.mq.partition=0
# hash partition config
#canal.mq.partitionsNum=3
#canal.mq.partitionHash=test.table:id^name,.*\\..*
#canal.mq.dynamicTopicPartitionNum=test.*:4,mycanal:6
#################################################

 主要修改这几个地方

canal.instance.master.address=127.0.0.1:3306
canal.instance.master.journal.name=mysql-bin.000001
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal

这里其实是mysql的binlog

\canal\bin

 

 我这里就直接用window来启动了哈,linux也是这样,但是是执行./startup.sh这个文件

demo

好了我们实现一个mysql的数据监控

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zkb</groupId>
    <artifactId>canal-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>canal-demo</name>
    <description>canal-demo</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.protocol</artifactId>
            <version>1.1.4</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.4</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
canal:
  server:
    ip: 127.0.0.1
    port: 11111
    username: canal
    password: canal
  promotion:
    destination: example
    batchSize: 1000
    subscribe: xxx.test    #这里是具体的库名和表名,当然你也可以监控所有

package com.zkb;

import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.List;


@Slf4j
@Component
public class CanalUtil implements CommandLineRunner {
    @Value("${canal.server.ip}")
    private String canalServerIp;

    @Value("${canal.server.port}")
    private int canalServerPort;

    @Value("${canal.server.username}")
    private String userName;

    @Value("${canal.server.password}")
    private String password;

    @Value("${canal.promotion.destination}")
    private String destination;
    @Value("${canal.promotion.subscribe}")
    private String subscribe;


    @Override
    public void run(String...args) {
        CanalConnector connector =
            CanalConnectors.newSingleConnector(new InetSocketAddress(canalServerIp, canalServerPort), destination, userName, password);
        int batchSize = 1000;
        try {
            connector.connect();
            System.out.println("连接中");
            connector.subscribe(subscribe);

            connector.rollback();
            try {
                while (true) {
                    Message message = connector.getWithoutAck(batchSize);
                    long batchId = message.getId();
                    int size = message.getEntries().size();
                    if (batchId == -1 || size == 0) {
                        Thread.sleep(1000);
                    } else {
                        log.info("msgId -> " + batchId);
                        dataHandle(message.getEntries());
                    }
                    connector.ack(batchId);
                                   }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (InvalidProtocolBufferException e) {
                e.printStackTrace();
            }
        } finally {
            connector.disconnect();
            //防止频繁访问数据库链接: 线程睡眠 5秒
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void dataHandle(List<Entry> entries) throws InvalidProtocolBufferException {
        for (Entry entry : entries) {
            if(entry.getEntryType() != CanalEntry.EntryType.ROWDATA){
                continue;
            }
            if (EntryType.ROWDATA == entry.getEntryType()) {
                RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
                for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
                    switch (rowChange.getEventType()){
                        case INSERT:
                            // 表名
                            String tableName = entry.getHeader().getTableName();
                            List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                            StringBuffer sb = new StringBuffer();
                            afterColumnsList.stream().forEach(s->{
                                sb.append(s.getValue()).append(",");
                            });
                            System.out.println("插入字段为:["+sb.toString()+"]");
                            break;
                        case UPDATE:
                            List<CanalEntry.Column> beforeColumnsList1 = rowData.getBeforeColumnsList();
                            StringBuffer sb1 = new StringBuffer();
                            beforeColumnsList1.stream().forEach(s->{
                                sb1.append(s.getValue()).append(",");
                            });
                            System.out.println("更新前的数据是:["+sb1.toString()+"]");
                            List<CanalEntry.Column> afterColumnsList2 = rowData.getAfterColumnsList();
                            StringBuffer sb2 = new StringBuffer();
                            afterColumnsList2.stream().forEach(s->{
                                sb2.append(s.getValue()).append(",");
                            });
                            System.out.println("更新后的数据是:["+sb2.toString()+"]");
                            break;
                        case DELETE:
                            List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
                            StringBuffer sb3 = new StringBuffer();
                            beforeColumnsList.stream().forEach(s->{
                                sb3.append(s.getValue()).append(",");
                            });
                            System.out.println("被删除的数据是:["+sb3.toString()+"]");
                            break;
                        default:
                    }
                }
            }
        }
    }
  }

package com.zkb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CanalDemoApplication {

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

}

 

 我这里用的是一个test表,里面有两条非常简单的测试数据

当我修改id为1的name时

 

可以很清晰的监控到我的数据变化,其它的我就不试了哈,到这里我们的demo就已经很好的工作,如此如果我们结合mq或者kafka是不是就能很好的解决我们缓存最终一致性的问题

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/748628.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

冷门研究冒险家同济陈涵晟:让科技帮助未来人类拓展艺术边界

原来他们是这样走过来的&#xff01; 【AI红人荟】——这里是TechBeat人工智能社区为优秀的AI工作者开设的人物专访栏目。从膜拜“红人”到成为“红人”&#xff0c;TechBeat与你一起&#xff0c;在AI进阶之路上&#xff0c;升级打怪、完美通关~ 本篇人物&#xff0c;是来自同…

消息中间件RabbitMQ详解

一、 消息中间件 简介 消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流&#xff0c;并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型&#xff0c;它可以在分布式环境下扩展进程间的通信。 使用环境 消息中间件适用于需要可靠的数据传送…

超细整理,接口自动化测试-DDT参数化驱动实战,一招打通...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 ddt说明 一般进行…

MySQL数据库——索引练习

一、练习题目 1、建立一个utf8编码的数据库test1 2、建立商品表goods和栏目表category&#xff08;要求&#xff1a;按如下表结构创建表&#xff0c;并且存储引擎engine myisam 字符集charset utf8&#xff09; 3、删除 goods 表中的 goods_desc 字段及货号字段,并增加 click…

JMeter中如何实现接口之间的关联?

关联是Jmeter工具中非常重要的一个技术。因为在测试过程过有些数据是经常发生变化的&#xff0c;要获取并使用这些数据&#xff0c;就要使用关联。 比如&#xff1a;用户登录后&#xff0c;session信息都不同&#xff0c;有些操作要使用session&#xff0c;就需要将这个动态的…

【国产复旦微FMQL45教程】-Procise应用流程

本教程采用 FMQL7045 FPGA开发板来完成整个试验&#xff0c;板卡照片如下&#xff1a; 具有丰富的接口资源&#xff0c;系统框图如下&#xff1a; 本教程用于完成基于Vivado的FMQL45的LED实验&#xff0c;目标是能够将这款开发板PL端先跑起来。 2 Procise工程建立 &#xff0…

AI绘画在线生成!推荐这个国产Midjourney平替

AI技术越来越成熟&#xff0c;不仅能生成文本&#xff0c;还能绘画。AI绘画软件层出不穷&#xff0c;很多人接触的是Midjourney。我之前也用过Midjourney&#xff0c;确实在作品精确度、图像细节等方面很出色。但用Midjourney需要有良好的网络&#xff0c;而且必须得是会员。 因…

Mysql常用存储引擎------MyISAM存储引擎

文章目录 一、MyISAM存储引擎1、1加锁与并发2、1修复3、1索引特性3、4 延迟更新索引键3、5 MyISAM 压缩表3、6 MyISAM 性能 二、MySQL 存储引擎 MyISAM 与 InnoDB 如何选择&#xff1f;一、InnoDB支持事务&#xff0c;MyISAM不支持&#xff0c;这一点是非常重要。事务是一种高级…

【嵌入式Qt开发入门】Qt如何网络编程——建立TCP通信服务端(附项目代码)

TCP 简介 TCP 协议&#xff08;Transmission Control Protocol&#xff09;全称是传输控制协议是一种面向连接的、可靠的、 基于字节流的传输层通信协议。 TCP 通信必须先建立 TCP 连接&#xff0c;通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来…

7.kafka+ELK连接

文章目录 kafkaELK连接部署Kafkakafka操作命令kafka架构深入FilebeatKafkaELK连接 kafkaELK连接 部署Kafka ###关闭防火墙systemctl stop firewalld systemctl disable firewalldsetenforce 0vim /etc/selinux/configSELINUXdisabled###下载安装包官方下载地址&#xff1a;ht…

python散记

"""字符串格式化的两种方法"""name"sans" age18 math_score90.56 english_score88.8print(f"这个学生的名字叫{name},年龄{age},数学分数是{math_score},总分是{math_scoreenglish_score}") print("这个学生的名字叫%s…

APP开发的未来:虚拟现实和增强现实的角色

移动应用程序越来越多地在我们的日常生活中发挥着重要作用。但是&#xff0c;随着技术的不断发展&#xff0c;未来的 APP开发会有什么新的发展方向呢&#xff1f;这是每个人都在关心的问题。在过去的几年中&#xff0c;移动应用程序领域发生了巨大变化。像 VR/AR这样的技术为人…

第63讲:Python编程案例之猴子吃桃

文章目录 1.需求描述以及分析2.递推方式实该该程序3.递归方式实现该程序 1.需求描述以及分析 需求描述&#xff1a; 猴子第一天摘了若干个桃子&#xff0c;第一天吃了若干个桃子中的一半&#xff0c;觉得不过瘾&#xff0c;又多吃了一个。 第二天早上又将第一天剩下的桃子吃…

Spring 项目创建和使用2 (Bean对象的存取)

目录 一、创建 Bean 对象 二、将Bean对象存储到 Spring容器中 三、创建 Spring 上下文&#xff08;得到一个Spring容器&#xff09; 1. 通过在启动类中 ApplicationContext 获取一个 Spring容器 2. 通过在启动类种使用 BeanFactory 的方式来得到 Spring 对象 &#xff08;此…

MAYA粒子目标goalV和goalU详细应用

一下就填充到点 一个一个点填充 nParticleShape1.goalV0.5; nParticleShape1.goalU0.5; 粒子向中心移动 V方向使用渐变 删除U方向表达式 也使用渐变 使用圆角 nParticleShape1.goalUrand(0,1); nParticleShape1.goalUnParticleShape1.goalU0.02; nParticleShape1.goalUnPartic…

Excel-公式VLOOKUP 使用方法-小记

个人愚见 表示 MongoDB列中的任意一条数据 在 MySQL列 精确查找 和MongoDB列 中一模一样的数据&#xff0c;有的话返回MongoDB列数据&#xff0c;没有话返回#N/A 官方解释

redis 三种缓存更新策略

今天聊聊redis 三种缓存更新策略分别是&#xff1a; Cache Aside&#xff08;旁路缓存&#xff09;策略&#xff1b; Read/Write Through&#xff08;读穿 / 写穿&#xff09;策略&#xff1b; Write Back&#xff08;写回&#xff09;策略&#xff1b; 其中 Cache Aside策略…

php通过IP获取用户当前所在城市

php获取当前用户所在城市 php通过ip免申请api获取所在城市的代码包括省市区sql数据 <?php function getName($pinyin,$lv){$servername "localhost";$username "root";$password "root";$dbname "ttx";try {$conn new PDO(…

Blazor前后端框架Known-V1.2.4

V1.2.4 Known是基于C#和Blazor开发的前后端分离快速开发框架&#xff0c;开箱即用&#xff0c;跨平台&#xff0c;一处代码&#xff0c;多处运行。 Gitee&#xff1a; https://gitee.com/known/KnownGithub&#xff1a;https://github.com/known/Known 概述 基于C#和Blazor…

一款开源的Hitomi-Downloader视频下载工具,几乎支持所有主流视频网站

一款开源的Hitomi-Downloader视频下载工具&#xff0c;几乎支持所有主流视频网站 用过IDM的朋友可能知道IDM有个强大的功能就是可以嗅探网站各种视频、音频等资源&#xff0c;然后提供快捷下载&#xff0c;可不巧的是IDM是收费软件。对于不愿意付费购买IDM的朋友&#xff0c;能…