Django项目中的问题

文章目录


nginx + uwsgi + django

nginx + uwsgi + django 生产环境部署
部署的作用:
请求处理整体流程
在这里插入图片描述
1.nginx接收到浏览器发送过来的http请求,将包进行解析,分析url
2.静态文件请求:就直接访问用户给nginx配置的静态文件目录,直接返回用户请求的静态文件
3.动态接口请求:那么nginx就将请求转发给uWSGI,最后到达django处理

nginx的作用
反向代理,可以拦截一些web攻击,保护后端的web服务器
反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址

负载均衡,根据轮询算法,分配请求到多节点web服务器

缓存静态资源,加快访问速度,释放web服务器的内存占用,专项专用
配置完之后

python3 manage.py collectstatic

在项目acapp就有static文件了

nginx怎么实现的负载均衡
一台服务器能处理的并发数是固定的,如果并发数超过了最大线程数,那么新来的请求就只能排队等待处理了。如果有负载均衡的话,我们就可以将所有的请求分配到不同的服务器上。

Nginx 主要的负载均衡策略:
1.轮询策略: 每次将请求按顺序轮流发送至相应的服务器上
2.最少连接数负载均衡:指每次将请求分发到当前连接数最少的服务器上,也就是 Nginx 会将请求试图转发给相对空闲的服务器以实现负载平衡
3.加权负载均衡:对某些性能比较好的服务器使用较大权重,增加其响应次数。
比如后端有三台服务器,对某些性能比较好的服务器,weight = 3 , 其它两台weight=1.总共有五次请求的话,有三次都会给这个性能比较好的服务器去处理。
4.ip-hash负载均衡策略:ip-hash 负载均衡策略可以根据客户端的 IP,将其固定的分配到相应的服务器上。
假如用户的登录信息是保存在单台服务器上的,而不是保存在类似于 Redis 这样的第三方中间件上时,如果不能将每个客户端的请求固定的分配到一台服务器上,就会导致用户的登录信息丢失。因此用户在每次请求服务器时都需要进行登录验证,这样显然是不合理的

uWSGI的使用

单节点服务器的简易部署
轻量级,好部署

如何实现第三方认证

在这里插入图片描述
用户点完第三方登录按钮,就向web发送了一个申请,说我要用第三方的账号去登录;然后web就把appid报给第三方,第三方会给用户一个页面询问是否要把信息授权给刚刚这个网站。

如果用户同意的话,表明我要把信息授权给刚刚那个网站,acwing接受到同意的信息,会把一个授权码发给网站;网站收到授权码之后再把它加上自己的身份信息appid, app-secret,再向acwing申请一个授权令牌access-token和用户的id openid(用来识别用户的东西);网站拿到这两个东西之后就能向第三方申请用户的信息了。这里是一个用户名和一个头像。

第一步,申请授权码code

为了防止跨站请求伪造攻击,需要web应用和第三方之间约定一个暗号,就是接到一个code判断是不是从第三方过来的请求。然后就增加一个参数state,它可以设成一个8位随机数,然后发送到第三方,第三方会把这个参数值原封不动的返回给你,只要判断一下接收到的state和发送的state是不是一个state就可以了。这样的话别有用心的人就很难伪造一个8位随机数来攻击你。

这个state需要存下来用来判断请求和回调的一致性,我们可以存在redis里,cache.set(state,True, 7200)搞个有效期两个小时,两小时后它就会被自动删掉
然后我们后端函数receive_code收到第三方发来的code和state,我们判断是否这个state在内存里,if not cache.haskey(state)return redirect(‘index’),如果存在,我们再继续接下来的逻辑步骤

第二步,申请令牌access_token和open_id
第三步,申请用户信息user_info

多人联机对战

为什么使用django_channels,http协议是一个单向协议,我们只能向服务器请求信息,如果我们没有向服务器请求,服务器是不能主动向客户端发送信息的。但是在多人对战中我们想实现,在其中一个客户端发生的事件我们传到服务器,然后服务器再把这个事件同步给所有玩家。这个过程是双向通信的,django_channels就是让我们的django支持wss协议

实现过程

要实现这个多人联机对战,首先因为要在不同的窗口区分对象的身份,我们需要给所有对象增加一个id来同步这些对象在所有窗口的身份编号。这个uuid可以是一个八位的随机数,这样重复的概率就非常低。这样,每一个对象,player, 技能的火球,创建出来之后就有一个能够标识身份的uuid.

然后这个多人联机对战是通过同步事件来实现的,我通过同步几个事件,创建玩家create_player,移动move_to()函数,释放技能shoot_fireball, 和判断攻击attack()函数,来实现同步所有玩家的游戏

玩家的血量等信息需要动态的通信维护,需要做到实时所以对读写效率要求很高,需要用内存数据库redis来存
cache.set(self.room_name, players, 3600) #当最后一个玩家创建完之后,整个对局会保存一个小时

具体编写同步函数的逻辑是,比如实现create_player, 首先引入room概念每个房间有个上限人数。在前端如果一个新用户想进入一个多人联机对战的模式,然后前端就会向后端发送一个send_create_player的请求,后端会有receive()函数接收到这个信息,根据event判断这是一个什么事件做一个路由,路由到对应的后端函数。然后后端函数create_player会实现相应的创建玩家,把玩家信息加进redis,然后把这个信息广播给组里所有玩家await self.channel_layer.group_send

同步移动move_to()
我在多人模式里点击鼠标,监听事件知道我这个行为,我自己因为调用本窗口的move_to()函数可以在窗口内移动;别人怎么知道我移动呢?
如果是多人模式,会通过写的send_move_to()函数向服务器发送信息。send_move_to()通过web_socket向服务器发送一个事件。服务器里的话,会有一个receive()函数接受到信息,发现事件是move_to()会调用后端写的move_to()函数。在这个move_to()函数里会向所有的channel.layer群发,群发我们这个uuid玩家移动的消息。每一个窗口都会在前端接收到信息,路由到前端的receive_move_to()函数,然后在这里,每个玩家就会在各自的窗口里调用各自的move_to()函数了

然后shoot_fireball的逻辑也是类似的

然后同步attack(),也就是技能击中这个事件。我们判断碰撞只在炮弹的发出者所在的窗口进行判断,也就是说 我在我的窗口里击中了某个玩家,这个事件会从前端传到后端然后群发给组内所有玩家。

因为只同步事件,在子弹碰撞的时候由于网络延迟等导致三角函数计算会出现误差;我们在子弹击中的时候,同步一下attackee的位置,做一个补偿

然后也是通过这套逻辑实现的聊天系统,首先在前端渲染出这个聊天框,然后广播这个聊天系统的逻辑和同步各种操作的逻辑是一样的。在前端我们多人对战模式下我监听到了这个事件,然后通过send()函数把这个事件发到后端,后端recieve函数接收到这个事件通过event判断出要路由到后端message()函数,然后通过self.channel_layer.group_send()把信息广播到各个前端窗口,然后各个玩家接收到信息。

使用Trift服务实现匹配模块

因为我们匹配这个过程具有延时性,可能时间比较长,那我们这个线程就会被阻塞住,造成资源的浪费。Thrift服务可以让我们在另外一台机器上,开一个进程做我们想要的工作,thrift是可以让两个进程之间进行通信。
这个通信是通过ip+端口号进行的,可以放在不同机器上,也可以放在一台机器上

匹配模块的实现大概是需要一个消息队列和一个匹配池,我们把玩家加进消息队列,然后有一个线程worker(是一个死循环)如果有元素的话就直接没有阻塞的把它加进匹配池,如果说消息队列里没有元素的话,我们执行一次匹配然后休息一秒sleep(1)。

然后匹配的逻辑的话,用了一下贪心的思想,我们想把就是分数接近的三名玩家匹配到一起(因为我们房间的容积设置为了3),首先把玩家按照score排个序,然后遍历一下每次取三名玩家,两两判断它们是否满足要求。我们的check要求是分差小于两名玩家的随着等待时间而增加的一个容忍阈值。如果成功了我们就把这三名玩家加到一个组里,async_to_sync(channel_layer.group_add)(room_name, p.channel_name) 然后广播这个信息channel_layer.group_send

mq 基础知识

mq , 应用场景, 可能会出现的问题,
二转率 测试总结。联盟的业务模式, 接口,链路

MQ

消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠专递,消息发布者只管把消息发布到MQ中而不管谁来取,消息使用者只管从MQ中取消息而不管谁发布的,这样发布者和使用者都不用知道对方的存在。

为什么需要mq

在高并发分布式环境下,由于来不及同步处理,通过使用消息队列,可以异步处理请求,从而缓解系统的压力。

举一个订单系统的例子:用户点击下订单,会触发以下业务逻辑流程:

扣减库存

生成相应的订单

发短信通知等等

在业务发展初期这些逻辑可能放在一起同步执行,随着业务订单量增长,需要提升系统服务的性能,这时候可以将一些不需要立即生效的操作拆分出来异步执行,比如发短信通知等,这种场景就可以使用消息队列MQ。

本质还是通过异步来解决同步的系统压力,所以我们在做架构设计的时候有一个原则:能异步的就尽量不要同步。

mq的使用场景

1.异步处理。 消息队列的主要特点是异步处理,主要目的是减少请求响应时间,实现非核心流程异步化,提高系统响应性能。

所以典型的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作,作为消息放入消息队列。

2.应用解耦。

使用了消息队列后,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦。

每个成员不必受其他成员影响,可以更独立自主,只通过消息队列MQ来联系。

举一个例子:用户下订单流程,下订单后会发生扣库存这个动作,上游系统订单和下游系统扣库存,就可以通过上图的消息队列MQ来联系,扣库存异步化,从而实现订单系统与库存系统的应用解耦。

3.流量削锋
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。

应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。

4.日志处理。
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。

5.消息通讯
消息队列一般都内置了高效的通信机制,因此也可以用于单纯的消息通讯,比如实现点对点消息队列或者聊天室等。

消息队列优点

1、屏蔽异构平台的细节:发送方、接收方系统之间不需要了解双方,只需认识消息。

2、异步:消息堆积能力;发送方接收方不需同时在线,发送方接收方不需同时扩容(削峰)。

3、解耦:防止引入过多的API给系统的稳定性带来风险;调用方使用不当会给被调用方系统造成压力,被调用方处理不当会降低调用方系统的响应能力。

4、复用:一次发送多次消费。

5、可靠:一次保证消息的传递。如果发送消息时接收者不可用,消息队列会保留消息,直到成功地传递它。

6、提供路由:发送者无需与接收者建立连接,双方通过消息队列保证消息能够从发送者路由到接收者,甚至对于本来网络不易互通的两个服务,也可以提供消息路由。

mq的问题

由于网络延迟造成的重复消费

如何保证消息不被重复消费
正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。只是不同的消息队列发出的确认消息形式不同,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,kafka实际上有个offet的概念,简单说一下,就是每一个消息都有一个offset,kafka消费过消息后,需要提交offset,让消息队列知道自己已经消费过了。
造成重复消费的原因,就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决方案:
(1)比如,你拿到这个消息做数据库的insert操作,那就容易了,给这个消息做一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
(2)再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
(3)如果上面两种情况还不行。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis.那消费者开始消费前,先去redis中查询有没有消费记录即可。

丢数据

生产者丢数据
支持事务的队列,如RabbitMQ,可以开始事务,但是会造成吞吐量降低
消息队列丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。
如何持久化
将queue的持久化标识durable设置为true,则代表是一个持久的队列
发送消息的时候将deliveryMode=2
消费者丢数据
消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时rabbitMQ会立即将消息删除,这种情况下,如果消费者出现异常而未能处理消息,就会丢失该消息。至于解决方案,采用手动确认消息即可。

无序
保证顺序性

rabbitmq:拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。
kafka:一个topic,一个partition,一个consumer,内部单线程消费,写N个内存queue,然后N个线程分别消费一个内存queue即可。

设立过期时间,直接丢弃数据
恢复消费者,临时扩容,快速消费

消息队列中的topic

Topic
topic消息主题,通过Topic对不同的业务消息进行分类。我的理解就是生产者消费者之间的中间件,让mq知道去消费什么。
Tag
消息标签,用来进一步区分某个Topic下的消息分类,消息从生产者发出即带上的属性
Topic和Tag的关系如下图所示
图片:
在这里插入图片描述

适用场景
到底什么时候该用Topic,什么时候该用Tag?
建议您从以下几个方面进行判断:
消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的Topic,无法通过Tag进行区分。
业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的Topic进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用Tag进行区分。
消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市24小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的Topic进行区分。
消息量级是否相当:有些业务消息虽然量小但是实时性要求高,如果跟某些万亿量级的消息使用同一个Topic,则有可能会因为过长的等待时间而“饿死”,此时需要将不同量级的消息进行拆分,使用不同的Topic。

总的来说,针对消息分类,您可以选择创建多个Topic,或者在同一个Topic下创建多个Tag。但通常情况下,不同的Topic之间的消息没有必然的联系,而Tag则用来区分同一个Topic下相互关联的消息,例如全集和子集的关系、流程先后的关系。
场景示例
以天猫交易平台为例,订单消息和支付消息属于不同业务类型的消息,分别创建Topic_Order和Topic_Pay,其中订单消息根据商品品类以不同的Tag再进行细分,例如电器类、男装类、女装类、化妆品类等被各个不同的系统所接收。
通过合理的使用Topic和Tag,可以让业务结构清晰,更可以提高效率。
消息过滤

做项目时遇到的难点

1.首先,对于这个多人联机对战的小游戏来说,这个多人联机模式肯定是我着重注意的功能。刚刚我在介绍这部分内容的时候,解释了我是通过同步几个事件也就是函数,来实现多人联机的对战的一个功能。相比于同步每一时刻的坐标来说,这样的方式会减轻一些服务器的压力。但是因为只同步事件,在子弹碰撞的时候由于网络延迟可能会导致三角函数计算会出现误差,可能会造成同一玩家在不同窗口的坐标有一些差异,会降低我们的游戏体验。然后我也是去做了一些思考以及参考了一些方法,我们在子弹击中的时候,同步一下attackee的位置,做一个补偿。这样的话,我的attack函数不止是说广播击中的信息,也是说我击中了你,你在我窗口的位置将会被同步给所有窗口,对之前的延迟造成的问题做一个补偿。

  1. 就是在具体实现过程中,会经常遇到各种各样的问题,各种各样的报错。在一开始的时候,我的调试bug的能力还比较弱。刚开始写前端的时候经常会有一些变量名写错之类的错误,一开始我也是点开F12控制台去看报错信息,定位一下位置。随着项目的进行,后期前端和后端一起写的时候,也会遇到各种问题,逐渐的我慢慢去学习了判断一下是前端还是后端的问题。比如点开f12 network响应的链接,然后看response返回的数据字段、值。一般如果说1.响应可能没有数据,我后端数据没有返回,可能是后端的原因 。2.响应中有数据但字段对不上了,可能是前端的代码有问题3.再或者说有数据但是不太对,可能需要看一下数据库有没有记录,没有的话可能是代码逻辑问题。做一些判断。之后可能会那么我会就是把一些回调的值resp console.log()打印出来,看一下和预期的一些差距,根据打印出来的值做一些分析和判断。去定位问题。也是在开发的过程中,就是经常写一部分代码要花很长时间去找错,我认识到了测试的一个重要性。所以在这个项目中我也去学习了一些测试的理论以及常用的工具。可能也是我希望找测开相关工作的一个原因吧。

redis和mysql

1.redis的基本知识

redis的数据结构

常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
字符串
String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。
底层实现:int和动态字符串
应用场景:缓存对象、计数、共享session信息、分布式锁
常见指令:SET key value、GET key、EXSITS key、DEL

List:
List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
底层实现:List 类型的底层数据结构是由双向链表或压缩列表实现的

应用场景:消息队列
1.如何满足消息保存需求 :List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。 List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了
2.如何处理重复的消息:给每一个消息有一个全局的id, 收到消息消费者对比这个id与处理过的id判断有没有被处理
3.保证消息的可靠性:List 有一个BRPOPLPUSH命令,从消息队列取走一个,就把这个消息插在另外一个list里备份一下

常见指令:LPUSH, RPOP, RPUSH, LPOP, BLPOP, BRPOP key [key…]timeout (如果没有的话自动阻塞timeout)

哈希
Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},…{fieldN,valueN}]。Hash 特别适合用于存储对象
底层实现:Hash 类型的底层数据结构是由压缩列表或哈希表实现的

应用场景:缓存对象,购物车
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
购物车: 以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素

常见指令:HSET key field value 、HGET key field、HMSET key field value [field value…]、HMGET key field [field …]

set:
Set 类型是一个无序并**唯一(非重复)**的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
特点:无序、不可重复、支持并交差等操作
底层实现:Set 类型的底层数据结构是由哈希表或整数集合实现的

应用场景:Set 类型比较适合用来数据去重和保障数据的唯一性
1.点赞
Set 类型可以保证一个用户只能点一个赞,这里举例子一个场景,key 是文章id,value 是用户id
SADD article:1 uid:1 # uid:1 用户对文章 article:1 点赞
SREM article:1 uid:1 # uid:1 用户取消对文章 article:1 点赞
2.共同关注
Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。
SADD uid:1 5 6 7 8 9
SADD uid:2 7 8 9 10 11
SINTER uid:1 uid:2 #uid:1 和 uid:2 共同关注的公众号
SDIFF uid:1 uid:2 # 给uid:2 推荐 uid:1 关注的公众号

Zset:
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。
底层实现:Zset 类型的底层数据结构是由压缩列表或跳表实现的

应用场景:
Zset 类型(Sorted Set,有序集合) 可以根据元素的权重来排序,我们可以自己来决定每个元素的权重值。比如说,我们可以根据元素插入 Sorted Set 的时间确定权重值,先插入的元素权重小,后插入的元素权重大。

在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用 Sorted Set。

常见指令:
ZADD key score member [[score member]…]
ZREM key member [member…] #删除
ZSCORE key member #返回有序集合key中元素member的分值

持久化

AOF日志

Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它
在这里插入图片描述
只会记录写操作命令,读操作命令是不会被记录的

三种写回策略
在「主进程阻塞」和「减少数据丢失」之间平衡
在这里插入图片描述
AOF重写机制
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大, AOF 日志文件过大就会带来性能问题,重启的时候,整个恢复过程会很慢。

Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件

重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后,将新的 AOF 文件覆盖现有的 AOF 文件。

AOF后台重写
重写的操作很耗时,所以重写的操作不能放在主进程里
Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的

子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
子进程带有主进程的数据副本(数据副本怎么产生的后面会说),这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。

RDB快照

RDB 文件的内容是二进制数据,AOF 文件的内容是操作命令
RDB 快照是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。

因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:

执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;

混合使用 AOF 日志和内存快照,也叫混合持久化。

使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。

Redis有三种集群模式,分别是:主从模式,哨兵模式,Cluster集群模式

主从复制(Master-Slave Replication):在主从复制模式中,一个Redis节点(即主节点)会将自己的数据复制到一个或多个从节点上。当主节点更新数据时,它会将更新操作同步到所有从节点上,从而保证数据的一致性。从节点可以提供读取数据的服务,从而减轻主节点的压力,同时也可以在主节点宕机时接替主节点继续提供服务。

哨兵模式(Sentinel):在哨兵模式中,多个Redis节点被组织成一个哨兵集群,其中一个节点作为主节点,其他节点作为从节点。哨兵节点会监控主节点的状态,当主节点宕机时,哨兵节点会自动将某个从节点提升为主节点,从而保证服务的可用性。哨兵模式还支持自动故障转移、配置更新等功能,提高了Redis集群的可靠性和可维护性。

Redis Cluster:Redis Cluster是Redis官方推出的一种分布式集群模式,它将多个Redis节点组成一个分布式集群,支持数据分片、自动故障转移、节点自动发现等功能。Redis Cluster的数据分片机制使得数据可以分散存储在不同的节点上,从而提高了系统的可扩展性和容错能力。

总之,以上三种集群模式都可以实现Redis的高可用和可扩展性,具体使用哪一种需要根据实际情况进行权衡。主从复制适合单机部署、读多写少的场景;哨兵模式适合高可用性要求较高的场景;Redis Cluster适合海量数据、高并发读写的场景。

用到redis的部分

这个项目里用到redis的部分有两个,一个是django_redis, 是在第三方登录的部分用的。作用是缓存一下授权码。
为了防止跨站请求伪造攻击,需要web应用和第三方之间约定一个暗号,就是接到一个code判断是不是从第三方过来的请求。然后就增加一个参数state,它可以设成一个8位随机数,然后发送到第三方,第三方会把这个参数值原封不动的返回给你,只要判断一下接收到的state和发送的state是不是一个state就可以了。这样的话别有用心的人就很难伪造一个8位随机数来攻击你。

这个state需要存下来用来判断请求和回调的一致性,我们可以存在redis里,cache.set(state,True, 7200)搞个有效期两个小时,两小时后它就会被自动删掉
然后我们后端函数receive_code收到第三方发来的code和state,我们判断是否这个state在内存里,if not cache.haskey(state)return redirect(‘index’),如果存在,我们再继续接下来的逻辑步骤

另一个是channels_redis, 在联机对战的时候,玩家的血量等信息需要动态的通信维护,需要做到实时所以对读写效率要求很高,需要用内存数据库redis来存;django本身也要存一下信息存到redis的话效率更高

if not cache.has_key(self.room_name): #如果没有这个房间
            cache.set(self.room_name, [], 3600)  # 有效期1小时

cache.set(self.room_name, players, 3600)  #当最后一个玩家创建完之后,整个对局会保存一个小时

用到数据库的地方

要把玩家的信息存在player表中并且把这个表给注册到后台管理界面,就可以看到用户的信息了。
增加一个查看历史的功能,把历史对战的信息存在一张表里,通过openid来关联player表

from django.db import models
from django.contrib.auth.models import User


class Player(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    photo = models.URLField(max_length=256, blank=True)
    openid = models.CharField(default="", max_length=50, blank=True, null=True)
    score = models.IntegerField(default=1500)

    def __str__(self):
        return str(self.user)

Django 模型使用自带的 ORM。

对象关系映射(Object Relational Mapping,简称 ORM )用于实现面向对象编程语言里不同类型系统的数据之间的转换。

ORM 解析过程:

1、ORM 会将 Python 代码转成为 SQL 语句。
2、SQL 语句通过 pymysql 传送到数据库服务端。
3、在数据库中执行 SQL 语句并将结果返回。

2.MySql相关 (索引)

mysql 索引

MySQL中的索引是一种数据结构,用于加快数据库查询操作的速度。它通过在存储数据的表上创建索引来提高查询的性能。
MySQL中有多种类型的索引,包括:

B-tree索引:这是MySQL中最常见的索引类型,它可以快速定位需要查找的数据。B-tree索引适用于所有数据类型,包括数字、字符串和日期等。

哈希索引:哈希索引使用哈希表存储键值对,它们适用于等值查找,但不适合范围查找或排序操作。

全文索引:全文索引是一种特殊的索引,它用于对文本数据进行全文搜索,例如搜索博客文章或产品说明。

空间索引:空间索引是一种特殊的索引,用于对空间数据进行查询,例如地图和位置数据。

在MySQL中,可以使用CREATE INDEX语句在表的一列或多列上创建索引。下面是创建索引的示例语句:

在单个列上创建索引:

CREATE INDEX idx_last_name ON customers (last_name);

在多个列上创建索引:

CREATE INDEX idx_last_first_name ON customers (last_name, first_name);

在这个示例中,创建了一个联合索引,使用last_name列和first_name列来定义索引。这个联合索引可以提高按照这两个列的组合进行搜索时的查询速度。

需要注意的是,索引的创建可能需要一定的时间,具体时间取决于表的大小和索引的复杂度。此外,索引会占用存储空间,因此需要权衡索引对性能的提升和存储成本的影响。

可以使用SHOW INDEX语句来查看表中的索引信息:

SHOW INDEX FROM customers;

这将列出customers表的所有索引信息,包括索引名称、列名、索引类型等。

使用索引查找数据的方式和普通查询类似,但是需要在查询语句中加上关键字"WHERE",并指定需要查询的列和相应的查询条件。MySQL会利用已经创建的索引来加速查询,减少查询所需的时间。

SELECT * FROM customers WHERE last_name = 'Smith';

这个查询语句将查询名为"customers"的表中所有姓为"Smith"的顾客信息。如果在"last_name"列上创建了索引,MySQL会利用该索引来快速查找相应的数据,提高查询效率。

需要注意的是,在使用索引查找数据时,应尽量避免使用"LIKE"、“NOT”、"OR"等复杂的操作符,因为这些操作符可能会降低查询效率,甚至使索引无法发挥作用。此外,应避免在索引列上使用函数,因为这会使索引失效。

在编写查询语句时,应尽量避免使用全表扫描,即不带任何查询条件的查询语句。全表扫描会使MySQL从头到尾扫描整个表,消耗大量的计算资源,导致查询速度变慢。如果需要查询所有数据,应考虑分页查询,每次只查询一部分数据,减少查询所需的时间。

  1. MySQL中有两种类型的索引:聚簇索引和非聚簇索引。

聚簇索引是将表中的数据行存储在与索引相同的B-tree中。换句话说,聚簇索引决定了表中的数据行的物理存储方式。每个表只能有一个聚簇索引,因为数据行只能按照一种方式进行排序和存储。一般情况下,聚簇索引是由表的主键创建的。

非聚簇索引与聚簇索引不同,它们将索引数据和表数据分开存储,因此它们不会影响表中数据行的物理存储方式。在非聚簇索引中,每个索引项包含一个指向实际数据行的指针,这意味着非聚簇索引需要多次查找才能检索出完整的数据行。

在MySQL中,一个表可以有多个非聚簇索引,但是对于任何给定的非聚簇索引,每个索引项只能引用一行数据。非聚簇索引通常是由非唯一索引和全文索引等创建的。

在实际使用中,聚簇索引对于频繁的查询和范围查询非常有效,因为数据行存储在B-tree的同一层级,因此可以更快地访问。非聚簇索引对于唯一性查询非常有效,因为每个索引项只引用一行数据,因此可以更快地检索出数据行。

索引的原理

数据库索引的原理是利用B树或B+树等数据结构来快速定位匹配条件的数据,以加快数据库查询的速度,并通过查询优化器来选择最佳的索引和执行计划。

通过合理创建和使用索引,可以显著提高数据库的查询性能。但同时,过多或不合理的索引可能会导致写入性能下降和资源浪费,因此在创建索引时需要仔细权衡和规划。

1.B树(或B+树)数据结构:大多数数据库系统使用B树或B+树作为索引的数据结构。B树是一种平衡的树状数据结构,它允许在对数时间log内进行查找、插入和删除操作。B树在数据库索引中的应用允许有效地组织和维护大量数据,使得查询性能保持在较高水平。

唯一性:数据库索引通常用于实现唯一性约束。通过在索引中强制唯一性,可以确保表中的某一列没有重复的值。

适合索引和不适合索引的情况

什么时候适合索引

1.频繁作为查询条件的列:如果某列经常用于WHERE子句或JOIN条件中,那么为该列创建索引可以显著提高查询速度。

2.外键列:外键列通常用于JOIN操作,为其创建索引可以加快关联表之间的查询。

3.唯一性约束:对于唯一性约束的列,数据库系统会自动为其创建索引,以确保数据的唯一性。

4.ORDER BY 和 GROUP BY 子句:当查询需要按照某列进行排序或分组时,为该列创建索引可以加快排序和分组的操作。

5.多表连接:对于经常进行多表连接的查询,为连接列创建索引可以提高连接操作的效率。

6.大表中的频繁查询列:在大表中,如果某个列经常用于查询而且查询效率不高,可以考虑为该列创建索引。

什么时候不适合用索引

数据量太小:如果数据表中的数据量非常小,例如只有几百条数据,那么使用索引的作用并不明显,反而会增加数据库的负担。

频繁更新的列:如果一个表中有一列数据需要经常更新,那么对该列创建索引可能会降低数据库的性能。因为每次更新数据都需要更新索引,这会导致索引的维护成本变高,甚至使索引失效。

包含大量重复值的列:如果一列数据中包含大量重复的值,那么在该列上创建索引并不能提高查询效率。因为重复值太多会使索引变得不稳定,导致MySQL不得不扫描大量数据才能找到符合条件的记录。

查询条件中使用了函数:如果查询条件中使用了函数,那么索引可能不会生效,因为函数会使查询条件变得复杂,导致MySQL无法利用索引进行优化。

经常进行范围查询:如果查询条件涉及到范围查询,例如使用"IN"、“>”、"<"等操作符,那么对于复杂的查询条件,MySQL可能需要扫描大量的数据才能找到符合条件的记录,此时使用索引可能不是最好的选择。

如果数据量大、查询频繁、查询条件简单且能够使用索引优化查询,那么创建索引可以提高数据库的性能;如果数据量小、查询不频繁、查询条件复杂且不适合使用索引优化查询,那么不要创建索引,以免增加数据库的负担。

数据库事务

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
事务用来管理 insert,update,delete 语句

事务的四个特点

一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

事务控制语句

MYSQL 事务处理主要有两种方法:
1、用 BEGIN, ROLLBACK, COMMIT来实现

BEGIN / START TRANSACTION 开始一个事务
ROLLBACK 事务回滚
COMMIT 事务确认
2、直接用 SET 来改变 MySQL 的自动提交模式:

SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交

身份认证,cookie session JWT

cookie session token JWT
为什么JWT方便做跨域

JSON Web Token (JWT) 相对于 Session 的跨域访问更加容易,是因为 Session 通常是基于 Cookie 实现的,而 Cookie 在跨域访问时会受到浏览器的同源策略限制。而 JWT 是基于 HTTP 请求头实现的,不会受到浏览器同源策略的限制。

在使用 Session 的情况下,如果用户在一个域名下登录了网站并生成了 Session,那么在跨域请求另一个域名下的网站时,由于 Cookie 受到浏览器同源策略的限制,该网站无法获取到 Session ID,从而无法获取用户的登录状态,也就无法完成授权等操作。这就需要通过设置 CORS 或者使用代理的方式来解决跨域访问的问题。

而在使用 JWT 的情况下,JWT 通常存储在 HTTP 请求头中,而不是在 Cookie 中。由于 HTTP 请求头不受同源策略的限制,因此在跨域请求时,请求头中的 JWT 会被传递到目标网站中,并可以被该网站解析和验证,从而完成授权等操作。

另外,由于 JWT 是基于 Token 实现的,它本身包含了用户身份信息、过期时间等信息,因此不需要像 Session 一样需要在服务器端存储用户状态信息,也更适合在分布式系统中使用。

Linux 常见面试题

Linux指令查看文件

1、cat
用于显示小文件的内容,或者在shell脚本里显示文件内容,不支持翻页。
cat /etc/fstab

#/etc/fstab
#Created by anaconda on Fri Jun 14 18:02:15 2019

可以显示/etc/fstab的内容,如果想显示行号,只要加上 cat -n参数即可
2、less
当文件比较大,cat查看时并不方便,因为cat不支持翻页,是一次性显示完的。
所以cat更适合在shell脚本中使用,让机器自动读取文件,而不适合人类来看。
所以当我们在看大文件时,一般会会用less命令
这个文件 /etc/man_db.conf有一百多行,我们用less来翻页查看
[root@localhost ~]# less /etc/man_db.conf
上下翻页,q键退出

3、head
head -n

4、tail
和head相对,tail是只显示末尾几行,比如我们想查看一个文件的更新情况,只要看最后几行就可以了
[root@localhost ~]# tail -1 /etc/fstab
/dev/mapper/centos-swap swap swap defaults 0 0

tail-n
tail也可以自动监测文件的更新情况,如果有更新,立即在屏幕显示,使用tail -F参数,注意,使用大写-F

5、wc
可以查看一个文件有多少行,多少单词,多少字节
[root@localhost ~]# wc /etc/fstab
19 62 504 /etc/fstab

这个文件有19行,62个单词,504个字节

6、grep
grep 关键词 文件路径

从一个文件中查找到某个关键词,并将包含该关键词的行显示出来,以地址配置文件为例
[root@localhost ~]# grep DNS /etc/sysconfig/network-scripts/ifcfg-eth0
DNS1=180.76.76.76

grep默认是区分大小写的,可以使用-i参数,忽略大小写
有时我们需要取反,就是显示出不包含某个词的的行,使用-v参数,就能发显示结果里没有dns一行了
[root@localhost ~]# grep -i -v dns /etc/sysconfig/network-scripts/ifcfg-eth0

如何查看进程A是否存活和占用的端口号

查看进程A是否存活

ps -aux | grep 进程名

ps -aux 将列出系统上的所有进程,
ps -ef 也是会列出进程信息
之后可以查看预期的进程是否启动,或者杀死指定的进程
“grep” 将从输出中过滤出包含进程名称的行。

查看进程占用的端口号

lsof -i -p | grep 进程名

lsof -i 将列出所有网络相关的文件,“-P” 将以数字形式显示端口号,“-n” 将以数字形式显示主机名

查看进程A的运行路径

readlink /proc/<pid>/exe

/proc//exe 是一个特殊的符号链接,指向该进程的可执行文件的实际路径
readlink 命令将读取该链接并显示它所指向的路径

Linux 给定一个端口 如何杀死该端口进程

  1. 查找进程的pid
sudo lsof -i:<port_number>
  1. kill掉进程
sudo kill <PID>
# 如果杀不死直接
sudo kill -9 <PID> #直接终止进程,而不会给进程发送终止信号

查看占用8080端口的进程

netstat -anp | grep 8080

免密连接的几种配置

1.公钥秘钥验证
ssh-keygen 生成一对公钥和私钥,使用 ssh-copy-id 命令将本地的公钥复制到远程主机的 authorized_keys 文件中。
2.ssh-agent:使用 ssh-agent 存储私钥,这样就不需要每次都输入密码。
3.NFS 共享:使用 NFS 共享允许远程主机读取本地文件系统,并且不需要密码。

常用的shell函数有哪些

read 函数:该函数用于从标准输入读取字符串。
printf 函数:该函数用于格式化输出字符串。
test 函数:该函数用于测试文件和字符串的类型、大小、权限等特征
echo 函数:该函数用于输出字符串到控制台
cut 函数:该函数用于从文本文件中删除指定的字段。
sed 函数:该函数用于在文本文件中进行替换、删除和插入操作
sort 函数:该函数用于对文本文件的内容进行排序

测试工作的核心竞争力有以下几个方面:

全面的技术能力:测试人员需要具备全面的技术能力,包括但不限于编程语言、测试工具、测试框架、数据库等方面的知识。只有这样才能更好地应对各种复杂的测试任务。

专业的测试知识:测试人员需要熟悉各种测试方法和技术,包括黑盒测试、白盒测试、性能测试、安全测试等,同时需要了解软件开发的基本原理和流程,以便能够更好地进行测试和沟通。

准确的分析能力:测试人员需要具备敏锐的分析能力,能够快速、准确地找出问题的根本原因,并提出解决方案。

团队合作能力:测试工作通常需要与开发人员、项目经理、产品经理等多个角色进行紧密的协作,测试人员需要具备出色的团队合作能力,能够与其他团队成员密切配合,共同完成项目。

用户体验意识:测试人员需要具备用户体验意识,能够从用户角度出发,对软件的各个方面进行评估和测试,以确保用户能够获得良好的使用体验。

持续学习和创新精神:测试人员需要具备持续学习和创新精神,能够不断学习和掌握新的测试技术和工具,并运用到工作中,不断提升测试工作的质量和效率。

为HR面准备的问题答案(对测试的理解)

  1. 对于第一份工作,你更看重的是什么
    对于第一份工作我最看重的是平台和学习机会吧,对于像淘宝这样的大的平台,工作中会学习到许多有用的知识,以及能接触像双十一这种大型活动,这种大的高并发的场景,对自己能力的提升会有很大的帮助。以及在这样的大的平台,身边会遇到很多优秀的人,他们身上应该有很多值得我去学习的地方。
    再一个应该是工作内容吧,打个比方吧,之前面试我的前辈介绍他们是做互动的业务场景的,比如淘宝人生,粑粑农场,盖楼养猫这样的活动,我自己平时就是挺喜欢玩这些东西的,比如粑粑农场我就合种成功过十几次。我应聘的是测试相关的工作嘛,在测试中用户体验也是一个非常重要的点,因为我是老用户么,我对淘宝这些产品的体验也比较深。并且我自己平时也尝试去写一些互动的联机对战小游戏,所以我应聘的工作我是非常喜欢的,这也是我的一个工作内容的考量吧。

  2. 对于测试工作的理解吧
    对于测试,我是这么理解的,测试是通过技术手段来验证我们的软件是不是满足使用的需求。我们的目的是为了减少缺陷,保障质量
    对于测试开发的理解,首先测开开发也是测试,是为保障我们软件质量的。其次,我们测试开发也需要在测试的过程中主动的去开发一些工具。作用有两点,一个是这些工具能够减轻人力负担,提升我们测试工作的效率。另一点是,我们人工去手工一条一条的执行测试用例,有时候会漏过一些情况,使用一些开发好工具的话可以保障不重不漏,提升测试工作的质量。

  3. 为什么要选择测试
    首先,我选择测试开发的原因有两点:1是在学习和做项目的过程中,我经常会遇到各种各样的问题,我需要定位和解决bug, 在这个过程中我学习了一些测试相关的基础知识和常用的一些工具,他们为我的项目开发节省了很多功夫,我也认识到了测试工作的重要性吧。2第二点是我个人的技术栈比较匹配吧。在研究生期间,我的研究方向也是机器学习深度学习相关的,对于python的使用频率比较高,一些测试的框架比如Selenium也是用python去做。同时我对Linux系统也比较熟悉,以及我自学了测试的基础知识。

  4. 对未来的前景和趋势怎么看
    首先我认为测试工作在未来将继续保持重要性,因为随着技术的不断发展和应用场景的不断扩大,软件质量和安全性的要求也会越来越高。我对测试工作趋势的一些看法:
    1自动化测试的普及:随着自动化测试技术的不断发展和成熟,越来越多的公司和项目用自动化测试工具来提高测试效率和准确性。未来,自动化测试将成为测试工作的主流方式,需要测试工程师具备自动化测试的技能和经验。
    2是AI和机器学习会在测试工作中更多的被使用。比如现在的计算机视觉的发展和成熟,也许我们可以用计算机视觉去做一些UI界面的测试,通过机器视觉代替人眼做这个测试的工作,能提高效率和准确性吧。再比如随着自然语言理解的进步,就比如说最近爆火的Chatgpt,可能帮助我们做一些文字相关的测试。这都会提升我们的工作效率。再比如说随着强化学习的落地,互动的业务场景抽象成了基于行为节点的有向图,我们可以用强化学习的AI来做这个有向图的路径构建问题,全面覆盖我们的测试用例,实现自动化,提高工作效率和质量。
    3 是我认为测试人员需要具有一些产品思维,不是说需求文档是怎么写我就怎么测,如果发现产品设计方面的缺陷我认为要有一双慧眼,及时发掘并沟通。

  5. 有没有遇到协作上的问题,是怎么解决的
    之前我在做实验室的项目的时候,经常是三个人或以上共同完成一个项目。因为每个人的经验技能不太一样,有些人能力更强一些,有些人可能处于了解项目的初级阶段。所以导致会有一些沟通问题吧,就是不同的人对于信息的理解可能会有些偏差,对项目的需求和实现方式可能会有不同的想法吧。也会导致任务的分配上面有些不合理的地方吧,有些能力强的人可能很快就干完了他负责的模块,但是刚入门的同学可能需要一些帮助才能完成任务。
    怎么解决的。第一点,我们一个团队,需要加强沟通,构建起彼此相互信任这样一个感觉,这样的话我们在具体的项目实现的过程中,团队成员之间的沟通会更加坦诚。不会说出现一些比如说我们新人刚入门可能有些障碍,他因为怕被指责而拖延进度。我们项目各个模块的进度能够比较顺利的对齐。如果一个人需要帮助的话,有能力的同学也会尽力的去帮助。我觉得这就是团队彼此沟通相互信任的一个好处。第二点的话,我觉得项目的负责人的作用应该凸显,更好的根据每个成员的情况去尽可能的合理的分配这样一个任务,在项目的推进过程中遇到一些问题也要即时的协调。第三点的话,我觉得构建一种意见交流以及问题反馈的机制比较重要吧。也就是说团队成员之间需要定期的交流意见和问题,这样的话有一些隐患才不会到最后才发现,当发现的时候可能需要更大的工作量去修复。以及问题的反馈和解决也要定期的交流沟通,为我们的项目指明下一步的方向。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值