6.584-Lab4A
6.584-LabA
- Homework
- Reference Code
- Reference Blog
通过作业提供的概览图可以看出整个系统的组成:用户 Clerk 会发出命令(Get、Put、Append)到每个 Service,每个 Service 接收到命令后向下传递到 RaftCode 层,由 RaftCode 层负责自己的“事情”(选举、生成log、提交Commit log、应用Apply log…)。 RaftCode 层将 Apply log通过“通道”传递到自己的 Service,Service 将Apply log的命令(Get、Put、Append)应用到自己的本地数据库db。
本次作业是实现 RaftCode 之上的“应用层”,主要是三个方面:
- Service 接收 Clerk发来的命令;
- Service 将接收的命令下放到自己的 RaftCode 层;
- RaftCode 层将自己提交Apply 的 log 返回给自己上层的 Service,Service 将接收到 RaftCode 已经 Apply log 应用到 数据库db;
文件包含的函数介绍
kvraft/common.go
包含 Clerk 与 Service 进行 RPC 的 Args、Reply 结构体。
PutAppendArgs & PutAppendReply:
由于 Put 和 Append 命令都包含一个 Key 和 Value,所以可以将 Put & Append 信息合并为同一个结构体。
Op
来区分 Put 和Append;
Identifier
:表示这个命令来自哪个 Clerk;
Seq
:表示这个命令来自 Clerk 的第几条命令;
Identifier
+Seq
共同构成了命令的唯一标号。
type PutAppendArgs struct {Key stringValue stringOp string // Op = "Put" or "Append"Identifier int64Seq uint64
}type PutAppendReply struct {Err Err
}
GetArgs & GetReply:
Get 命令只包含一个 Key
type GetArgs struct {Key stringIdentifier int64Seq uint64
}type GetReply struct {Err ErrValue string
}
kvraft/client.go
负责将 Clerk 的命令传递给 Service;根据接收到 Service 处理结果的信息,并做出相应的反应。
Clerk结构体的字段:
identifier
:会有多个 Clerk 并行向 Service 发送命令,为了区分 Clerk 要给一个身份标识;
leaderId
:记录当前为 Leader 的 Service,不用每次都要轮询去找 Leader Service.
seq
:为 Clerk 下条发送命令的编号
type Clerk struct {servers []*labrpc.ClientEnd // 所有的Serviceseq uint64 // 单调递增序列号identifier int64 // 标识clerkleaderId int // 记录leader的id
}
MakeClerk():
func MakeClerk(servers []*labrpc.ClientEnd) *Clerk {ck := new(Clerk)ck.servers = serversck.seq = 0ck.identifier = nrand()return ck
}
创建一个 Clerk,并将 ClerkID 初始化为一个唯一的id。
Get_Seq():
func (ck *Clerk) Get_Seq() (SendSeq uint64) {SendSeq = ck.seqck.seq += 1return
}
返回一个标号给当前的命令,并自增1做为下一条命令的标号。
Get(key string) string:
- 将 Get 包装为
GetArgs
通过 PRC 发送给 Service - 得到 Service 的回复
GetReply
Service的回复有几种情况:
2.1 接收的 Service 并不是 Leader 或者 是一个 过时的Leader,那么继续询问下一个 Service
2.2 当通道关闭(至于为什么会通道关闭,后面解释)或者处理超时都继续轮询这个 Service 发送命令
2.3 没有出现错误,则return reply.Value
(如果没有Key的话,会返回空字符串)
func (ck *Clerk) Get(key string) string {args := &GetArgs{Key: key, Identifier: ck.identifier, Seq: ck.Get_Seq()}for {reply := GetReply{}ok := ck.servers[ck.leaderId].Call("KVServer.Get", args, &reply)if !ok || reply.Err == ErrNotLeader || reply.Err == ErrLeaderOutDated { // 询问的server是follower or 过时的leader,就继续轮询下一个serverck.leaderId = (ck.leaderId + 1) % len(ck.servers)continue}switch reply.Err { // 当返回 通道关闭&操作超时 则继续轮询这个leadercase ErrChanClose:continuecase ErrHandleOpTimeOut:continuecase ErrKeyNotExist:return reply.Value // 不存在Key,那么Value就是默认零值--空字符串""}return reply.Value}
}
PutAppend():
同Get()
做同样处理。不过不会出现ErrKeyNotExist
这个错误,也没有返回值。
func (ck *Clerk) PutAppend(key string, value string, op string) {// Identifier:表示该Com来自哪个clerk 、Seq:表示来自第几个Cmd。 Identifier+Seq构成Cmd的唯一标识args := &PutAppendArgs{Key: key, Value: value, Op: op, Identifier: ck.identifier, Seq: ck.Get_Seq()}for {reply := PutAppendReply{} // 重试RPC时, 需要新建reply结构体, 重复使用同一个结构体将导致labgob报错ok := ck.servers[ck.leaderId].Call("KVServer.PutAppend", args, &reply)if !ok || reply.Err == ErrNotLeader || reply.Err == ErrLeaderOutDated {ck.leaderId = (ck.leaderId + 1) % len(ck.servers)continue}switch reply.Err {case ErrChanClose:continuecase ErrHandleOpTimeOut:continue}return}
}
kvraft/server.go
这个文件中主要实现的逻辑:
- Service 接收到命令后传递给 Raft ;
- Service 接收到 Raft 提交后的命令后 Apply 到本地数据库db中;
- 如果是 Leader 还肩负处理完之后通知 Clerk 的职责;
相关结构体:
type Op struct {OpType OpType // 操作类型Key string Val stringSeq uint64 // 该操作命令的Seq编号Identifier int64 // 发出该操作命令的Clerk的ID
}
type result struct { // 存储一个请求的序列号和结果LastSeq uint64Err ErrValue stringResTerm int // ResTerm记录commit被apply时的term 因为其可能与Start相比发生了变化, 需要将这一信息返回给客户端
}
type KVServer struct {mu sync.Mutexme intrf *raft.RaftapplyCh chan raft.ApplyMsgdead int32 // set by Kill()// Code HerewaiCh map[int]*chan result // 映射 startIndex->Ch 纪录等待commit信息的RPC handler的通道historyMap map[int64]*result // 映射 Identifier->*result 记录某clerk的最高序列号的请求的序列号和结果resultmaxraftstate int // snapshot if log grows this bigmaxLen intdb map[string]string
}
RPC Handler:Get() & PutAppend()
func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {_, isLeader := kv.rf.GetState()if !isLeader { // 访问的server不是leaderreply.Err = ErrNotLeaderreturn}opArgs := &Op{OpType: OpGet, Key: args.Key, Seq: args.Seq, Identifier: args.Identifier}res := kv.HandleOp(opArgs)reply.Err = res.Errreply.Value = res.Value
}
// Get和PutAppend都将请求封装成Op结构体, 统一给HandleOp处理
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {_, isLeader := kv.rf.GetState()if !isLeader {reply.Err = ErrNotLeaderreturn}opArgs := &Op{Key: args.Key, Val: args.Value, Seq: args.Seq, Identifier: args.Identifier}if args.Op == "Put" {opArgs.OpType = OpPut}if args.Op == "Append" {opArgs.OpType = OpAppend}res := kv.HandleOp(opArgs)reply.Err = res.Err
}
可以从代码中看到 Get()
和 PutAppend()
的逻辑基本相似:
- 先判断下层的 Raft 是否为 Leader,若不是那么就返回
ErrNotLeader
。因为在 Raft 层中,只有 Leader 能接收命令,由 Leader 通过“心跳”发送给 Follower。 - 将接收到的命令(Get、Put、Append)同一封装为
Op
结构体。 - 将封装命令的
Op
结构体传入HandleOp()
函数进一步处理并得到返回的结果。
HandleOp()
func (kv *KVServer) HandleOp(opArgs *Op) (res result) {startIndex, startTerm, isLeader := kv.rf.Start(*opArgs) // 这里调用Raft层,将Clerk的Cmd下传到Raftif !isLeader {return result{Err: ErrNotLeader, Value: ""}}kv.mu.Lock()newCh := make(chan result)kv.waiCh[startIndex] = &newCh // ApplyHandler 通过通道将Cmd的结果返回kv.mu.Unlock() // Start函数耗时较长, 先解锁defer func() {kv.mu.Lock()delete(kv.waiCh, startIndex)close(newCh)kv.mu.Unlock()}()select { // 管道多路复用的控制结构,同时监测多个管道是否可用case <-time.After(HandOpTimeOut):res.Err = ErrHandleOpTimeOutreturncase msg, success := <-newCh: // 取出ApplyHandler的结果if !success {// 通道已经关闭, 有另一个协程收到了消息 或 通道被更新的RPC覆盖res.Err = ErrChanClosereturn} else if success && msg.ResTerm == startTerm {res = msgreturn} else {// Cmd执行完传递回来的term与一开始传入Cmd建立log的term不一致,说明这个leader可能过期了res.Err = ErrLeaderOutDatedres.Value = ""return}}
}
在函数的第一行调用了 Raft中的 Start 函数kv.rf.Start(*opArgs)
,Start函数如下图:
可以看出,
start()
函数会接收一个命令,判断是否是 Leader,然后会将命令封装为Entry
插入 Raft 的 log 中,返回(这条命令在 log 中的全局下标,插入该条命令时的 Term,是否为 Leader)。
回到HandleOp
函数的逻辑:
- 判断 RaftCode 层是否为 Leader,若不是则返回
ErrNotLeader
- 利用插入的命令在 RaftCode 层的 log 中的下标索引映射一个通道,后面利用这个通道获取 Apply命令到本地后的结果
- 检查是否超时,若超时则返回
ErrHandleOpTimeOut
- 若在规定时间(2S)内接收到了
ApplyHandler
放到通道中的结果的话,就取出通道中的结果
4.1 要提前判断通道是否关闭。设想一下这种情况,有一个 RPC 信息已经创建了通道Ch1
,然后执行ApplyHandler
之后因为某种原因无法进行而“死掉”(可能是网络原因),Clerk 那边超时重发一个包含相同编号命令的 RPC 创建了通道Ch2
覆盖了之前的通道Ch1
。不对,覆盖不了之前的通道Ch1
哇,当两个 RPC 命令传递给 Raft 后返回的startIndex
一定不会相同,创建的通道就不会覆盖哇,不懂了,QAQ(有人懂这里通道为什么会提前关闭呢,请不吝赐教)。
4.2 如果msg.ResTerm != startTerm
表明已经上个 Leader 已经过期了,已经不属于上个 Term 了。
HandleOp
中的select
与switch
作用相似,不过select
是管道的多路复用,用于检测多个管道是否能用
ApplyHandler():
func (kv *KVServer) ApplyHandler() {for !kv.killed() {log := <-kv.applyCh // Raft层处理完负责的部分(选举、生成日志、Snapshot等),Raft将提交的Cmd通过通道应用到K/V的db(数据库)if log.CommandValid {op := log.Command.(Op) // 类型断言:检查变量是否为某种类型kv.mu.Lock()var res resultneedApply := false //判断这个log是否需要被再次应用到K/Vdbif hisMap, isexist := kv.historyMap[op.Identifier]; isexist {if hisMap.LastSeq == op.Seq { // 历史记录存在且Seq相同,直接返回之前的历史结果res = *hisMap} else if hisMap.LastSeq < op.Seq {needApply = true // 历史记录中的Cmd是之前的Cmd,而这个是更新的Seq的Cmd仍需要在db中创建}} else { // 历史db中没有该记录,需要创建needApply = true}_, isLeader := kv.rf.GetState()if needApply {// 在K/Vdb上执行log中的Cmdres = kv.DBExecute(&op, isLeader)res.ResTerm = log.SnapshotTerm// 更新历史的记录kv.historyMap[op.Identifier] = &res}if !isLeader { // kv.rf不是leader就处理下一个logkv.mu.Unlock()continue}// 是leader则还需要额外通知handler处理clerk回复ch, isexist := kv.waiCh[log.CommandIndex]if !isexist {// 接收端的通道已经被删除了并且当前节点是 leader, 说明这是重复的请求, 但这种情况不应该出现, 不然panic// Raft 层可能因为网络等某种原因,发送了两次 apply 同一个 log 的请求,第二次发现通道已关闭,就跳过处理下一个 applykv.mu.Unlock()continue}kv.mu.Unlock()func() {defer func() {if recover() != nil {// 如果这里有 panic,是因为通道关闭DPrintf("leader %v ApplyHandler 发现 identifier %v Seq %v 的管道不存在, 应该是超时被关闭了", kv.me, op.Identifier, op.Seq)}}()res.ResTerm = log.SnapshotTerm*ch <- res // 这里将结果通过通道返回给}()}}
}
逻辑:
- 取出 RaftCode 放入通道
applyCh
Apply 的 log,要保证取出 log 中的命令 Cmd 是有效的。 - 需要判断命令 Cmd 是否在本地数据库db应用过,如果
hisMap.LastSeq == op.Seq
表明之前执行过,直接返回保存的结果。如果不存在 或者 保存的hisMap.Seq < op.Seq
表明这是编号为op.Identifier
的 Clerk 新的 Cmd,均需要在本地数据库db中 Apply - 如果命令需要在本地数据库db中应用则调用函数
DBExecute
在本地数据库 apply 命令 - 如果 Service 是 Leader 的话还需要负责向 Clerk 通知在本地数据库 apply 的结果,如果是 Follower 的话就处理下一个 log 即可。
4.1 通过在HandleOp
中创建的通道返回结果,要先判断通道是否存在。Raft 层可能因为网络等某种原因,发送了两次 apply 同一个 log 的请求,第二次发现通道已关闭,就跳过处理下一个 apply
有关恢复
panic
的recover
函数的使用请跳转:Blog
相关文章:
6.584-Lab4A
6.584-LabA HomeworkReference CodeReference Blog 通过作业提供的概览图可以看出整个系统的组成:用户 Clerk 会发出命令(Get、Put、Append)到每个 Service,每个 Service 接收到命令后向下传递到 RaftCode 层,由 RaftC…...
语义版本控制
注意: 本文内容于 2024-11-27 22:25:05 创建,可能不会在此平台上进行更新。如果您希望查看最新版本或更多相关内容,请访问原文地址:语义版本控制。感谢您的关注与支持! 由于自己平时喜欢写点小玩意,自然而…...
深入理解HTML基本结构:构建现代网页的基石
深入理解HTML基本结构:构建现代网页的基石 在数字时代,HTML(超文本标记语言)是构建和设计网页的基础。了解HTML的基本结构对于任何希望掌握网页开发的人来说至关重要。本文将详细介绍HTML文件的基本骨架,包括其核心标…...
一体化数据安全平台uDSP 入选【年度创新安全产品 TOP10】榜单
近日,由 FreeBuf 主办的 FCIS 2024 网络安全创新大会在上海隆重举行。大会现场揭晓了第十届 WitAwards 中国网络安全行业年度评选获奖名单,该评选自 2015 年举办以来一直饱受赞誉,备受关注,评选旨在以最专业的角度和最公正的态度&…...
【机器学习】机器学习的基本分类-监督学习(Supervised Learning)
监督学习是一种通过已有的输入数据(特征)和目标输出(标签)对模型进行训练的机器学习方法,旨在学到一个函数,将输入映射到正确的输出。 1. 监督学习概述 监督学习需要: 输入数据(特…...
Oracle之提高PLSQL的执行性能
目录 1、SQL解析详解 2、演示示例 3、启用Oracle跟踪事件 4、查看改造后SQL性能对比结果 更多技术干货,关注个人博客吧 1、SQL解析详解 SQL解析是数据块处理SQL语句不可缺少的步骤,是在解析器中执行的。将SQL转换成数据库可以执行的低级指令。 SQL解析分为硬解析和软…...
[VSCode] vscode下载安装及安装中文插件详解(附下载文件)
前言 vscode 链接:https://pan.quark.cn/s/3acbb8aed758 提取码:dSyt VSCode 是一款由微软开发且跨平台的免费源代码编辑器;该软件支持语法高亮、代码自动补全、代码重构、查看定义功能,并且内置了命令行工具和Git版本控制系统。 …...
PHP中类名加双冒号的作用
在 PHP 中,类名加双冒号(::) 是一种用于访问类的静态成员和常量的语法。它也可以用来调用类的静态方法和访问 PHP 的类相关关键词(如 parent、self 和 static)。以下是详细的解释和用法。 1. 用途概述 :: 被称为作用域…...
前端编程训练 异步编程篇 请求接口 vue与react中的异步
文章目录 前言代码执行顺序的几个关键点接口请求vue与react中的异步vue中的异步react的state修改异步 前言 本文是B站三十的前端课的笔记前端编程训练,异步编程篇 代码执行顺序的几个关键点 我们可以理解为代码就是一行一行,一句一句是执行(定义变量&…...
【kafka03】消息队列与微服务之Kafka 读写数据
Kafka 读写数据 参考文档 Apache Kafka 常见命令 kafka-topics.sh #消息的管理命令 kafka-console-producer.sh #生产者的模拟命令 kafka-console-consumer.sh #消费者的模拟命令 创建 Topic 创建topic名为 chen,partitions(分区)为3࿰…...
【分布式系统】唯一性ID的实现
1、UUID(通用唯一标识符) 1、UUID本身 一种用于标识信息的标准化方法。一个128位的数字,常表示为32个十六进制数字,以连字符分隔成五组:8-4-4-4-12。 版本: UUID有不同的版本,最常见的是基于时…...
哪里能找到好用的动物视频素材 优质网站推荐
想让你的短视频增添些活泼生动的动物元素?无论是搞笑的宠物瞬间,还是野外猛兽的雄姿,这些素材都能让视频更具吸引力。今天就为大家推荐几个超实用的动物视频素材网站,不论你是短视频新手还是老手,都能在这些网站找到心…...
SRAM芯片数据采集解决方案
SRAM芯片数据采集解决方案致力于提供一种高效、稳定且易于操作的方法,以确保从静态随机存取存储器SRAM芯片中准确无误地获取数据。 这种解决方案通常包括硬件接口和软件工具,它们协同工作,以实现对SRAM芯片的无缝访问和数据传输。 在硬件方…...
【贪心算法第七弹——674.最长连续递增序列(easy)】
目录 1.题目解析 题目来源 测试用例 2.算法原理 3.实战代码 代码分析 1.题目解析 题目来源 674.最长递增子序列——力扣 测试用例 2.算法原理 贪心思路 3.实战代码 class Solution { public:int findLengthOfLCIS(vector<int>& nums) {int n nums.size();in…...
[AI] 知之AI推出3D智能宠物:助力语言学习与口语提升的新选择
Hello! 知之AI官网 [AI] 知之AI推出3D智能宠物:助力语言学习与口语提升的新选择 随着人工智能技术的飞速发展,虚拟助手和智能设备不断进入我们的生活。近日,知之AI重磅推出了一款创新产品——3D智能宠物。这一产品不仅具备多国语言交流能力&…...
Android 14之HIDL转AIDL通信
Android 14之HIDL转AIDL通信 1、interface接口1.1 接口变更1.2 生成hidl2aidl工具1.3 执行hidl2aidl指令1.4 修改aidl的Android.bp文件1.5 创建路径1.6 拷贝生成的aidl到1和current1.7 更新与冻结版本1.8 编译模块接口 2、服务端代码适配hal代码修改2.1 修改Android.bp的hidl依…...
【R库包安装】R库包安装总结:conda、CRAN等
【R库包安装】R studio 安装rgdal库/BPST库 R studio 安装rgdal库解决方法 R studio 安装BPST库(github)解决方法方法1:使用devtools安装方法2:下载安装包直接在Rstudio中安装 参考 基础 R 库包的安装可参见另一博客-【R库包安装】…...
学习PMC要不要去培训班?
在当今快速变化的商业环境中,PMC作为供应链管理的核心环节之一,其重要性日益凸显。PMC不仅关乎产品的物料计划、采购、库存控制及物流协调,还直接影响到企业的生产效率、成本控制以及市场竞争力。面对这一专业领域的学习需求,许多…...
前端 用js封装部分数据结构
文章目录 Stack队列链表Setset 用来数组去重set用来取两个数组的并集set用来取两个数组的交集set用来取两个数组的差集 字典 Stack 栈,先进后出,后进先出。用数组来进行模拟,通过push存入,通过pop取出。 class Stack {// 带#表示…...
cocoscreator-doc-TS:目录
cocoscreator-doc-TS-脚本开发-访问节点和组件-CSDN博客 cocoscreator-doc-TS-常用节点和组件接口-CSDN博客 cocoscreator-doc-TS-脚本开发-创建和销毁节点-CSDN博客 cocoscreator-doc-TS-脚本开发-加载和切换场景-CSDN博客 cocoscreator-doc-TS-脚本开发-获取和设置资源-CS…...
理解Java集合的基本用法—Collection:List、Set 和 Queue,Map
本博文部分参考 博客 ,强烈推荐这篇博客,写得超级全面!!! 图片来源 Java 集合框架 主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合(单列…...
IOC容器实现分层解耦
文章开始之前,先引入软件开发的两个名词:耦合和内聚。耦合是指:衡量软件中各个层(三层架构)/各个模块的依赖关联程度;内聚是指:软件中各个功能模块内部的功能联系。三层架构中Controller、Servi…...
Flutter 共性元素动画
在 Flutter 中,共性元素动画(Shared Element Transitions)用于在页面导航或组件切换时创建视觉上更流畅和连贯的动画效果。这种动画可以使用户感受到两个界面之间的“物理联系”,比如图片从缩略图到全屏的扩大效果。 前置知识点整…...
K8s内存溢出问题剖析:排查与解决方案
文章目录 一、背景二、排查方案:1. 可能是数据量超出了限制的大小,检查数据目录大小2. 查看是否是内存溢出2.1 排查数据量(查看数据目录大小是否超过limit限制)2.2 查看pod详情发现问题 三、解决过程 一、背景 做redis压测过程中…...
乌班图单机(不访问外网)部署docker和服务的方法
面向对象:Ubuntu不能访问外网的机子,部署mysql、redis、jdk8、minio 过程: 1、安装docker(照着图去这里找对应的下载下来https://download.docker.com/linux/static/stable/),将7个docker官网下载的文件下载下来后,传上去服务器随便一个文件夹或者常用的opt或者/usr/lo…...
使用 pycharm 新建使用 conda 虚拟 python 环境的工程
1. conda 常见命令复习: conda env list // 查看 conda 环境列表 conda activate xxxenv // 进入指定 conda 环境2. 环境展示: 2.1. 我的物理环境的 Python 版本为 3.10.9: 2.2. 我的 conda 虚拟环境 env_yolov9_python_3_8 中的 pyth…...
Docker的save和export命令的区别,load和import的区别 笔记241124
Docker的save和export命令的区别,load和import的区别 解说1: Docker的save和export命令,以及load和import命令,在功能和使用场景上存在显著的区别。以下是对这两组命令的详细对比和解释: Docker save和export命令的区别 使用方式和目的&am…...
通俗理解人工智能、机器学习和深度学习的关系
最近几年人工智能成为极其热门的概念和话题,可以说彻底出圈了。但人工智能的概念在1955年就提出来了,可以说非常古老。我在上小学的时候《科学》课本上就有人工智能的概念介绍,至今还有印象,但那些年AI正处于“寒冬”,…...
使用 pycharm 新建不使用 python 虚拟环境( venv、conda )的工程
有时候我们发现一个好玩的 demo,想赶快在电脑上 pip install 一下跑起来,发现因为 python 的 venv、conda 环境还挺费劲的,因为随着时间的发展,之前记得很清楚的 venv、conda 的用法,不经常使用,半天跑不起…...
【大数据学习 | Spark-SQL】SparkSQL读写数据
我们使用sparksql进行编程,编程的过程我们需要创建dataframe对象,这个对象的创建方式我们是先创建RDD然后再转换rdd变成为DataFrame对象。 但是sparksql给大家提供了多种便捷读取数据的方式。 //原始读取数据方式 sc.textFile().toRDD sqlSc.createDat…...
wordpress增加网址大全/百度刷搜索词
在ActiveMQ 解压缩后的目录如下: 各个目录说明如下: bin:ActiveMQ的启动脚本conf:ActiveMQ的所有配置文件data:日志文件及持久性消息数据docs:ActiveMQ官方文档examples:ActiveMQ官方提供的…...
wordpress定时发布插件/如何优化seo
每逢双11,无论智慧物流、自营物流,还是第三方物流,菜鸟、苏宁、四通一达纷纷招兵买马。随着今年双11的临近,种种迹象显示,今年快递行业双11备战提前触发。“高薪急聘、包吃住”,一些招聘网站中,…...
什么是网站站点建设介绍/沙洋县seo优化排名价格
如果参数传递的是文件路径:我们需要将文件的路径用encodeURIComponent(obj.url)转化一下即可成功解决上述异常 "<a href\"<%path%>/paperManager/toDownFile.do?filepath"encodeURIComponent(obj.url)"\">附件下载</a>…...
dw5做简单的企业网站/百度推广话术全流程
Django的配置文件setting.py用于配置整个网站的环境和功能,核心配置必须有项目密钥配置、域名访问权限、App列表、中间件、资源文件、模板配置、数据库的连接方式。 1、基本配置信息 一个简单的项目必须具备的基本配置信息有:项目路径、密钥配置、域名…...
wordpress古腾堡编辑器如何使用/网络营销推广的方式
文章转载自:http://www.hack520.com/169.html RAID 技术相信大家都有接触过,尤其是服务器运维人员,RAID 概念很多,有时候会概念混淆。这篇文章为网络转载,写得相当不错,它对 RAID 技术的概念特征、基本原理…...
崇明建设镇政府工作网站/搜索引擎优化排名技巧
数据类型分为两种基础类型和引用类型: 1、基础类型:像Number、String、Boolean等这种为基本类型 2、引用类型:Object和Array 浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的…...