redis持久化、主从和哨兵架构

一、redis持久化

1、RDB快照(snapshot)

redis配置RDB存储模式,修改redis.conf文件如下配置:

# 在300s内有100个或者以上的key被修改就会把redis中的数据持久化到dump.rdb文件中
# save 300 100# 配置数据存放目录(现在指定的是redis目录下的data文件夹)
dir ./data# 配置rdb文件存储文件名
dbfilename dump.rdb

 也可以在连接redis时手动输入save或者bgsave命令都会将所有redis内存快照生成到一个新的dump.rdb文件,并覆盖原来的rdb文件

bgsave写时复制(COW)机制

主线程在执行redis命令,在内存中生成副本的同时由主线程fork出来的子进程开始读取redis内存中的数据并且写到rdb文件中,fork出来的bgsave子进程和主线程互不影响(redis后台生成rdb文件是采用bgsave方式)

save和bgsave

  1. save是和主线程同步执行,bgsave是异步执行
  2. save会阻塞其它命令,bgsave在主线程fork时会有短暂的阻塞
  3. save不会额外消耗内存,bgsave不会阻塞客户端命令

命令

save

bgsave

IO类型

同步

异步

是否阻塞redis其它命令

否(在生成子进程执行调用fork函数时会有短暂阻塞)

复杂度

O(n)

O(n)

优点

不会消耗额外内存

不阻塞客户端命令

缺点

阻塞客户端命令

需要fork子进程,消耗内存

2、AOF(append-only file)

rdb的缺点也非常明显,不是实时持久化数据到rdb文件,如果系统宕机,redis会丢失最近时间的数据。AOF持久化是将修改的每一条指令报存到appendonly.aof文件中(和mysql的redolog一样先写os cache操作系统缓存,每隔一段时间sync到磁盘)

开启aof

修改redis.conf文件如下配置:

# 开启aof
appendonly yes
# aof文件名
appendfilename "appendonly.aof"# aof三种策略
# 每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
# appendfsync always
# 每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
# appendfsync everysec
# 从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
# appendfsync no

执行set gaorufeng 123 ex 100 命令生成的aof文件如下(星号后面的数字代表命令有多少个参数,$号后面的数字代表这个参数有几个字符,如果执行带过期时间的set命令,aof文件里记录的是并不是执行的原始命令,而是记录key过期的时间戳)

*5
$3
SET
$9
gaorufeng
$3
123
$4
PXAT
$13
1693895615978
AOF重写 

比如一条自增命令,在生成aof文件就会生成多条指令,incr count命令的aof文件如下:

*2
$4
incr
$5
count
*2
$4
incr
$5
count
*2
$4
incr
$5
count
*2
$4
incr
$5
count

重写的作用旨在减少指令的存储,可以配置aof让redis重写,修改redis.conf文件如下配置

# 当前文件是之前文件大小的两倍(设置100),自动触发重写
auto-aof-rewrite-percentage 100
# aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
auto-aof-rewrite-min-size 64mb

 redis客户端执行bgrewriteaof手动重写aof,此时命令如下:

*3
$3
SET
$5
count
$1
4

aof方式存储数据完整,rdb存储数据文件小,故启动redis时当选rdb恢复内存数据,二进制恢复数据快 

命令

RDB

AOF

启动优先级

体积

恢复速度

数据安全性

容易丢数据

根据策略决定

3、混合持久化

# 开启混合持久化
aof-use-rdb-preamble yes

在开启aof的前提下,aof重写前内存做rdb快照处理,并且将rdb快照内容和增量的aof修改内存数据的命令存在一起

Redis数据备份策略:

  1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  3. 每次copy备份的时候,都把太旧的备份给删了
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

二、redis主从架构

redis主从架构搭建,配置从节点步骤:

1、进入到redis目录下,创建数据存储目录
mkdir data
cd data
mkdir 6379
mkdir 6380
mkdir 63812、copy不同配置文件
cp -r redis.conf redis.conf.6379
cp -r redis.conf redis.conf.6380
cp -r redis.conf redis.conf.63813、(6379为主节点,其余为从节点)修改配置文件
vim redis.conf.6379
在6379配置文件中修改如下
port 6379
daemonize yes
protected-mode no
pidfile /var/run/redis_6379.pid
logfile "6379.log"
dir /usr/local/redis/redis-6.2.7/data/6379
# 需要注释掉bind
# bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)4、配置从节点6380
vim redis.conf.6380
在6380配置文件中修改如下
port 6380
daemonize yes
protected-mode no
pidfile /var/run/redis_6380.pid
logfile "6380.log"
dir /usr/local/redis/redis-6.2.7/data/6380
# 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replicaof 192.168.146.128 6379
# 配置从节点只读
replica-read-only yes5、配置从节点6381
vim redis.conf.6381
在6381配置文件中修改如下
port 6381
daemonize yes
protected-mode no
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dir /usr/local/redis/redis-6.2.7/data/6381
# 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replicaof 192.168.146.128 6379
# 配置从节点只读
replica-read-only yes6、先启动主节点6379,后启动从节点6380、6381
src/redis-server redis.conf.6379
src/redis-server redis.conf.6380
src/redis-server redis.conf.63817、连接从节点查看是否能获取主节点数据
src/redis-cli -p 6379
src/redis-cli -p 6381
Redis主从工作原理 

如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。

master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。

当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

主从复制(全量复制)流程图:

数据部分复制

当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。

 主从复制(部分复制,断点续传)流程图:

如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据

Jedis连接代码示例:

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version>
</dependency>
public class JedisSingleTest {public static void main(String[] args) {JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();jedisPoolConfig.setMaxTotal(20);jedisPoolConfig.setMaxIdle(10);jedisPoolConfig.setMinIdle(5);// timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.146.128", 6379, 3000, null);Jedis jedis = null;try {//从redis连接池里拿出一个连接执行命令jedis = jedisPool.getResource();System.out.println(jedis.set("single", "gao"));System.out.println(jedis.get("single"));//管道示例//管道的命令执行方式:cat redis.txt | redis-cli -h 127.0.0.1 -a password - p 6379 --pipePipeline pl = jedis.pipelined();for (int i = 0; i < 10; i++) {pl.incr("pipelineKey");pl.set("gao" + i, "gao");}List<Object> results = pl.syncAndReturnAll();System.out.println(results);//lua脚本模拟一个商品减库存的原子操作//lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10jedis.set("product_count_10016", "15");  //初始化商品10016的库存String script = " local count = redis.call('get', KEYS[1]) " +" local a = tonumber(count) " +" local b = tonumber(ARGV[1]) " +" if a >= b then " +"   redis.call('set', KEYS[1], a-b) " +"   return 1 " +" end " +" return 0 ";Object obj = jedis.eval(script, Arrays.asList("product_count_10016"), Arrays.asList("10"));System.out.println(obj);} catch (Exception e) {e.printStackTrace();} finally {//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。if (jedis != null)jedis.close();}}
}

管道(Pipeline)

客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。

pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令不会有影响,继续执行。

三、redis哨兵高可用架构

sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。

哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)

 基于主从架构的redis哨兵架构搭建步骤:

1、复制一份sentinel.conf文件
cp sentinel.conf sentinel-26379.conf2、将相关配置修改为如下值:
port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.146.128 6379 2   # mymaster这个名字随便取,客户端访问时会用到3、启动sentinel哨兵实例
src/redis-sentinel sentinel-26379.conf4、查看sentinel的info信息
src/redis-cli -p 26379
127.0.0.1:26379>info
可以看到Sentinel的info里已经识别出了redis的主从5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:

sentinel known-replica mymaster 192.168.146.128 6380 #代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.146.128 6381 #代表redis主节点的从节点信息
sentinel known-sentinel mymaster 192.168.146.128 26381 c3c89b0c5d1b81f5e9487ae17260c8fbfe477c2e  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.146.128 26380 b3d47687e6b2d6d569b0c6a53a1b92be5c442c5c  #代表感知到的其它哨兵节点

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集群元数据信息会变成如下所示:

sentinel known-replica mymaster 192.168.146.128 6380 #代表主节点的从节点信息
sentinel known-replica mymaster 192.168.146.128 6379 #代表主节点的从节点信息
sentinel known-sentinel mymaster 192.168.146.128 26381 c3c89b0c5d1b81f5e9487ae17260c8fbfe477c2e  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.146.128 26379 af83ca977cadbe5842472fea925c85906c26e1e0  #代表感知到的其它哨兵节点

同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380

sentinel monitor mymaster 192.168.146.128 6381 2

 当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点加入集群

哨兵的Jedis连接代码:

public class JedisSentinelTest {public static void main(String[] args) throws IOException {JedisPoolConfig config = new JedisPoolConfig();config.setMaxTotal(20);config.setMaxIdle(10);config.setMinIdle(5);String masterName = "mymaster";Set<String> sentinels = new HashSet<String>();sentinels.add(new HostAndPort("192.168.0.60",26379).toString());sentinels.add(new HostAndPort("192.168.0.60",26380).toString());sentinels.add(new HostAndPort("192.168.0.60",26381).toString());//JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池//JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);Jedis jedis = null;try {jedis = jedisSentinelPool.getResource();System.out.println(jedis.set("sentinel", "gao"));System.out.println(jedis.get("sentinel"));} catch (Exception e) {e.printStackTrace();} finally {//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。if (jedis != null)jedis.close();}}
}

1、引入相关依赖:

<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>

springboot项目核心配置:

server:port: 8080spring:redis:database: 0timeout: 3000sentinel:    #哨兵模式master: mymaster #主服务器所在集群名称nodes: 192.168.146.128:26379,192.168.146.128:26380,192.168.146.128:26381lettuce:pool:max-idle: 50min-idle: 10max-active: 100max-wait: 1000

访问代码:

@RestController
public class IndexController {private static final Logger logger = LoggerFactory.getLogger(IndexController.class);@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到* 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,* 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip** @throws InterruptedException*/@RequestMapping("/test_sentinel")public void testSentinel() throws InterruptedException {int i = 1;while (true){try {stringRedisTemplate.opsForValue().set("gao"+i, i+"");System.out.println("设置key:"+ "gao" + i);i++;Thread.sleep(1000);}catch (Exception e){logger.error("错误:", e);}}}
}

StringRedisTemplate与RedisTemplate详解

spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在RedisTemplate中提供了几个常用的接口方法的使用,分别是:

private ValueOperations<K, V> valueOps;
private HashOperations<K, V> hashOps;
private ListOperations<K, V> listOps;
private SetOperations<K, V> setOps;
private ZSetOperations<K, V> zSetOps;

RedisTemplate中定义了对5种数据结构操作

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

Redis客户端命令对应的RedisTemplate中的方法列表:

String类型结构

Redis

RedisTemplate rt

set key value

rt.opsForValue().set("key","value")

get key

rt.opsForValue().get("key")

del key

rt.delete("key")

strlen key

rt.opsForValue().size("key")

getset key value

rt.opsForValue().getAndSet("key","value")

getrange key start end

rt.opsForValue().get("key",start,end)

append key value

rt.opsForValue().append("key","value")

Hash结构

hmset key field1 value1 field2 value2...

rt.opsForHash().putAll("key",map) //map是一个集合对象

hset key field value

rt.opsForHash().put("key","field","value")

hexists key field

rt.opsForHash().hasKey("key","field")

hgetall key

rt.opsForHash().entries("key")  //返回Map对象

hvals key

rt.opsForHash().values("key") //返回List对象

hkeys key

rt.opsForHash().keys("key") //返回List对象

hmget key field1 field2...

rt.opsForHash().multiGet("key",keyList)

hsetnx key field value

rt.opsForHash().putIfAbsent("key","field","value"

hdel key field1 field2

rt.opsForHash().delete("key","field1","field2")

hget key field

rt.opsForHash().get("key","field")

List结构

lpush list node1 node2 node3...

rt.opsForList().leftPush("list","node") 

rt.opsForList().leftPushAll("list",list) //list是集合对象

rpush list node1 node2 node3...

rt.opsForList().rightPush("list","node") 

rt.opsForList().rightPushAll("list",list) //list是集合对象

lindex key index

rt.opsForList().index("list", index)

llen key

rt.opsForList().size("key")

lpop key

rt.opsForList().leftPop("key")

rpop key

rt.opsForList().rightPop("key")

lpushx list node

rt.opsForList().leftPushIfPresent("list","node")

rpushx list node

rt.opsForList().rightPushIfPresent("list","node")

lrange list start end

rt.opsForList().range("list",start,end)

lrem list count value

rt.opsForList().remove("list",count,"value")

lset key index value

rt.opsForList().set("list",index,"value")

Set结构

sadd key member1 member2...

rt.boundSetOps("key").add("member1","member2",...)

rt.opsForSet().add("key", set) //set是一个集合对象

scard key

rt.opsForSet().size("key")

sidff key1 key2

rt.opsForSet().difference("key1","key2") //返回一个集合对象

sinter key1 key2

rt.opsForSet().intersect("key1","key2")//同上

sunion key1 key2

rt.opsForSet().union("key1","key2")//同上

sdiffstore des key1 key2

rt.opsForSet().differenceAndStore("key1","key2","des")

sinter des key1 key2

rt.opsForSet().intersectAndStore("key1","key2","des")

sunionstore des key1 key2

rt.opsForSet().unionAndStore("key1","key2","des")

sismember key member

rt.opsForSet().isMember("key","member")

smembers key

rt.opsForSet().members("key")

spop key

rt.opsForSet().pop("key")

srandmember key count

rt.opsForSet().randomMember("key",count)

srem key member1 member2...

rt.opsForSet().remove("key","member1","member2",...)

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构:树的概念和结构

文章目录 1. 树的概念2. 树的结构3. 树的相关概念4. 树的表示孩子表示法双亲表示法孩子兄弟表示法 5. 树在实际中的应用5. 树在实际中的应用 1. 树的概念 树是一种非线性的数据结构,它是由 n (n > 0)个有限结点组成一个具有层次关系的. 把它叫做树是因为它看起来像一棵倒挂的…

学会用命令行创建uni-app项目并用vscode开放项目

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 创建 uni-app 项目 命令行创建 uni-app 项目 编译和运行 uni-app 项目&#xff1a; 用 VS Code 开发 uni…

机器学习笔记之最优化理论与方法(七)无约束优化问题——常用求解方法(上)

机器学习笔记之最优化理论与方法——基于无约束优化问题的常用求解方法[上] 引言总体介绍回顾&#xff1a;线搜索下降算法收敛速度的衡量方式线性收敛范围高阶收敛范围 二次终止性朴素算法&#xff1a;坐标轴交替下降法最速下降法(梯度下降法)梯度下降法的特点 针对最速下降法缺…

自动创建设备结点:udev机制的实现过程

什么是udev&#xff1f; 在Linux系统中&#xff0c;/dev目录是用来存放设备文件的&#xff0c;每个文件指向一个系统设备文件&#xff0c;用户的程序可以通过使用这些文件来对真实硬件进行设备操作&#xff0c;但是在2.4内核时代&#xff0c;/dev下保存了所有kernel可以支持的硬…

Prometheus+Grafana可视化监控【MySQL状态】

文章目录 一、Prometheus监控MySQL二、Grafana添加MySQL监控模板 PrometheusGrafana部署请参考上篇文章: PrometheusGrafana部署: 一、Prometheus监控MySQL 1、创建MySQL监控用户 mysql> create user "prometheus""%" identified by NTQ34tg*19VF; m…

Sharding-JDBC分库分表-分布式事务-5

分布式事务 Sharding JDBC事务介绍 分库分表必然会涉及到分布式事务的问题&#xff0c;关于这方面&#xff0c;sharding JDBC为用户提供了两种分布式事务解决方案&#xff1a;XA事务和BASE事务&#xff0c;这两个的区别是&#xff1a;XA事务&#xff0c;追求强一致性&#xf…

Linux——环境变量

✅<1>主页&#xff1a;&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;Linux——环境变量 ☂️<3>开发环境&#xff1a;Centos7 &#x1f4ac;<4>前言&#xff1a;环境变量(environment variables)一般是指在操作系统中用来指定操作…

Redis优化 RDB AOF持久化

---------------------- Redis 高可用 ---------------------------------------- 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。 但是在Redis语境…

每日一博 - 闲聊Https工作原理

文章目录 HTTPS工作原理数据的加密和解密与HTTP相比&#xff0c;HTTPS增加了多少性能开销&#xff1f;Flow小结 HTTPS工作原理 HTTPS&#xff08;Hypertext Transfer Protocol Secure&#xff09;是 Hypertext Transfer Protocol&#xff08;HTTP&#xff09;的扩展。HTTPS使用…

【工具】【Charles】rewrite:重写

GET请求 protocol/host/port 可填可不填&#xff0c;可填&#xff08;*&#xff09; path需完整匹配&#xff0c;必填 Query&#xff1a;有填写需完整匹配&#xff0c;可填可不填&#xff0c;可填&#xff08;*&#xff09; 匹配规则&#xff1a; Type&#xff1a;修改请求参…

使用C语言EasyX 创建动态爱心背景

简介 在计算机图形学的世界中&#xff0c;有很多方法可以使程序的界面更加吸引人。在本篇博客中&#xff0c;我将向大家介绍如何使用 EasyX 图形库在 C 中创建一个动态的爱心背景。这不仅是一个简单的动画效果&#xff0c;它还包括背景的星星、旋转的心形以及一个美观的背景渐…

抖音中文点选验证码识别方案

最近研究了一下抖音中文点选验证码的识别&#xff0c;居然正确率高达98%。 首先我们来看一下效果 一、识别方法 1、数据集准备 我们需要借助爬虫去下载大量的验证码图片。这些有两种图片&#xff0c;一种是小图&#xff0c;包含需要点选的中文和顺序&#xff0c;还有一种是大…