来源: Sql Server2008-读写分离 – 孤狼灬 – 博客园
.Net微服务实战之技术架构分层篇 - 陈珙 - 博客园
来源: .Net微服务实战之技术架构分层篇 – 陈珙 – 博客园
一拍即合
上一篇《.Net微服务实战之技术选型篇》,从技术选型角度讲解了微服务实施的中间件的选择与协作,工欲善其事,必先利其器,中间件的选择是作为微服务的基础与开始,也希望给一直想在.Net入门微服务的同行有一个很好的方向。在此篇重新整理了一下整个微服务项目的demo,希望对有需要的朋友起到一定的帮助:https://github.com/SkyChenSky/Sikiro
那么我在公司实施微服务的时候,也不是一拍脑袋想上就上的。刚入职公司的时候才3、4个人,产品给到我的规划只有一个很简单的系统,包含权限、客服IM、内容管理三个模块,我当时想着优先证明我们的开发能力和效率,于是使用简单的单体架构不到三个星期项目就完成了。产品在我们开发的期间把整个项目的规划和平台系统的划分给梳理了一遍,终于让我有一个很明确的技术实施方向,同时公司的人力成本预算也批了下来开始进行团队扩招。
于是我与老领导商量了一下,在现在这个情况,无论业务还是团队都具有使用微服务架构的可操作性,再采用部分DevOps的思想给与微服务实施的支持,能顺利的实施落地微服务问题不大。我们俩讨论了一番,我有良好的微服务技术储备,他有很好的运维支撑,就这样咱两达成了共识。于是我着手翻出了收藏已久的微服务中间件、架构分层、服务拆分的资料,从此开始了我的微服务实施之路。
PS:我们讨论实施微服务的时候除了以上冠冕堂皇的理由之外,其实还存有一点私心,就是现在企业招聘很多需要有实施微服务经验的人才,但是80%的项目和同行又是没有这样的实施必要与经验,这就是鸡生蛋和蛋生鸡的问题。我毫无隐瞒的说出我们的私心并不是怂恿大家冒着风险去实施,而是希望大家通过分析现在团队的组织架构、技术储备、业务架构,在条件允许的情况下满足您的小小要求,微服务虽不是银弹,但我们也需要成长。
架构思维
抽象是作为架构思维的核心,使我们站在大局观察,屏蔽细节;这系统划分哪几个模块?模块之间的如何协作的?抽象又可以衍生出两种思想划分与协作。
划分的目的是为了定责与拆分,定责不是交通事故的定责而是划定职责,明确模块的使用场景,应该被什么依赖?应该依赖什么?拆分其实就是分而治之的思想,把一个复杂的大问题拆分成一个个简单而小的问题,化繁为简逐个击破自然就迎刃而解。
协作的目的是整合划分好的模块,被拆分的模块如果无法整合到一起,拆分则失去了他原有的意义。
不谋而合
技术服务于架构,架构服务于业务,业务服务于商务。所以有明确的业务蓝图才可以很好的规划架构方向;选择好合适的技术才能很好的支撑架构。此时我们开始着手实施微服务,然而在实施时我们还会考虑一个比较核心点,究竟如何微?粒度究竟到什么程度?怎么明确依赖关系?大家或多或少都会听说身边同行有实施微服务的失败案例:拆分粒度过细导致系统复杂度过高;拆分粒度太粗又没达到微服务该有的效果等。那么是否在业界有一套科学的指导方法论?我认为是有的,DDD战略设计与分层架构。
埃里克、埃文斯在2004年发表了《领域驱动设计》一书的,此后一直是雷声大雨点小,在2014年软件教父马丁花给微服务一个全面描述,让它走向一个高潮后,DDD终于赢来了他的春天。为什么说DDD适合微服务呢?DDD是一种通过划分业务边界,将复杂的业务领域简单化的设计思想,也就是化繁为简。为什么在上文重点强调DDD战略设计?DDD分为战略设计与战术设计。
战略设计
主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的界限上下文,界限上下文可以作为微服务设计的参考边界。
战术设计
主要从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,例如我们常讨论的聚合根、实体、值对象、领域服务等代码逻辑的设计与实现。
从以上两点的描述可以看出,战略设计从业务视角出发,而架构服务于业务,两者都需要从业务出发,DDD战略设计与微服务都有同样的设计思想:分而治之、化繁为简,那么战略设计的思想完全可以作为微服务架构设计的指导思想,此时此刻此场景不谋而合。
分层&切割
也可以叫N层架构(N>=2),其实本质在于划分职责、隔离关注点,保证各层之间的差异足够清晰,边界足够明显,其特点自顶向下依赖,逐层传递。
横向拆分(横向分层)
首先我按照分层架构的思想以纵向维度拆分,通俗点就是按照自定向下的按照各层职责进行分解,主要共分5层,UI层、聚合API服务层、基础业务API服务层、基础设施层、数据库层。
调用链路自顶往下,用户–>UI–>API网关–>聚合API服务–>Consul+Consul Template+Nginx–>业务API服务–>数据库
UI层
依赖于聚合API服务层,操作与接口11对应,主要负责可见即可得的工作:数据展示、交互动画等。
入站API网关
主要负责聚合API服务层内外网隔离、入站规则控制,防止外部大流量冲垮内部服务。
聚合API服务层
被UI层依赖,依赖于基础业务API服务层,主要负责基础业务API服务层的接口的逻辑组合,不直连数据库,可通过API网关暴露给UI层调用。
注册服务中心
记录基础业务API服务层的服务IP列表,内网使用,衔接聚合API服务层与基础业务API服务层。
基础业务API服务层,
被聚合API服务层依赖,依赖于数据库层,可做具体的数据库读写处理,内网使用,同层服务之间不互相依赖引用。
数据库层
包括非关系型数据库与关系型数据库。
基础设施服务层
可被所有层都依赖,如果被UI层依赖则通过API网关暴露,如果被内网服务依赖则通过注册发现,可直连数据库。
出站API网关
主要负责基础设施服务层内外网隔离,转发第三方开放API请求,出站规则控制,防止被无法把控的第三方服务而拖垮内部服务。
纵向拆分(纵向切割)
接下来,我们可以通过DDD进行服务的切割,通俗点描述就是将同一个较大的服务拆解成为多个较小的服务。
那么究竟要根据什么样的信息和过程进行拆解呢?有一个工作坊叫作”事件风暴“,事件风暴是一个从拆解到整合的过程,过程中需要领域专家(需求提出者)和技术实践者协作完成领域建模。
一般采用场景分析和用例分析尽可能分解出领域对象(实体、命令、事件),可以从交流的过程中提取出领域专家(需求提出者)口中的名词、动作、触发事件等,这是一个拆解的过程。
将以上沟通后的结果进行重新梳理,寻找他们的关系进行汇聚,形成聚合与界限上下文,这就是一个整合的过程。
一个微服务粒度可以粗与界限上下文一致,粒度可以细化到一个聚合。
举个例子:
我们平台拥有三种不同业务领域的系统:客户中心、企业管理系统、内部管理系统。
那么,聚合API服务层则拥有客户系统API服务、企业管理系统API服务,内部管理系统API服务。
客户中心的拥有客户信息管理、支付、订单管理等业务模块。
企业管理系统拥有订单管理、权限管理、支付、仓储等业务模块。
内部管理系统拥有权限管理、报表、账户管理等业务模块。
所有系统涉及到自定义订单号、消息推送等业务。
从以上得知,核心域包括仓储、订单业务、客户信息。通用域包括权限管理、账户认证、支付模块、消息推送等。支撑域包括自定义订单号。
因此基础业务API层可以划分:仓储API服务、订单API服务、客户API服务、权限API服务、认证API服务,支付API服务。
基础设施API层可以划分:ID发号API服务,消息推送API服务。
后来多次跟产品经理沟通后得知,仓储服务在某个场景下需要把修改订单服务的状态,那么这里有个触发事件,而且是跨微服务的,因此引入了基于消息的最终一致性的分布式事务进行解决。
如果随着业务继续扩大,团队人数增多,则可以更加的细分,例如仓储拆分成快运、集运等。支付拆分成微信支付、支付宝等。
项目示例
上一篇《.Net微服务实战之技术选型篇》我整理了我们公司使用的框架开源到了github,这次我拿了部分业务项目作为示例并上传了。
https://github.com/SkyChenSky/Sikiro
首先想说明几点:
1.这个不是标准,只是针对我们公司情况取舍后的结果,每个公司的业务有复杂有简单大家视情况完善自己的项目。
2.为了保护公司原有的业务隐私,我做了部分逻辑的删除,所以大家如果看到不完整的逻辑是正常现象。
3.希望大家把思维放高,不要死抠细节,求同存异。
4.代码在原有的基础上修改了名称和引用路径会有变化,如果有问题随时在评论和github反馈给我。
作 者: 陈珙
出 处:http://www.cnblogs.com/skychen1218/
关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是作者坚持原创和持续写作的最大动力!
.Net微服务实战之Kubernetes的搭建与使用 - 陈珙 - 博客园
来源: .Net微服务实战之Kubernetes的搭建与使用 – 陈珙 – 博客园
系列文章
前言
说到微服务就得扯到自动化运维,然后别人就不得不问你用没用上K8S。无论是概念上还是在实施搭建时,K8S的门槛比Docker Compose、Docker Swarm高了不少。我自己也经过了多次的实践,整理出一套顺利部署的流程。
我这次搭建花了一共整整4个工作实践与一个工作日写博客,中间有一个网络问题导致reset了集群重新搭了一次,完成后结合了Jenkins使用,还是成就感满满的。如果对大家有用,还请点个推荐与关注。
基本概念
Kubectl
kubectl用于运行Kubernetes集群命令的管理工具,Kubernetes kubectl 与 Docker 命令关系可以查看这里
http://docs.kubernetes.org.cn/70.html
Kubeadm
kubeadm 是 kubernetes 的集群安装工具,能够快速安装 kubernetes 集群,相关命令有以下:
kubeadm init kubeadm join
Kubelet
kubelet是主要的节点代理,它会监视已分配给节点的pod,具体功能:
- 安装Pod所需的volume。
- 下载Pod的Secrets。
- Pod中运行的 docker(或experimentally,rkt)容器。
- 定期执行容器健康检查。
Pod
Pod是Kubernetes创建或部署的最小(最简单)的基本单位,一个Pod代表集群上正在运行的一个进程,它可能由单个容器或多个容器共享组成的资源。
一个Pod封装一个应用容器(也可以有多个容器),存储资源、一个独立的网络IP以及管理控制容器运行方式的策略选项。
Pods提供两种共享资源:网络和存储。
网络
每个Pod被分配一个独立的IP地址,Pod中的每个容器共享网络命名空间,包括IP地址和网络端口。Pod内的容器可以使用localhost相互通信。当Pod中的容器与Pod 外部通信时,他们必须协调如何使用共享网络资源(如端口)。
存储
Pod可以指定一组共享存储volumes。Pod中的所有容器都可以访问共享volumes,允许这些容器共享数据。volumes 还用于Pod中的数据持久化,以防其中一个容器需要重新启动而丢失数据。
Service
一个应用服务在Kubernetes中可能会有一个或多个Pod,每个Pod的IP地址由网络组件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象。
Kubernetes ServiceTypes 允许指定一个需要的类型的 Service,默认是 ClusterIP 类型。
Type 的取值以及行为如下:
- ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。
- NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
- LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
- ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。
其他详细的概念请移步到 http://docs.kubernetes.org.cn/227.html
物理部署图
Docker-ce 1.19安装
在所有需要用到kubernetes服务器上安装docker-ce
卸载旧版本 docker
yum remove docker docker-common docker-selinux dockesr-engine -y
yum upgrade -y
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum makecache fast yum install docker-ce-19.03.12 -y
vim /etc/docker/daemon.json { "exec-opts": ["native.cgroupdriver=systemd"], "registry-mirrors" : [ "http://ovfftd6p.mirror.aliyuncs.com", "http://registry.docker-cn.com", "http://docker.mirrors.ustc.edu.cn", "http://hub-mirror.c.163.com" ], "insecure-registries" : [ "registry.docker-cn.com", "docker.mirrors.ustc.edu.cn" ], "debug" : true, "experimental" : true }
启动服务
systemctl start docker systemctl enable docker
安装kubernetes-1.18.3
所有需要用到kubernetes的服务器都执行以下指令。
cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF
yum install kubeadm-1.18.3 kubectl-1.18.3 kubelet-1.18.3
启动kubelet
systemctl enable kubelet systemctl start kubelet
vim /etc/profile
export KUBECONFIG=/etc/kubernetes/admin.conf
执行命令使其起效
source /etc/profile
初始化k8s集群
在master节点(server-a)进行初始化集群
开放端口
firewall-cmd --permanent --zone=public --add-port=6443/tcp firewall-cmd --permanent --zone=public --add-port=10250/tcp firewall-cmd --reload
vim /etc/fstab #注释swap那行 swapoff -a
设置iptables规则
echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables echo 1 > /proc/sys/net/bridge/bridge-nf-call-ip6tables
初始化
kubeadm init --kubernetes-version=1.18.3 --apiserver-advertise-address=192.168.88.138 --image-repository registry.aliyuncs.com/google_containers --service-cidr=10.10.0.0/16 --pod-network-cidr=10.122.0.0/16 --ignore-preflight-errors=Swap
pod-network-cidr参数的为pod网段:,apiserver-advertise-address参数为本机IP。
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get node kubectl get pod --all-namespaces
安装flannel组件
在master节点(server-a)安装flannel组件
找个梯子下载kube-flannel.yml文件
https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
下载不了也没关系,我复制给到大家:
先拉取依赖镜像
docker pull quay.io/coreos/flannel:v0.12.0-amd64
把上面文件保存到服务器然后执行下面命令
kubectl apply -f kube-flannel.yml
安装dashboard
在master节点(server-a)安装dashboard组件
继续用梯子下载recommended.yml文件
https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.3/aio/deploy/recommended.yaml
没梯子的可以复制下方原文件
spec: type: NodePort ports: - port: 443 targetPort: 8443 nodePort: 30221 selector: k8s-app: kubernetes-dashboard
第137行开始,修改账户权限,主要三个参数,kind: ClusterRoleBinding,roleRef-kind: ClusterRole,roleRef-name: cluster-admin
--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: k8s-app: kubernetes-dashboard name: kubernetes-dashboard namespace: kubernetes-dashboard roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: kubernetes-dashboard namespace: kubernetes-dashboard ---
保存到服务器后执行以下命令
kubectl apply -f recommended.yaml
等待一段时间启动成功后,https://ip+nodePort,查看UI
Token通过下面指令获取
kubectl -n kubernetes-dashboard get secret kubectl describe secrets -n kubernetes-dashboard kubernetes-dashboard-token-kfcp2 | grep token | awk 'NR==3{print $2}'
加入Worker节点
在server-b与server-c执行下面操作
把上面init后的那句join拷贝过来,如果忘记了可以在master节点执行下面指令:
kubeadm token list openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
通过返回的数据拼装成下面指令
kubeadm join 192.168.88.138:6443 --token 2zebwy.1549suwrkkven7ow --discovery-token-ca-cert-hash sha256:c61af74d6e4ba1871eceaef4e769d14a20a86c9276ac0899f8ec6b08b89f532b
查看节点信息
kubectl get node
部署Web应用
在master节点(sever-a)执行下面操作
部署应用前建议有需要的朋友到【.Net微服务实战之CI/CD】看看如何搭建docker私有仓库,后面需要用到,搭建后私有库后执行下面指令
kubectl create secret docker-registry docker-registry-secret --docker-server=192.168.88.141:6000 --docker-username=admin --docker-password=123456789
docker-server就是docker私有仓库的地址
下面是yaml模板,注意imagePullSecrets-name与上面的命名的一致,其余的可以查看yaml里的注释
把yaml文件保存到服务器后执行下面命令
kubectl create -f testdockerswarm.yml
整个搭建部署的过程基本上到这里结束了。
访问
可以通过指令kubectl get service得到ClusterIP,分别在server-c和sever-b执行curl 10.10.184.184
也可以通过执行kubectl get pods -o wide得到pod ip,在server-c执行curl 10.122.2.5 和 server-b执行curl 10.122.1.7
也可以在外部访问 server-c和server-b的 ip + 31221
如果节点有异常可以通过下面指令排查
journalctl -f -u kubelet.service | grep -i error -C 500
如果Pod无法正常running可以通过下面指令查看
kubectl describe pod testdockerswarm-deployment-7bc647d87d-qwvzm
作 者: 陈珙
出 处:http://www.cnblogs.com/skychen1218/
关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是作者坚持原创和持续写作的最大动力!
.NET Core-全局性能诊断工具 - chaney1992 - 博客园
来源: .NET Core-全局性能诊断工具 – chaney1992 – 博客园
前言:
现在.NET Core 上线后,不可避免的会出现各种问题,如内存泄漏、CPU占用高、接口处理耗时较长等问题。这个时候就需要快速准确的定位问题,并解决。
这时候就可以使用.NET Core 为开发人员提供了一系列功能强大的诊断工具。
接下来就详细了解下:.NET Core 全局诊断工具
- dotnet-counters
- dotnet-dump
- dotnet-gcdump
- dotnet-trace
- dotnet-symbol
- dotnet-sos
1、dotnet-counters:
简介:dotnet-counters 是一个性能监视工具,用于初级运行状况监视和性能调查。 它通过 EventCounter API 观察已发布的性能计数器值。例如,可以快速监视CUP使用情况或.NET Core 应用程序中的异常率等指标
安装:通过nuget包安装:
dotnet tool install --global dotnet-counters
主要命令:
- dotnet-counters ps
- dotnet-counters list
- dotnet-counters collect
- dotnet-counters monitor
a)dotnet-counters ps:显示可监视的 dotnet 进程的列表
b)dotnet-counters list命令:显示按提供程序分组的计数器名称和说明的列表
包括:运行时和Web主机运行信息
c)dotnet-counters collect 命令:定期收集所选计数器的值,并将它们导出为指定的文件格式
dotnet-counters collect [-h|--help] [-p|--process-id] [-n|--name] [--diagnostic-port] [--refresh-interval] [--counters <COUNTERS>] [--format] [-o|--output] [-- <command>]
参数说明:
示例:收集dotnet core 服务端所有性能计数器值,间隔时间为3s
d)dotnet-counters monitor命令:显示所选计数器的定期刷新值
dotnet-counters monitor [-h|--help] [-p|--process-id] [-n|--name] [--diagnostic-port] [--refresh-interval] [--counters] [-- <command>]
示例: dotnet-counters monitor –process-id 18832 –refresh-interval 2
2、dotnet-dump:
简介:通过 dotnet-dump 工具,可在不使用本机调试器的情况下收集和分析 Windows 和 Linux 核心转储。
安装:
dotnet tool install --global dotnet-dump
命令:
- dotnet-dump collect
- dotnet-dump analyze
a) dotnet-dump collect:从进程生成dump
dotnet-dump collect [-h|--help] [-p|--process-id] [-n|--name] [--type] [-o|--output] [--diag]
参数说明:
-h|–help | 显示命令行帮助。 |
-p|–process-id <PID> | 指定从中收集转储的进程的 ID 号。 |
-n|–name <name> | 指定从中收集转储的进程的名称。 |
–type <Full|Heap|Mini> | 指定转储类型,它确定从进程收集的信息的类型。 有三种类型: Full – 最大的转储,包含所有内存(包括模块映像)。 Heap – 大型且相对全面的转储,其中包含模块列表、线程列表、所有堆栈、异常信息、句柄信息和除映射图像以外的所有内存。 Mini – 小型转储,其中包含模块列表、线程列表、异常信息和所有堆栈 |
-o|–output <output_dump_path> | 应在其中写入收集的转储的完整路径和文件名。 如果未指定: 在 Windows 上默认为 .\dump_YYYYMMDD_HHMMSS.dmp 。 在 Linux 上默认为 ./core_YYYYMMDD_HHMMSS 。 YYYYMMDD 为年/月/日,HHMMSS 为小时/分钟/秒。 |
–diag | 启用转储收集诊断日志记录。 |
示例:dotnet-dump collect -p 18832
b)dotnet-dump analyze:启动交互式 shell 以了解转储
dotnet-dump analyze <dump_path> [-h|--help] [-c|--command]
示例:dotnet-dump analyze dump_20210509_193133.dmp 进入dmp分析,查看堆栈和未处理异常
Sos命令列表:
命令 | 函数 |
---|---|
soshelp |
显示所有可用命令 |
soshelp|help <command> |
执行指定的命令。 |
exit|quit |
退出交互模式。 |
clrstack <arguments> |
仅提供托管代码的堆栈跟踪。 |
clrthreads <arguments> |
列出正在运行的托管线程。 |
dumpasync <arguments> |
显示有关垃圾回收堆上异步状态机的信息。 |
dumpassembly <arguments> |
显示有关指定地址处程序集的详细信息。 |
dumpclass <arguments> |
显示有关指定地址处的 EEClass 结构的信息。 |
dumpdelegate <arguments> |
显示有关指定地址处的委托的信息。 |
dumpdomain <arguments> |
显示所有 AppDomain 和指定域中的所有程序集的信息。 |
dumpheap <arguments> |
显示有关垃圾回收堆的信息和有关对象的收集统计信息。 |
dumpil <arguments> |
显示与托管方法关联的 Microsoft 中间语言 (MSIL)。 |
dumplog <arguments> |
将内存中压力日志的内容写入到指定文件。 |
dumpmd <arguments> |
显示有关指定地址处的 MethodDesc 结构的信息。 |
dumpmodule <arguments> |
显示有关指定地址处的模块的信息。 |
dumpmt <arguments> |
显示有关指定地址处的 MethodTable 的信息。 |
dumpobj <arguments> |
显示有关位于指定地址处的对象的信息。 |
dso|dumpstackobjects <arguments> |
显示在当前堆栈的边界内找到的所有托管对象。 |
eeheap <arguments> |
显示有关内部运行时数据结构所使用的进程内存的信息。 |
finalizequeue <arguments> |
显示所有已进行终结注册的对象。 |
gcroot <arguments> |
显示有关对指定地址处的对象的引用(或根)的信息。 |
gcwhere <arguments> |
显示传入参数在 GC 堆中的位置。 |
ip2md <arguments> |
显示 JIT 代码中指定地址处的 MethodDesc 结构。 |
histclear <arguments> |
释放由 hist* 命令系列使用的任何资源。 |
histinit <arguments> |
从保存在调试对象中的压力日志初始化 SOS 结构。 |
histobj <arguments> |
显示与 <arguments> 相关的垃圾回收压力日志重定位。 |
histobjfind <arguments> |
显示在指定地址处引用对象的所有日志项。 |
histroot <arguments> |
显示与指定根的提升和重定位相关的信息。 |
lm|modules |
显示进程中的本机模块。 |
name2ee <arguments> |
显示 <argument> 的 MethodTable 和 EEClass 结构。 |
pe|printexception <arguments> |
显示从 Exception 类派生的 <argument> 的任何对象。 |
setsymbolserver <arguments> |
启用符号服务器支持 |
syncblk <arguments> |
显示 SyncBlock 持有者信息。 |
threads|setthread <threadid> |
设置或显示 SOS 命令的当前线程 ID。 |
3、dotnet-gcdump:
简介:dotnet-gcdump 工具可用于为活动 .NET 进程收集 GC(垃圾回收器)转储。
dotnet-gcdump
全局工具使用 EventPipe 收集实时 .NET 进程的 GC(垃圾回收器)转储。 创建 GC 转储时需要在目标进程中触发 GC、开启特殊事件并从事件流中重新生成对象根图。 此过程允许在进程运行时以最小的开销收集 GC 转储。
这些转储对于以下几种情况非常有用:
- 比较多个时间点堆上的对象数。
- 分析对象的根(回答诸如“还有哪些引用此类型的内容?”等问题)。
- 收集有关堆上的对象计数的常规统计信息。
安装:
dotnet tool install --global dotnet-gcdump
示例:从当前正在运行的进程中收集 GC 转储
dotnet-gcdump collect [-h|--help] [-p|--process-id <pid>] [-o|--output <gcdump-file-path>] [-v|--verbose] [-t|--timeout <timeout>] [-n|--name <name>]
参数说明:
参数 | 说明: |
-h|–help | 显示命令行帮助。 |
-p|–process-id <pid> | 可从中收集 GC 转储的进程 ID。 |
-o|–output <gcdump-file-path> | 应写入收集 GC 转储的路径。 默认为 .\YYYYMMDD_HHMMSS_<pid>.gcdump。 |
-v|–verbose | 收集 GC 转储时输出日志。 |
-t|–timeout <timeout> | 如果收集 GC 转储的时间超过了此秒数,则放弃收集。 默认值为 30。 |
-n|–name <name> | 可从中收集 GC 转储的进程的名称。 |
生成示例:dotnet-gcdump collect -p 18832
查看生成文件:使用perfview查看:
4、dotnet-trace:
简介:分析数据通过 .NET Core 中的 EventPipe
公开。 通过 dotnet-trace 工具,可以使用来自应用的有意思的分析数据,这些数据可帮助你分析应用运行缓慢的根本原因。
安装:
dotnet tool install --global dotnet-trace
命令:
dotnet-trace [-h, --help] [--version] <command>
常用命令:
命令 | 说明 |
---|---|
dotnet-trace collect | 从正在运行的进程中收集诊断跟踪,或者启动子进程并对其进行跟踪(仅限 .NET 5+)。 若要让工具运行子进程并自其启动时对其进行跟踪,请将 -- 追加到 collect 命令。 |
dotnet-trace convert | 将 nettrace 跟踪转换为备用格式,以便用于备用跟踪分析工具。 |
dotnet-trace ps | 列出可从中收集跟踪的 dotnet 进程。 |
dotnet-trace list-profiles | 列出预生成的跟踪配置文件,并描述每个配置文件中包含的提供程序和筛选器。 |
示例:收集进程18832诊断跟踪:
使用Vs打开生成的跟踪文件如下:
5、dotnet-symbol:
简介:dotnet-symbol 用于下载打开核心转储或小型转储所需的文件(符号、DAC/DBI、主机文件等)。 如果需要使用符号和模块来调试在其他计算机上捕获的转储文件,请使用此工具。
安装:
dotnet tool install --global dotnet-symbol
命令:
dotnet-symbol [-h|--help] [options] <FILES>
options:
参数 | 说明 |
–microsoft-symbol-server | 添加“http://msdl.microsoft.com/download/symbols”符号服务器路径(默认)。 |
–server-path <symbol server path> | 将符号服务器添加到服务器路径。 |
authenticated-server-path <pat> <server path> | 使用个人访问令牌 (PAT) 将经过身份验证的符号服务器添加到服务器路径。 |
–cache-directory <file cache directory> | 添加缓存目录。 |
–recurse-subdirectories | 处理所有子目录中的输入文件。 |
–host-only | 仅下载 lldb 加载核心转储所需的主机程序(即 dotnet)。 |
–symbols | 下载符号文件(.pdb、.dbg 和 .dwarf)。 |
–modules | 下载模块文件(.dll、.so 和 .dylib)。 |
–Debugging | 下载特殊的调试模块(DAC、DBI 和 SOS)。 |
–windows-pdbs | 当可移植的 PDB 也可用时,会强制下载 Windows PDB。 |
-o, –output <output directory> | 设置输出目录。 否则,请在输入文件旁边写入(默认)。 |
-d, –diagnostics | 启用诊断输出。 |
-h|–help | 显示命令行帮助。 |
6、dotnet-sos:
简介:dotnet-sos 在 Linux 和 macOS(如果使用的是 Windbg/cdb,则在 Windows 上)安装 SOS调试扩展。
安装:
dotnet tool install --global dotnet-sos
命令:在本地安装用于调试 .NET Core 进程的 SOS 扩展
dotnet-sos install
示例:
总结:
微软提供了一套强大的诊断工具,熟练的使用这些工具,可以更快更有效的发现程序的运行问题,解决程序的性能问题。
过程中主要使用:counters、dump、trace 工具用于分析.NET Core性能问题。
最近又了解到微软已对这些基础工具已封装了对应包(Microsoft.Diagnostics.NETCore.Client),可以用来开发出自己的有界面的诊断工具。后续将了解实现一个。
参考文档:
https://docs.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters
https://channel9.msdn.com/Shows/On-NET/Introducing-the-Diagnostics-Client-Library-for-NET-Core
.NET Core with 微服务 - 什么是微服务 - Agile.Zhou - 博客园
来源: .NET Core with 微服务 – 什么是微服务 – Agile.Zhou – 博客园
微服务是这几年最流行的架构,说起架构不提微服务都不好意思跟人家打招呼。最近想要再梳理一下关于微服务的知识,并且结合本人的一些实践经验来做一些总结与分享。前面会分享一些概念性的东西,后面也会使用.net来实践,一步步完成一个简单的微服务架构的小demo。
什么是微服务
其实微服务并没有统一的标准定义。微服务是一种软件架构的风格。它首先由大神martin fowler提出,2014年3月25号在他的博客上发表了一篇博客来描述了这种微服务的架构。原文地址(https://www.martinfowler.com/articles/microservices.html)。
相对于传统的单体(Monolithic)架构应用,微服务把单个进程的应用拆分为多个单独部署的服务。每个服务对外提供一些接口来进行服务间的通讯或者对第三方提供功能。每个独立的服务甚至使用自己独立的存储技术,独立的语言技术栈。说到底微服务架构还是贯彻了软件开发中:单一职责、分而治之、解耦等基本理念,只是它把这种理念从类、类库级别提升到了进程级别。
图片引用自https://www.redhat.com/zh/topics/microservices/what-are-microservices
微服务与SOA
微服务架构看起来跟SOA架构非常相似。事实上微服务架构就是SOA的一种现代化的实现方式,一次进化。虽然不能在两者之间画等号,但是他们的思想确实是一致的。
图片引用自https://zato.io/docs/intro/esb-soa-cn.html
微服务与SOA之间的区别网上有很多,在此不再大段的复制黏贴网上现成的文字,简单谈谈自己的一些理解。
首先SOA大多数情况下是作用于企业内部,它通过ESB等总线技术把企业内的服务(或者称之为应用)串联起来。SOA虽然是在解耦、去中心化,但是它通常跟某种ESB技术强耦合起来,以至于ESB会成为那个最大的中心。微服务的作用范围是应用而不是庞大的企业。微服务不在依赖ESB等总线技术,服务间的通讯通过无状态、轻量级的接口实现。协议采用http、json等通用协议无关开发语言,谁都可以调用。所以相比SOA有更好的去中心化意义。
优点
上面说了这么多关于微服务的知识,那么实施微服务到底为我们带来了哪些好处?网上有很多复制黏贴的话其实我不太苟同,比如:部署简单,如果没有强大的运维团队微服务的部署显然是比传统单体应用部署难度更大了。 比如快速开发快速迭代:事实上单体应用也不用等到完全开发完才能上线。下面说下我认为的微服务的几个优点:
- 技术异构
采用微服务架构可以很方便的在每个服务中使用不同的技术栈。每个团队可以根据自身的业务情况,人员情况安排使用最合适的技术。如果我们服务业务是AI那就考虑pyhton,如果我们的人员比较熟悉JavaScript,那么可以选nodejs。当然技术的多样性也是要权衡的,不能说每个服务都撸一种语言每个都试验一把,这样未必就是好事情了。 - 扩展性
当我们的业务做的越来越大,流量越来越大的时候,需要对计算资源进行扩展。相对于单体应用,微服务可以更好的进行扩展。传统单体应用水平扩展的时候可能需要把整个应用都扩展多个实例。事实上我们的业务越来越大的时候,往往只是某个模块压力巨大。而采用微服务架构我们只需要对某压力大的服务进行水平扩展。配合现在的容器化技术能够更好的利用技术资源。 - 可靠性
由于每个服务都是独立部署,当某个服务故障的时候通常不会导致其它服务同时故障,只是丧失了部分能力。再配合服务降级、熔断等技术可以比单体应用提供更好的可靠性。 - 强模块化边界
这个概念在网上很少出现。我是在B站上杨波老师的一个关于微服务视频上看到的,对这个观点比较认同。模块化是我们软件开发常用的模式。原来我们按类、按类库进行模块化,现在通过微服务架构直接把模块服务化了,并且能独立部署运行。其它模块不在需要直接引用相关类库就可以使用它。而且实施微服务架构后会强制团队进行应用的模块化,对模块的边界进行明确的划分。当然模块的边界划分是个技术活,如果划分的不够好那就是场灾难。
缺点
这个世界上的事情都是具有两面性的。微服务除了有其优点,自然也有缺点。我们在做架构的时候要尽量处理好这些缺点,避免踩到巨坑。下面谈谈我对微服务缺点的一些看法。
- 运维难度增加
本来只需要部署一个IIS站点或者Tomcat服务、维护一个数据库,现在变成了需要部署N个不同的服务,N个不同类型的数据库。不同的服务甚至可能分散在不同的服务器上。要使这些服务正常的工作,正常的通讯,还要对其进行监控显然比单体架构时代对运维的考验提高了一个维度。没有强大的运维团队、自动化的运维工具的话微服务实施起来出故障的概率显然会大大增加。 - 分布式的挑战
微服务架构天然就是分布式的。但是分布式系统会带来很多单体架构没有的问题。比如分布式事务,数据一致性问题。本来在进程内一个锁或者在数据库开一个事务就能解决的事情,现在不得不借助分布式锁、分布式事务、数据最终一致性来处理。这些问题对开发人员写代码的时候也是很大的挑战。除了一致性的问题,微服务架构中服务之间的通信也会有很高的成本。本来进程内的方法调用变成了跨进程、跨服务的通讯。我们知道网络是不可靠的,出现故障的概率远远超过进程内调用。 - 调试,测试难度增加
由于服务之间互相依赖,在做集成测试或者调试的时候需要把所有依赖的服务、数据库等全部都跑起来。出现问题很难一次性定位到确切位置。由于服务器之间网络带宽的原因多次测试结果可能会有变动,测试的结果不稳定。 - 沟通成本提高
在采用微服务架构开发之后,团队的组织架构都可能跟着变动,团队免不了被拆分成多个小团队甚至不同部门。在公司呆过的都知道,跨团队跨部门之间沟通的成本有多大。本来一天就能修复的bug,很可能变成一周。 - 模块划分困难
我们前面说微服务把每个模块进行独立部署,采用独立的数据库。这么轻描淡写的一句话,事实上实施起来并没有那么容易。如果模块划分的不好,那么会出现非常多的跨库查询,非常多的跨库事务。本来单体架构上很简单的事情变得无比复杂。本来一句Transaction就你搞定的事情,现在可能需要先团队之间进行沟通,然后互相开接口,再使用分布式事务来完成。模块划分的一个好的方案就是采用DDD的思想进划分,但是事实上能把DDD玩好落地也不是一件容易的事。
微服务不是银弹
微服务这几年火热的很。很多公司、架构师言架构必微服务,好像微服务是包治百病的良药。不管项目大小,项目周期,人员配置,技术实力,一股脑的上微服务。见过3,5人小团队一个月就能开发上线的说要进行微服务改造。这么做怕不是微服务真的香,而是为了充实自己的简历。
微服务不是银弹,正如上面所述,微服务在享受它带来的好处的时候也是有巨大的成本开销的。它会带来组织架构上的变动,人员的变动。它大大的提高了系统的复杂性,给运维、开发、测试、调试都带来巨大的挑战。
在采用微服务架构之前最好先思考一下,真的需要微服务吗?权衡一下微服务带来的利弊再下决定。以我个人的经验来看,市面上绝大多数系统更适合单体架构,或者说没必要一上来就采用微服务架构。真正好的架构是在满足当前需求的前提下快速稳定的上线,并对后面的扩展、改造留好余地,以应对后面业务发展带来的需求进行架构的升级改造。
总结
通过以上这些铺垫我们讲了微服务的概念、微服务有哪些优点、微服务又有哪些缺点给我们带来了哪些方面的挑战。以上是我个人的一些浅薄的理解有可能有遗漏或者有错误,大家可以一起讨论一下。
下一篇将会对微服务架构、微服务使用的常用组件进行详细介绍,敬请期待。
谢谢阅读,帮忙点赞。
RabbitMQ实现延时消息的两种方法 - JavaLank - 博客园
来源: RabbitMQ实现延时消息的两种方法 – JavaLank – 博客园
RabbitMQ实现延时消息的两种方法
1、死信队列
1.1消息什么时候变为死信(dead-letter)
- 消息被否定接收,消费者使用basic.reject 或者 basic.nack并且requeue 重回队列属性设为false。
- 消息在队列里得时间超过了该消息设置的过期时间(TTL)。
- 消息队列到达了它的最大长度,之后再收到的消息。
1.2死信队列的原理
当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,这个exchange就为DLX。因此我们只需要在声明正常的业务队列时添加一个可选的”x-dead-letter-exchange”参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上,我们接着监听这个交换机就可以了。
1.3 代码实现
- 引入amqp依赖
- 声明交换机,队列
package com.lank.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author lank
* @since 2020/12/14 10:44
*/
@Configuration
public class RabbitmqConfig {
//死信交换机,队列,路由相关配置
public static final String DLK_EXCHANGE = "dlk.exchange";
public static final String DLK_ROUTEKEY = "dlk.routeKey";
public static final String DLK_QUEUE = "dlk.queue";
//业务交换机,队列,路由相关配置
public static final String DEMO_EXCHANGE = "demo.exchange";
public static final String DEMO_QUEUE = "demo.queue";
public static final String DEMO_ROUTEKEY = "demo.routeKey";
//延时插件DelayedMessagePlugin的交换机,队列,路由相关配置
public static final String DMP_EXCHANGE = "dmp.exchange";
public static final String DMP_ROUTEKEY = "dmp.routeKey";
public static final String DMP_QUEUE = "dmp.queue";
@Bean
public DirectExchange demoExchange(){
return new DirectExchange(DEMO_EXCHANGE,true,false);
}
@Bean
public Queue demoQueue(){
//只需要在声明业务队列时添加x-dead-letter-exchange,值为死信交换机
Map<String,Object> map = new HashMap<>(1);
map.put("x-dead-letter-exchange",DLK_EXCHANGE);
//该参数x-dead-letter-routing-key可以修改该死信的路由key,不设置则使用原消息的路由key
map.put("x-dead-letter-routing-key",DLK_ROUTEKEY);
return new Queue(DEMO_QUEUE,true,false,false,map);
}
@Bean
public Binding demoBind(){
return BindingBuilder.bind(demoQueue()).to(demoExchange()).with(DEMO_ROUTEKEY);
}
@Bean
public DirectExchange dlkExchange(){
return new DirectExchange(DLK_EXCHANGE,true,false);
}
@Bean
public Queue dlkQueue(){
return new Queue(DLK_QUEUE,true,false,false);
}
@Bean
public Binding dlkBind(){
return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(DLK_ROUTEKEY);
}
//延迟插件使用
//1、声明一个类型为x-delayed-message的交换机
//2、参数添加一个x-delayed-type值为交换机的类型用于路由key的映射
@Bean
public CustomExchange dmpExchange(){
Map<String, Object> arguments = new HashMap<>(1);
arguments.put("x-delayed-type", "direct");
return new CustomExchange(DMP_EXCHANGE,"x-delayed-message",true,false,arguments);
}
@Bean
public Queue dmpQueue(){
return new Queue(DMP_QUEUE,true,false,false);
}
@Bean
public Binding dmpBind(){
return BindingBuilder.bind(dmpQueue()).to(dmpExchange()).with(DMP_ROUTEKEY).noargs();
}
}
- 声明一个类用于发送带过期时间的消息
package com.lank.demo.rabbitmq;
import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author lank
* @since 2020/12/14 10:33
*/
@Component
@Slf4j
public class MessageSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//使用死信队列发送消息方法封装
public void send(String message,Integer time){
String ttl = String.valueOf(time*1000);
//exchange和routingKey都为业务的就可以,只需要设置消息的过期时间
rabbitTemplate.convertAndSend(RabbitmqConfig.DEMO_EXCHANGE, RabbitmqConfig.DEMO_ROUTEKEY,message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置消息的过期时间,是以毫秒为单位的
message.getMessageProperties().setExpiration(ttl);
return message;
}
});
log.info("使用死信队列消息:{}发送成功,过期时间:{}秒。",message,time);
}
//使用延迟插件发送消息方法封装
public void send2(String message,Integer time){
rabbitTemplate.convertAndSend(RabbitmqConfig.DMP_EXCHANGE, RabbitmqConfig.DMP_ROUTEKEY,message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//使用延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
message.getMessageProperties().setHeader("x-delay",time*1000);
return message;
}
});
log.info("使用延迟插件发送消息:{}发送成功,过期时间:{}秒。",message,time);
}
}
- 编写一个类用于消费消息
package com.lank.demo.rabbitmq;
import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @author lank
* @since 2020/12/15 15:57
*/
@Component
@Slf4j
public class MessageReceiver {
@RabbitHandler
@RabbitListener(queues = RabbitmqConfig.DLK_QUEUE)
public void onMessage(Message message){
log.info("使用死信队列,收到消息:{}",new String(message.getBody()));
}
@RabbitHandler
@RabbitListener(queues = RabbitmqConfig.DMP_QUEUE)
public void onMessage2(Message message){
log.info("使用延迟插件,收到消息:{}",new String(message.getBody()));
}
}
- 编写Controller调用发送消息方法测试结果
package com.lank.demo.controller;
import com.lank.demo.rabbitmq.MessageSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lank
* @since 2020/12/14 17:05
*/
@RestController
public class MessageController {
@Autowired
public MessageSender messageSender;
//死信队列controller
@GetMapping("/send")
public String send(@RequestParam String msg,Integer time){
messageSender.send(msg,time);
return "ok";
}
//延迟插件controller
@GetMapping("/send2")
public String sendByPlugin(@RequestParam String msg,Integer time){
messageSender.send2(msg,time);
return "ok";
}
}
- 配置文件application.properties
server.port=4399
#virtual-host使用默认的/就好,如果需要/demo需自己在控制台添加
spring.rabbitmq.virtual-host=/demo
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
- 启动项目,打开rabbitmq控制台,可以看到交换机和队列已经创建好。
- 在浏览器中请求http://localhost:4399/send?msg=hello&time=5,从控制台的输出来看,刚好5s后接收到消息。
2020-12-16 22:47:28.071 INFO 13304 --- [nio-4399-exec-1] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:hello发送成功,过期时间:5秒。
2020-12-16 22:47:33.145 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:hello
1.4死信队列的一个小注意点
当我往死信队列中发送两条不同过期时间的消息时,如果先发送的消息A的过期时间大于后发送的消息B的过期时间时,由于消息的顺序消费,消息B过期后并不会立即重新publish到死信交换机,而是会等到消息A过期后一起被消费。
依次发送两个请求http://localhost:4399/send?msg=消息A&time=30和http://localhost:4399/send?msg=消息B&time=10,消息A先发送,过期时间30S,消息B后发送,过期时间10S,我们想要的结果应该是10S收到消息B,30S后收到消息A,但结果并不是,控制台输出如下:
2020-12-16 22:54:47.339 INFO 13304 --- [nio-4399-exec-5] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息A发送成功,过期时间:30秒。
2020-12-16 22:54:54.278 INFO 13304 --- [nio-4399-exec-6] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息B发送成功,过期时间:10秒。
2020-12-16 22:55:17.356 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息A
2020-12-16 22:55:17.357 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息B
消息A30S后被成功消费,紧接着消息B被消费。因此当我们使用死信队列时应该注意是否消息的过期时间都是一样的,比如订单超过10分钟未支付修改其状态。如果当一个队列各个消息的过期时间不一致时,使用死信队列就可能达不到延时的作用。这时候我们可以使用延时插件来实现这需求。
2 、延时插件
RabbitMQ Delayed Message Plugin是一个rabbitmq的插件,所以使用前需要安装它,可以参考的GitHub地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
2.1如何实现
- 安装好插件后只需要声明一个类型type为”x-delayed-message”的exchange,并且在其可选参数下配置一个key为”x-delayed-typ”,值为交换机类型(topic/direct/fanout)的属性。
- 声明一个队列绑定到该交换机
- 在发送消息的时候消息的header里添加一个key为”x-delay”,值为过期时间的属性,单位毫秒。
- 代码就在上面,配置类为DMP开头的,发送消息的方法为send2()。
- 启动后在rabbitmq控制台可以看到一个类型为x-delayed-message的交换机。
- 继续在浏览器中发送两个请求http://localhost:4399/send2?msg=消息A&time=30和http://localhost:4399/send2?msg=消息B&time=10,控制台输出如下,不会出现死信队列出现的问题:
2020-12-16 23:31:19.819 INFO 13304 --- [nio-4399-exec-9] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息A发送成功,过期时间:30秒。
2020-12-16 23:31:27.673 INFO 13304 --- [io-4399-exec-10] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息B发送成功,过期时间:10秒。
2020-12-16 23:31:37.833 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息B
2020-12-16 23:31:49.917 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息A
死信交换机官网介绍:https://www.rabbitmq.com/dlx.html
延时插件GitHub:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
从零开始搞监控系统(1)——SDK - 咖啡机(K.F.J) - 博客园
来源: 从零开始搞监控系统(1)——SDK – 咖啡机(K.F.J) – 博客园
目前市面上有许多成熟的前端监控系统,但我们没有选择成品,而是自己动手研发。这里面包括多个原因:
- 填补H5日志的空白
- 节约公司费用支出
- 可灵活地根据业务自定义监控
- 回溯时间能更长久
- 反哺运营和产品,从而优化产品质量
- 一次难得的练兵机会
前端监控地基本目的:了解当前项目实际使用的情况,有哪些异常,在追踪到后,对其进行分析,并提供合适的解决方案。
前端监控地终极目标: 1 分钟感知、5 分钟定位、10 分钟恢复。目前是初版,离该目标还比较遥远。
SDK(采用ES5语法)取名为 shin.js,其作用就是将数据通过 JavaScript 采集起来,统一发送到后台,采集的方式包括监听或劫持原始方法,获取需要上报的数据,并通过 gif 传递数据。
整个系统大致的运行流程如下:
一、异常捕获
异常包括运行时错误、Promise错误、框架错误等。
1)error事件
为 window 注册 error 事件,捕获全局错误,过滤掉与业务无关的错误,例如“Script error.”、JSBridge告警等,还需统一资源载入和运行时错误的数据格式。
// 定义的错误类型码 var ERROR_RUNTIME = "runtime"; var ERROR_SCRIPT = "script"; var ERROR_STYLE = "style"; var ERROR_IMAGE = "image"; var ERROR_AUDIO = "audio"; var ERROR_VIDEO = "video"; var ERROR_PROMISE = "promise"; var ERROR_VUE = "vue"; var ERROR_REACT = "react"; var LOAD_ERROR_TYPE = { SCRIPT: ERROR_SCRIPT, LINK: ERROR_STYLE, IMG: ERROR_IMAGE, AUDIO: ERROR_AUDIO, VIDEO: ERROR_VIDEO }; /** * 监控异常 */ window.addEventListener( "error", function (event) { var errorTarget = event.target; // 过滤掉与业务无关的错误 if (event.message === "Script error." || !event.filename) { return; } if ( errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()] ) { handleError(formatLoadError(errorTarget)); } else { handleError( formatRuntimerError( event.message, event.filename, event.lineno, event.colno, event.error ) ); } }, true //捕获 ); /** * 生成 laod 错误日志 * 需要加载资源的元素 */ function formatLoadError(errorTarget) { return { type: LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()], desc: errorTarget.baseURI + "@" + (errorTarget.src || errorTarget.href), stack: "no stack" }; }
2)unhandledrejection事件
为 window 注册 unhandledrejection 事件,捕获未处理的 Promise 错误,当 Promise 被 reject 且没有 reject 处理器时触发。
window.addEventListener( "unhandledrejection", function (event) { //处理响应数据,只抽取重要信息 var response = event.reason.response; //若无响应,则不监控 if (!response) { return; } var desc = response.request.ajax; desc.status = event.reason.status; handleError({ type: ERROR_PROMISE, desc: desc }); }, true );
Promise 常用于异步通信,例如axios库,当响应异常通信时,就能借助该事件将其捕获,得到的结果如下。
{ "type": "promise", "desc": { "response": { "data": "Error occured while trying to proxy to: localhost:8000/monitor/performance/statistic", "status": 504, "statusText": "Gateway Timeout", "headers": { "connection": "keep-alive", "date": "Wed, 24 Mar 2021 07:53:25 GMT", "transfer-encoding": "chunked", "x-powered-by": "Express" }, "config": { "transformRequest": {}, "transformResponse": {}, "timeout": 0, "xsrfCookieName": "XSRF-TOKEN", "xsrfHeaderName": "X-XSRF-TOKEN", "maxContentLength": -1, "headers": { "Accept": "application/json, text/plain, */*", }, "method": "get", "url": "/api/monitor/performance/statistic" }, "request": { "ajax": { "type": "GET", "url": "/api/monitor/performance/statistic", "status": 504, "endBytes": 0, "interval": "13.15ms", "network": { "bandwidth": 0, "type": "4G" }, "response": "Error occured while trying to proxy to: localhost:8000/monitor/performance/statistic" } } }, "status": 504 }, "stack": "Error: Gateway Timeout at handleError (http://localhost:8000/umi.js:18813:15)" }
这样就能分析出 500、502、504 等响应码所占通信的比例,当高于日常数量时,就得引起注意,查看是否在哪块逻辑出现了问题。
有一点需要注意,上面的结构中包含响应信息,这是需要对 Error 做些额外扩展的,如下所示。
import fetch from 'axios'; function handleError(errorObj) { const { response } = errorObj; if (!response) { const error = new Error('你的网络有点问题'); error.response = errorObj; error.status = 504; throw error; } const error = new Error(response.statusText); error.response = response; error.status = response.status; throw error; } export default function request(url, options) { return fetch(url, options) .catch(handleError) .then((response) => { return { data: response.data }; }); }
公司中有一套项目依赖的是 JQuery 库,因此要监控此处的异常通信,需要做点改造。
好在所有的通信都会请求一个通用函数,那么只要修改此函数的逻辑,就能覆盖到项目中的所有页面。
搜索了API资料,以及研读了 JQuery 中通信的源码后,得出需要声明一个 xhr() 函数,在函数中初始化 XMLHttpRequest 对象,从而才能监控它的实例。
并且在 error 方法中需要手动触发 unhandledrejection 事件。
$.ajax({ url, method, data, success: (res) => { success(res); }, xhr: function () { this.current = new XMLHttpRequest(); return this.current; }, error: function (res) { error(res); Promise.reject({ status: res.status, response: { request: { ajax: this.current.ajax } } }).catch((error) => { throw error; }); } });
3)框架错误
框架是指目前流行的React、Vue等,我只对公司目前使用的这两个框架做了监控。
React 需要在项目中创建一个 ErrorBoundary 类,捕获错误。
import React from 'react'; export default class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { this.setState({ hasError: true }); // 将component中的报错发送到后台 shin && shin.reactError(error, info); } render() { if (this.state.hasError) { return null // 也可以在出错的component处展示出错信息 // return <h1>出错了!</h1>; } return this.props.children; } }
其中 reactError() 方法在组装错误信息。
/** * 处理 React 错误(对外) */ shin.reactError = function (err, info) { handleError({ type: ERROR_REACT, desc: err.toString(), stack: info.componentStack }); };
如果要对 Vue 进行错误捕获,那么就得重写 Vue.config.errorHandler(),其参数就是 Vue 对象。
/** * Vue.js 错误劫持(对外) */ shin.vueError = function (vue) { var _vueConfigErrorHandler = vue.config.errorHandler; vue.config.errorHandler = function (err, vm, info) { handleError({ type: ERROR_VUE, desc: err.toString(), //描述 stack: err.stack //堆栈 }); // 控制台打印错误 if ( typeof console !== "undefined" && typeof console.error !== "undefined" ) { console.error(err); } // 执行原始的错误处理程序 if (typeof _vueConfigErrorHandler === "function") { _vueConfigErrorHandler.call(err, vm, info); } }; };
如果 Vue 是被模块化引入的,那么就得在模块的某个位置调用该方法,因为此时 Vue 不会绑定到 window 中,即不是全局变量。
4)难点
虽然把错误都搜集起来了,但是现代化的前端开发,都会做一次代码合并压缩混淆,也就是说,无法定位错误的真正位置。
为了能转换成源码,就需要引入自动堆栈映射(SourceMap),webpack 默认就带了此功能,只要声明相应地关键字开启即可。
我选择了 devtool: “hidden-source-map”,生成完成的原始代码,并在脚本中隐藏Source Map路径。
//# sourceMappingURL=index.bundle.js.map
在生成映射文件后,就需要让运维配合,编写一个脚本(在发完代码后触发),将这些文件按年月日小时分钟的格式命名(例如 202103041826.js.map),并迁移到指定目录中,用于后期的映射。
之所以没有到秒是因为没必要,在执行发代码的操作时,发布按钮会被锁定,其他人无法再发。
映射的逻辑是用 Node.js 实现的,会在后文中详细讲解。注意,必须要有列号,才能完成代码还原。
二、行为搜集
将行为分成:用户行为、浏览器行为、控制台打印行为。监控这些主要是为了在排查错误时,能还原用户当时的各个动作,从而能更好的找出问题出错的原因。
1)用户行为
目前试验阶段,就监听了点击事件,并且只会对 button 和 a 元素上注册的点击事件做监控。
/** * 全局监听事件 */ var eventHandle = function (eventType, detect) { return function (e) { if (!detect(e)) { return; } handleAction(ACTION_EVENT, { type: eventType, desc: e.target.outerHTML }); }; }; // 监听点击事件 window.addEventListener( "click", eventHandle("click", function (e) { var nodeName = e.target.nodeName.toLowerCase(); // 白名单 if (nodeName !== "a" && nodeName !== "button") { return false; } // 过滤 a 元素 if (nodeName === "a") { var href = e.target.getAttribute("href"); if ( !href || href !== "#" || href.toLowerCase() !== "javascript:void(0)" ) { return false; } } return true; }), false );
2)浏览器行为
监控异步通信,重写 XMLHttpRequest 对象,并通过 Navigator.connection 读取当前的网络环境,例如4G、3G等。
其实还想获取当前用户环境的网速,不过还没有较准确的获取方式,因此并没有添加进来。
var _XMLHttpRequest = window.XMLHttpRequest; //保存原生的XMLHttpRequest //覆盖XMLHttpRequest window.XMLHttpRequest = function (flags) { var req; req = new _XMLHttpRequest(flags); //调用原生的XMLHttpRequest monitorXHR(req); //埋入我们的“间谍” return req; }; var monitorXHR = function (req) { req.ajax = {}; req.addEventListener( "readystatechange", function () { if (this.readyState == 4) { var end = shin.now(); //结束时间 req.ajax.status = req.status; //状态码 if ((req.status >= 200 && req.status < 300) || req.status == 304) { //请求成功 req.ajax.endBytes = _kb(req.responseText.length * 2) + "KB"; //KB } else { //请求失败 req.ajax.endBytes = 0; } req.ajax.interval = _rounded(end - start, 2) + "ms"; //单位毫秒 req.ajax.network = shin.network(); //只记录300个字符以内的响应 req.responseText.length <= 300 && (req.ajax.response = req.responseText); handleAction(ACTION_AJAX, req.ajax); } }, false ); // “间谍”又对open方法埋入了间谍 var _open = req.open; req.open = function (type, url, async) { req.ajax.type = type; //埋点 req.ajax.url = url; //埋点 return _open.apply(req, arguments); }; var _send = req.send; var start; //请求开始时间 req.send = function (data) { start = shin.now(); //埋点 if (data) { req.ajax.startBytes = _kb(JSON.stringify(data).length * 2) + "KB"; req.ajax.data = data; //传递的参数 } return _send.apply(req, arguments); }; }; /** * 计算KB值 */ function _kb(bytes) { return _rounded(bytes / 1024, 2); //四舍五入2位小数 } /** * 四舍五入 */ function _rounded(number, decimal) { return parseFloat(number.toFixed(decimal)); } /** * 网络状态 */ shin.network = function () { var connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection; var effectiveType = connection && connection.effectiveType; if (effectiveType) { return { bandwidth: 0, type: effectiveType.toUpperCase() }; } var types = "Unknown Ethernet WIFI 2G 3G 4G".split(" "); var info = { bandwidth: 0, type: "" }; if (connection && connection.type) { info.type = types[connection.type]; } return info; };
在所有的日志中,通信占的比例是最高的,大概在 90% 以上。
浏览器的行为还包括跳转,当前非常流行 SPA,所以在记录跳转地址时,只需监听 onpopstate 事件即可,其中上一页地址也会被记录。
/** * 全局监听跳转 */ var _onPopState = window.onpopstate; window.onpopstate = function (args) { var href = location.href; handleAction(ACTION_REDIRECT, { refer: shin.refer, current: href }); shin.refer = href; _onPopState && _onPopState.apply(this, args); };
3)控制台打印行为
其实就是重写 console 中的方法,目前只对 log() 做了处理。在实际使用中发现了两个问题。
第一个是在项目调试阶段,将数据打印在控制台时,显示的文件和行数都是 SDK 的名称和位置,无法得知真正的位置,很是别扭。
并且在 SDK 的某些位置调用 console.log() 会形成死循环。后面就加了个 isDebug 开关,在调试时就关闭监控,省心。
function injectConsole(isDebug) { !isDebug && ["log"].forEach(function (level) { var _oldConsole = console[level]; console[level] = function () { var params = [].slice.call(arguments); // 参数转换成数组 _oldConsole.apply(this, params); // 执行原先的 console 方法 var seen = []; handleAction(ACTION_PRINT, { level: level, // 避免循环引用 desc: JSON.stringify(params, function (key, value) { if (typeof value === "object" && value !== null) { if (seen.indexOf(value) >= 0) { return; } seen.push(value); } return value; }) }); }; }); }
第二个就是某些要打印的变量包含循环引用,这样在调用 JSON.stringify() 时就会报错。
三、其他
1)环境信息
通过解析请求中的 UA 信息,可以得到操作系统、浏览器名称版本、CPU等信息。
{ "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36", "browser": { "name": "Chrome", "version": "89.0.4389.82", "major": "89" }, "engine": { "name": "Blink", "version": "89.0.4389.82" }, "os": { "name": "Mac OS", "version": "10.14.6" }, "device": {}, "cpu": {} }
图省事,就用了一个开源库,叫做 UAParser.js,在 Node.js 中引用了此库。
2)上报
上报选择了 Gif 的方式,即把参数拼接到一张 Gif 地址后,传送到后台。
/** * 组装监控变量 */ function _paramify(obj) { obj.token = shin.param.token; obj.subdir = shin.param.subdir; obj.identity = getIdentity(); return encodeURIComponent(JSON.stringify(obj)); } /** * 推送监控信息 */ shin.send = function (data) { var ts = new Date().getTime().toString(); var img = new Image(0, 0); img.src = shin.param.src + "?m=" + _paramify(data) + "&ts=" + ts; };
用这种方式有几个优势:
- 兼容性高,所有的浏览器都支持。
- 不存在跨域问题。
- 不会携带当前域名中的 cookie。
- 不会阻塞页面加载。
- 相比于其他类型的图片格式(BMP、PNG等),能节约更多的网络资源。
不过这种方式也有一个问题,那就是采用 GET 的请求后,浏览器会限制 URL 的长度,也就是不能携带太多的数据。
在之前记录 Ajax 响应数据时就有一个判断,只记录300个字符以内的响应数据,其实就是为了规避此限制而加了这段代码。
3)身份标识
每次进入页面都会生成一个唯一的标识,存储在 sessionStorage 中。在查询日志时,可通过该标识过滤出此用户的上下文日志,消除与他不相干的日志。
function getIdentity() { var key = "shin-monitor-identity"; //页面级的缓存而非全站缓存 var identity = sessionStorage.getItem(key); if (!identity) { //生成标识 identity = Number( Math.random().toString().substr(3, 3) + Date.now() ).toString(36); sessionStorage.setItem(key, identity); } return identity; }
关于当前Web前端技术的一些感悟和笔记 - 伍华聪 - 博客园
来源: 关于当前Web前端技术的一些感悟和笔记 – 伍华聪 – 博客园
最近这些年,随着前端应用技术突飞猛进,产生了很多新的前端框架,当然也引入了数不胜数的前端技术概念,前端不在是早期Web Form的拖拉处理方式,也不再是Ajax+HTML那么简单,随着前端技术的发展,前端的JS越来越重要,也越来越复杂,而为了开发的方便,引入了很多可以对JS+CSS进行编译的框架,而在发布的时候按需编译处理,从而增强了整个前端的开发过程,这些前端的技术包括AngularJS、React、Vue等等,这些前端技术应用框架又囊括了很多相关的技术,包括了MVVM(Model-View-ViewModel)、ES6、Babel、dva、umi、less等技术或概念。前端技术越滚越大,范围也越来越广,大有日新月异的感觉。
1、前端技术的自我回顾和展望
记得在上大学时候,开始玩asp的年代,前端和后端糅合一起的困境;也曾记得WebForm开发的乐趣和无奈,快捷但是很丑很笨重;而现在还在继续做着Ajax + HTML的这种前端的处理,痛并快乐着。技术总是一步步的推进则,但是眼光一旦聚焦在某个技术范畴,日月如梭,抬头间很快就会发现世界又多了新的前端技术,从开始的犹豫和不确信的停留这段时间后,发现整个前端的世界也已经渐成格局,包括Angular、React、Vue等技术应用已经日趋成熟,而且拥有着庞大的拥趸群体,也有着丰富的资源可供学习和了解。
下面是Angular、React、Vue几个技术框架的一些介绍。
AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVC(Model–view–controller)、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。Angular开发在全球开发人员中广泛流行,并被谷歌,福布斯,WhatsApp,Instagram,healthcare.gov和许多财富500强公司等大型组织使用。
React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。 由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。
Vue.js是讨论最多且发展最快的JavaScript框架之一。它由前谷歌员工Evan You创建,他在担任Google员工时曾在Angular工作过。您可以认为它是成功的,因为它能够使用HTML,CSS和JavaScript构建有吸引力的UI。
这些技术各有优点,很难片面的说明谁优谁劣,它们都各自有自己的生存土壤和大批的拥趸,而我开始选型做前端技术更新的时候,主要看中的是阿里巴巴的Ant-Design开发框架,这个它是使用了React的技术框架,因此也就自然而然的研究学习起React和Ant-Design来,虽然之前对前端的一些技术有所涉猎,但是真正等你想要进入Ant-Design的开发大门的时候,还是感觉自己像进入了一个前端技术的大观园,一个个新概念接踵而来,一种种代码的写法迎面冲击,教程看了几遍还是一头雾水,真的开始怀疑人生了,不过学习新技术还是需要很多平静的心态,调整好,一步一个脚印相信还是有所斩获的,偶尔看到阮一峰的大牛介绍在学习研究React的时候,也曾花了几个月的时候,虽然他的高度难以看齐,但是学习的韧劲和毅力,是值得我们学习的。学习新的东西,从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目(摘自阮一峰笔记)。
2、React的技术学习
接触一些新的东西,就必然需要投入精力来学习掌握。对于学习Ant-Desin,虽然这个框架本身提供了很多教程介绍,但是我们一些技术点,还是需要更细节的学习,首推还是阮一峰的技术日志吧。
5、Redux 入门教程(三):React-Redux 的用法
下面有些内容在学习的时候,掌握的不是很好,摘录并作为一个回顾吧。
模块的 Import 和 Export
import
用于引入模块,export
用于导出模块。
// 引入全部 import dva from 'dva'; // 引入部分 import { connect } from 'dva'; import { Link, Route } from 'dva/router'; // 引入全部并作为 github 对象 import * as github from './services/github'; // 导出默认 export default App; // 部分导出,需 import { App } from './file'; 引入 export class App extend Component {};
析构赋值
析构赋值让我们从 Object 或 Array 里取部分数据存为变量。
// 对象 const user = { name: 'guanguan', age: 2 }; const { name, age } = user; console.log(`${name} : ${age}`); // guanguan : 2 // 数组 const arr = [1, 2]; const [foo, bar] = arr; console.log(foo); // 1
我们也可以析构传入的函数参数。
const add = (state, { payload }) => { return state.concat(payload); }; //析构时还可以配 alias,让代码更具有语义 const add = (state, { payload: todo }) => { return state.concat(todo); };
对象展开运算符(Object Spread Operator)
//可用于组装数组。 const todos = ['Learn dva']; [...todos, 'Learn antd']; // ['Learn dva', 'Learn antd'] //也可用于获取数组的部分项。 const arr = ['a', 'b', 'c']; const [first, ...rest] = arr; rest; // ['b', 'c'] // With ignore const [first, , ...rest] = arr; rest; // ['c'] //还可收集函数参数为数组。 function directions(first, ...rest) { console.log(rest); } directions('a', 'b', 'c'); // ['b', 'c']; //代替 apply。 function foo(x, y, z) {} const args = [1,2,3]; // 下面两句效果相同 foo.apply(null, args); foo(...args); //对于 Object 而言,用于组合成新的 Object const foo = { a: 1, b: 2, }; const bar = { b: 3, c: 2, }; const d = 4; const ret = { ...foo, ...bar, d }; // { a:1, b:3, c:2, d:4 }
propTypes
JavaScript 是弱类型语言,所以请尽量声明 propTypes 对 props 进行校验,以减少不必要的问题。
function App(props) { return <div>{props.name}</div>; } App.propTypes = { name: React.PropTypes.string.isRequired, };
内置的 prop type 有:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
DVA数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State。
Reducer和effects
reducer 是一个函数,接受 state 和 action,返回老的或新的 state 。即:(state, action) => state
app.model({ namespace: 'todos', state: [], reducers: { add(state, { payload: todo }) { return state.concat(todo); }, remove(state, { payload: id }) { return state.filter(todo => todo.id !== id); }, update(state, { payload: updatedTodo }) { return state.map(todo => { if (todo.id === updatedTodo.id) { return { ...todo, ...updatedTodo }; } else { return todo; } }); }, }, };
建议最多一层嵌套,以保持 state 的扁平化,深层嵌套会让 reducer 很难写和难以维护。
app.model({ namespace: 'app', state: { todos: [], loading: false, }, reducers: { add(state, { payload: todo }) { const todos = state.todos.concat(todo); return { ...state, todos }; }, }, });
effects示例
app.model({ namespace: 'todos', effects: { *addRemote({ payload: todo }, { put, call }) { yield call(addTodo, todo); yield put({ type: 'add', payload: todo }); }, }, });
put用于触发 action,call用于调用异步逻辑,支持 promise。
异步请求
异步请求基于 whatwg-fetch,API 详见:https://github.com/github/fetch
GET 和 POST
import request from '../util/request'; // GET request('/api/todos'); // POST request('/api/todos', { method: 'POST', body: JSON.stringify({ a: 1 }), });
统一错误处理
假如约定后台返回以下格式时,做统一的错误处理。
{ status: 'error', message: '', }
编辑 utils/request.js
,加入以下中间件:
function parseErrorMessage({ data }) { const { status, message } = data; if (status === 'error') { throw new Error(message); } return { data }; }
然后,这类错误就会走到 onError
hook 里。
Subscription
subscriptions
是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。格式为 ({ dispatch, history }) => unsubscribe
。
异步数据初始化
比如:当用户进入 /users
页面时,触发 action users/fetch
加载用户数据。
app.model({ subscriptions: { setup({ dispatch, history }) { history.listen(({ pathname }) => { if (pathname === '/users') { dispatch({ type: 'users/fetch', }); } }); }, }, });
connect 的使用
connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。
connect 方法传入的第一个参数是 mapStateToProps 函数,该函数需要返回一个对象,用于建立 State 到 Props 的映射关系。
简而言之,connect接收一个函数,返回一个函数。
第一个函数会注入全部的models,你需要返回一个新的对象,挑选该组件所需要的models。
export default connect(({ user, login, global = {}, loading }) => ({ currentUser: user.currentUser, collapsed: global.collapsed, fetchingNotices: loading.effects['global/fetchNotices'], notices: global.notices }))(BasicLayout); // 简化版 export default connect( ({ user, login, global = {}, loading }) => { return { currentUser: user.currentUser, collapsed: global.collapsed, fetchingNotices: loading.effects['global/fetchNotices'], notices: global.notices } } )(BasicLayout);
@connect的使用
其实只是connect的装饰器、语法糖罢了。
// 将 model 和 component 串联起来 export default connect(({ user, login, global = {}, loading }) => ({ currentUser: user.currentUser, collapsed: global.collapsed, fetchingNotices: loading.effects['global/fetchNotices'], notices: global.notices, menuData: login.menuData, redirectData: login.redirectData, }))(BasicLayout);
// 改为这样(export 的不再是connect,而是class组件本身。),也是可以执行的,但要注意@connect必须放在export default class前面: // 将 model 和 component 串联起来 @connect(({ user, login, global = {}, loading }) => ({ currentUser: user.currentUser, collapsed: global.collapsed, fetchingNotices: loading.effects['global/fetchNotices'], notices: global.notices, menuData: login.menuData, redirectData: login.redirectData, })) export default class BasicLayout extends React.PureComponent { // ... }
以上部分内容摘自 https://blog.csdn.net/zhangrui_web/article/details/79651812
2、Ant-Design的框架
这款基于React开发的UI框架,界面非常简洁美观,是阿里巴巴旗下蚂蚁金融服务集团(旗下拥有支付宝、余额宝等产品)所设计的一个前端UI组件库。目前支持了React, 并且有一个对Vue支持的测试版本。
学习和使用Ant-Design,我们可以使用VSCode来对项目代码进行维护和编辑,这样可以在Mac和Window环境同样的开发体验和操作模式,非常方便。
如果需要掌握Ant-Design框架,我们需要了解model,namespace,connect,dispatch,action,reducer ,effect这些概念。
DVA 的 model 对象有几个基本的属性介绍。
namespace
:model 的命名空间,只能用字符串。一个大型应用可能包含多个 model,通过namespace
区分。
state
:当前 model 状态的初始值,表示当前状态。
reducers
:用于处理同步操作,可以修改state
,由action
触发。reducer 是一个纯函数,它接受当前的 state 及一个 action 对象。action 对象里面可以包含数据体(payload)作为入参,需要返回一个新的 state。
effects
:用于处理异步操作(例如:与服务端交互)和业务逻辑,也是由 action 触发。但是,它不可以修改 state,要通过触发 action 调用 reducer 实现对 state 的间接操作。
action
:action 就是一个普通 JavaScript 对象,是 reducers 及 effects 的触发器,形如{ type: 'add', payload: todo }
,通过 type 属性可以匹配到具体某个 reducer 或者 effect,payload 属性则是数据体,用于传送给 reducer 或 effect。
整体的数据流向见下图:
在Reducer里面,不要修改传入的 state
。 使用 Object.assign()
新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter })
,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。
function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } }
或者使用使用对象展开运算符(Object Spread Operator)来处理,从而使用 { ...state, ...newState }
达到相同的目的。
reducers: { save(state, action) { return { ...state, ...action.payload, }; }, },
在 default
情况下返回旧的 state
。遇到未知的 action 时,一定要返回旧的 state
。
每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state
参数都不同,分别对应它管理的那部分 state 数据。
下面两种合成 reducer 方法完全等价:
const reducer = combineReducers({ a: doSomethingWithA, b: processB, c: c })
function reducer(state = {}, action) { return { a: doSomethingWithA(state.a, action), b: processB(state.b, action), c: c(state.c, action) } }
dva封装了redux,减少很多重复代码比如action reducers 常量等,dva所有的redux操作是放在models目录下,通过namespace作为key,标识不同的模块state,可以给state设置初始数据。
reducers跟传统的react-redux写法一致,所有的操作放在reducers对象内
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作,Effects
的最终流向是通过 Reducers
改变 State
。
其中上面的effects里面,call, put其实是saga的写法,dva集成了saga,可以参考上图中的saga内容
DVA 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,DVA 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
DVA 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。
在Ant-Design的Pages/.umi目录里面,有一个initDva.js文件,就是用来统一批量处理 DVA 的引入的,如下所示。
在有 DVA 之前,我们通常会创建 sagas/products.js
, reducers/products.js
和 actions/products.js
,然后在这些文件之间来回切换。
有了 DVA 后,它最核心的是提供了 app.model
方法,用于把 reducer, initialState, action, saga 封装到一起,这样我们在书写代码的时候,把它主要内容,和加载分离出来。如果建立的Model比较多,每次开始的时候需要加入这一句好像也是挺麻烦的,如果可以自动把这个model批量加入,应该会更好吧,不过不知道是基于什么考量。
专注于Winform开发框架/混合式开发框架、Web开发框架、Bootstrap开发框架、微信门户开发框架的研究及应用。
转载请注明出处:
撰写人:伍华聪 http://www.iqidi.com
电商商品数据库的设计和功能界面的处理 - 伍华聪 - 博客园
来源: 电商商品数据库的设计和功能界面的处理 – 伍华聪 – 博客园
前阵子对电商商品及其相关的内容很感兴趣,总有一种不弄明白不罢休的冲劲。因此整整花了几周的时间来了解电商商品的各种概念,参考看不同的人数据库设计,以及参考不同的思路。网上确实有很多文章对这方面进行介绍,而且基础、通用的概念都差不多,不过数据库设计方向倒是有所差异。本篇随笔针对一些介绍比较好的文章或者资料进行对比,并着手进行一定的数据库设计和基于ABP框架+Vue+Element的后台管理进行设计,后续可能会基于商品、会员、订单、售后的基础上进行一个多端的系统整合,如ABP后端API、Vue+Element的后台管理、UniApp小程序或H5公众号进行开发。
1、参考部分优秀的做法
关于电商商品的数据库设计,介绍比较详细的文章有:https://blog.csdn.net/qq_37457564/article/details/101289358,或者https://www.cnblogs.com/cnblogs-programs/articles/5651557.html 、https://blog.csdn.net/thc1987/article/details/80426063、http://www.360doc.com/content/18/0124/11/40769523_724661250.shtml、https://www.cnblogs.com/eggTwo/p/6404805.html。
这些设计理念都一致,另外关于概念的介绍,这篇随笔:http://www.woshipm.com/pd/2122609.html 和https://www.sohu.com/a/341684657_114819也介绍的很详细。
关于电商商品的分类,我看基本上是分为三类,包括京东、淘宝商品类别,也都基本按照这样分级处理。关于商品的类别分类,我们可以参考一篇文档的介绍《电商商城产品三级分类》,这个文档挺好,不过没有办法获得Word格式的文档用来参考。另外我们可以参考《国美的电商商品分类》,国美的商品分类很好,可以用作我们前期商品的类别数据。
后来,无意间看到了《商派ONex在线零售产品操作手册》,这个文档的概念各方面感觉更精确,而且这个公司好像一直从事电子商务方面的咨询、软件开发等事宜,其中它对商品类型和商品分类的分开,我觉得更加贴切,毕竟商品的分类可以更加弹性化一些或自定义组合一些特殊的类别。
因此综合这些考虑,把整体的概念梳理一遍,着手做一下电商商品的数据库设计和功能界面的管理开发了。
在开始之前,我们先来参考一下电商领域的一些概念。
商品分类
商品分类,俗称商品类别、商品目录,指的是为了方便顾客分门别类查找商品,同时方便商家进行商品管理的分类方式。
虚拟分类
在原商品分类基础上,依据商品的品牌、属性、价格等条件筛选而形成的新分类方式,例如200—300元的商品,女性滑盖手机等分类。
商品类型
商品类型不同于商品分类,指的是依据某一类商品的相同属性归纳成的属性集合,例如手机类型都有屏幕尺寸、铃声、网络制式等共同的属性;书籍类型都有出版社、作者、ISBN号等共同的属性。
通用商品类型
系统内置的仅含有商品名、重量、销售价格、简介、库存、品牌等基本属性的一种商品类型。
商品规格
是依据顾客的购买习惯而独立出来的一种商品的特殊属性,例如顾客先选好了某一款衬衫,然后必须再选择颜色和尺码才可以订购,这里的颜色和尺码被称为规格。
商品关键词
商品关键词是商品名称的有效补充,可以实现更多的搜索结果匹配机会,如:索尼爱立信W910i手机中设 置商品关键词“索爱W910i”,则用户搜索“索爱W910i”也可以找到这款手机。
版块
商店前台面页的不同区域,例如特价商品、销售排行榜、最新发货清单等,商家可以在后台的模版编辑中进行版块设置来修改前台表现样式。
市场价
顾客购买商品时的参考价格,不作为购买支付价格。
销售价
是普通顾客在商店中购买商品的结算价格。
会员价
顾客注册成为商店的会员之后,购买商品所享受的价格。商家针对同一商品可以根据会员等级不同,设置不同的价格。
商品配件
是与此商品出现在同一个页面并且可同时购买的其他商品,如:购买诺基亚N95,可同时购买手机电池、内存卡、蓝牙耳机等配件。
相关商品
商家为了促进其它商品的销售而将其显示在当前商品的页面上,这些商品就叫做相关商品。
商品
在系统中,商品是一个销售单位,在前台表现为一个商品详细页。
货品
在系统中,货品与商品不相同,货品是一个库存单位,例如“索爱W910i”是一个商品,但红色的“索爱W910i”是一个货品,黑色的是另一个货品。
货号
是货品的唯一编号,可用于仓库管理。
商品编号
商品的唯一编号,可用于商店前台的商品检索,一般使用数字编号,方便电话订购。
标签
是一种分组标识,可用于商品、订单,店主可以利用标签筛选分组,如:为某几个商品增加“热卖商品”的标签,可以通过板块设置,让这几个商品显示在前台首页的热卖商品区。
SPU:即标准化产品单位,是最接近用户认知的产品单元。比如iphone6、iphone4、小米4都是SPU。
SKU:即库存量单位,例如有iphone6这个SPU,当用户购买时要确定买什么颜色的、内存多大的、支持什么网络等等。就用库存单元SKU去规范它。库存里存在的东西是具体某种规格的,不同的颜色、版本、容量肯定有不同的价格和不同的SKU。
2、数据库设计
电商商品有品牌、商品分类、商品类型、规格分组、规格参数、规格参数选项值、商品SPU、货品SKU等等概念对象,我依照上面的一些设计思路,整合了这些概念,大概有如下的设计关系图。
其中的关系看起来很多,不过总体就那么些概念。这里我吸纳了一位仁兄说把规格和参数作为一个表设计,用标志字段分开的思路。
商品参数(有些人叫商品规格参数)信息如下所示,一般可以分为分组、属性及属性值。这些信息不影响SKU,只是作为商品的一些参数展示。
另外一些参数影响SKU的信息,可以认为是特殊的规格参数,如下所示。
我们选择不同的颜色、版本等规格,可能影响我们SKU的记录,也就是对应的销售价格和库存量。
其中商品品牌、商品列表比较独立,但是商品规格及规格值等信息设计和商品类型关联,从而影响商品信息。
商品其实设计的概念不少,不过都是为了使得数据更加有规律,实现更好的弹性设计。从商品管理扩展出去,还会设计到会员和积分管理相关信息,也是一个不小的设计领域,另外还有设计到订单管理,也是一个大的体系,但是商品是其中的关键,也是很多管理的开始。
3、软件界面的设计
针对商品的管理,主要就是后台数据的管理,前端界面的展示,一般就是电商领域的商品销售了,如可以结合小程序、公众号、官网等方式展示商品进行销售。
我们这里先对商品管理的界面进行设计,其中包括了商品品牌、商品分类、商品规格分组、规格定义、规格选项及它们之间的关系等功能的处理。
按照我们的功能规划,我们定义好以下的菜单
1)品牌管理
其中品牌管理界面如下所示。
品牌编辑或者新增界面如下
品牌信息相对独立,没有和其他模块表之间有直接关系,那么只需要维护他的基础数据和相关的图片信息即可。
2)分类管理
分类设计是一个无穷级的树列表,一般电商商品类别分为三类,我们可以通过左侧树列表快速定位,分类列表界面如下所示。
电商的类别比较多,一个个录入肯定有点麻烦,我就弄了一个快速的批量新增处理。
分类直接从国美商品分类中复制过来即可,非常方便。
其中,我们可以根据商品类别的分级层次,来自动构建分级编码,方便以后根据编码直接定位商品分类的一级、二级、三级信息。
3)商品类型
前面介绍过了,商品类型不同于商品分类,指的是依据某一类商品的相同属性归纳成的属性集合,例如手机类型都有屏幕尺寸、铃声、网络制式等共同的属性;书籍类型都有出版社、作者、ISBN号等共同的属性。
其实这里商品类型和品牌有多对多的关系。
也和规格分组和规格参数和规格选项有相关的关系。
功能界面设计的时候,就需要考虑和这些表之间的关系维护,如基本信息里面和品牌关系进行绑定。
以及商品规格里面的规格及规格列表的维护。
规格选项可以输入图片,也可以上传图片,到时候终端根据选择显示方式进行展示即可。
另外除了影响SKU的特殊规格参数外,还有一个常规的规格参数,这里称为商品参数。商品参数按分组的方式进行管理,如下界面所示。
而其中的参数,除了设置一些选项外(如是否可查询、数值、单位等),和上面的规格类似,也是可以填写列表选项的,如下所示。
4) 商品信息
商品信息,除了维护SPU信息外,还需要管理SKU和库存信息,因此需要综合上面很多信息进行分类,商品列表界面主要是提供快速商品的检索和创建SKU记录信息的入口,商品列表如下所示。
其中商品分类,我们可以根据数据库记录进行展示并选择过滤数据。
商品创建及SKU记录信息处理,我们可以引入 hooray / vue-sku-form 组件进行信息的创建,如下界面所示。
通过不同的商品规格,如颜色、内存等生成多个不同规格的SKU记录,并设置对应的价格和库存信息。
以上就是关于电商商品的一些数据库设计和功能界面的截图,主要就是用来理清各个电商商品的概念,以及模块之间的关系,为后面的会员管理、订单管理等大领域进行基础的处理。
为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:
循序渐进VUE+Element 前端应用开发(1)— 开发环境的准备工作
循序渐进VUE+Element 前端应用开发(2)— Vuex中的API、Store和View的使用
循序渐进VUE+Element 前端应用开发(3)— 动态菜单和路由的关联处理
循序渐进VUE+Element 前端应用开发(4)— 获取后端数据及产品信息页面的处理
循序渐进VUE+Element 前端应用开发(5)— 表格列表页面的查询,列表展示和字段转义处理
循序渐进VUE+Element 前端应用开发(6)— 常规Element 界面组件的使用
循序渐进VUE+Element 前端应用开发(7)— 介绍一些常规的JS处理函数
循序渐进VUE+Element 前端应用开发(8)— 树列表组件的使用
循序渐进VUE+Element 前端应用开发(9)— 界面语言国际化的处理
循序渐进VUE+Element 前端应用开发(10)— 基于vue-echarts处理各种图表展示
循序渐进VUE+Element 前端应用开发(11)— 图标的维护和使用
循序渐进VUE+Element 前端应用开发(12)— 整合ABP框架的前端登录处理
循序渐进VUE+Element 前端应用开发(13)— 前端API接口的封装处理
循序渐进VUE+Element 前端应用开发(14)— 根据ABP后端接口实现前端界面展示
循序渐进VUE+Element 前端应用开发(15)— 用户管理模块的处理
循序渐进VUE+Element 前端应用开发(16)— 组织机构和角色管理模块的处理
循序渐进VUE+Element 前端应用开发(17)— 菜单管理
循序渐进VUE+Element 前端应用开发(18)— 功能点管理及权限控制
循序渐进VUE+Element 前端应用开发(19)— 后端查询接口和Vue前端的整合
循序渐进VUE+Element 前端应用开发(20)— 使用组件封装简化界面代码
循序渐进VUE+Element 前端应用开发(21)— 省市区县联动处理的组件使用
循序渐进VUE+Element 前端应用开发(22)— 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中
循序渐进VUE+Element 前端应用开发(23)— 基于ABP实现前后端的附件上传,图片或者附件展示管理
循序渐进VUE+Element 前端应用开发(24)— 修改密码的前端界面和ABP后端设置处理
循序渐进VUE+Element 前端应用开发(25)— 各种界面组件的使用(1)
循序渐进VUE+Element 前端应用开发(26)— 各种界面组件的使用(2)
专注于Winform开发框架/混合式开发框架、Web开发框架、Bootstrap开发框架、微信门户开发框架的研究及应用。
转载请注明出处:
撰写人:伍华聪 http://www.iqidi.com
.net平台的MongoDB使用 - 陈珙 - 博客园
来源: .net平台的MongoDB使用 – 陈珙 – 博客园
前言
最近花了点时间玩了下MongoDB.Driver,进行封装了工具库,平常也会经常用到MongoDB,因此写一篇文章梳理知识同时把自己的成果分享给大家。
本篇会设计到Lambda表达式的解析,有兴趣的同学也看看我之前写的《表达式树的解析》。
文章最后会给出源码下载地址。
MongoDB简介
MongoDB是一个基于分布式文件存储的非关系型数据库,相比于其他NoSQL它支持复杂的查询。
文本是类似JSON的BSON格式,BSON是在JSON的基础上进化:更快的遍历、操作更简易、更多的数据类型。因此MongoDB可以存储比较复杂的数据类型,同样也支持建立索引。
MongoDB的概念有:
- DataBase(库)
- Collections(集合),类似于关系型数据库的表
- Document(文档),类似于关系型数据库的一条数据
MongoDB优缺点
-
优点
- 高效性,内置GridFS,从而达到海量数据存储,并且满足大数据集的快速范围查询。
- 高扩展性,分片使MongoDB的有更高的吞吐量,复制使MongoDB更高的可用性。
- BSON文档,易于理解、查看,
- 免费
-
缺点
- 不支持事务
- 不支持表关联
- 不耗CPU却耗内存
- 没有成熟的管理工具
MongoDB使用场景
拥有高效的存储的特点,让MongoDB用在操作日志记录是非常流行的做法。
随着版本的升级提供更加强大的功能,产品逐渐成熟用在主业务也很多,例如电商行业的订单系统与包裹跟踪模块,海量的主订单与订单明细,包裹的状态变更信息。
然而因为BSON文档的存储方式,使平常的开发的思维模式有所变更。举个栗子,传统用关系型数据库,订单模块就会分主订单表和订单明细表,创建订单就会用事务同时添加两表的数据,查找订单也会通过两表关联查询出来。但是使用MongoDB,主订单表与其明细,将会以一个完整的对象保存为文档。
也因为不支持事务、表关联的原因,它更加适合用作于一个完整的业务模块。
部分朋友会带着一个问题,非关系型数据库和关系型数据库哪个更好。我认为,谁都无法代替谁,一般情况下,非关系型数据库更多的作为关系型数据库扩展,用好了效果甚佳,滥用了只会寸步难行。
MongoDB安装
本来想写的,相应的文章在园子太多了,借用一位仁兄的博文,传送门
MongoDB下载地址:https://www.mongodb.com/download-center#community
管理工具:Robomongo,传送门
MongoDB.Driver的使用
创建一个控制台,到Nuget下载MongoDB.Driver。写入以下代码:
1 using System; 2 using FrameWork.MongoDB.MongoDbConfig; 3 using MongoDB.Bson.Serialization.Attributes; 4 using MongoDB.Driver; 5 6 namespace FrameWork.MongoDb.Demo 7 { 8 class Program 9 { 10 static void Main(string[] args) 11 { 12 var database = "testdatabase"; 13 var collection = "TestMongo"; 14 var db = new MongoClient("您的地址").GetDatabase(database); 15 var coll = db.GetCollection<TestMongo>(collection); 16 17 var entity = new TestMongo 18 { 19 Name = "SkyChen", 20 Amount = 100, 21 CreateDateTime = DateTime.Now 22 }; 23 24 coll.InsertOneAsync(entity).ConfigureAwait(false); 25 26 } 27 } 28 29 public class TestMongo : MongoEntity 30 { 31 32 [BsonDateTimeOptions(Kind = DateTimeKind.Local)] 33 public DateTime CreateDateTime { get; set; } 34 35 public decimal Amount { get; set; } 36 37 public string Name { get; set; } 38 39 } 40 }
第一个demo:添加数据就完成了。F12可以看到IMongoCollection这个接口,增删改查都有,注意分One和Many。基础的使用就不扯过多,在文章尾部的代码已经提供增删改查的封装。
增删查的封装相对简单,但是MongoDB.Driver提供的update的稍微比较特殊。通过Builders<T>.Update.Set(_fieldname, value)更新指定字段名,有多个字段名需要修改,就要通过new UpdateDefinitionBuilder<T>().Combine(updateDefinitionList)去完成
然而,这种方式并不适用于我们实际开发,因此需要对Update方法进行 实体更新封装和Lambda更新封装。
实体更新封装
通过ID作为过滤条件更新整个实体在实际工作中是常有的。既然通过ID作为条件,那么只能通过UpdateOneAsync进行约束更新一条数据。更新的字段可以通过反射实体对象进行遍历属性。下边是实现代码:
/// <summary> /// mongodb扩展方法 /// </summary> internal static class MongoDbExtension { /// <summary> /// 获取更新信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="entity"></param> /// <returns></returns> internal static UpdateDefinition<T> GetUpdateDefinition<T>(this T entity) { var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); var updateDefinitionList = GetUpdateDefinitionList<T>(properties, entity); var updateDefinitionBuilder = new UpdateDefinitionBuilder<T>().Combine(updateDefinitionList); return updateDefinitionBuilder; } /// <summary> /// 获取更新信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="propertyInfos"></param> /// <param name="entity"></param> /// <returns></returns> internal static List<UpdateDefinition<T>> GetUpdateDefinitionList<T>(PropertyInfo[] propertyInfos, object entity) { var updateDefinitionList = new List<UpdateDefinition<T>>(); propertyInfos = propertyInfos.Where(a => a.Name != "_id").ToArray(); foreach (var propertyInfo in propertyInfos) { if (propertyInfo.PropertyType.IsArray || typeof(IList).IsAssignableFrom(propertyInfo.PropertyType)) { var value = propertyInfo.GetValue(entity) as IList; var filedName = propertyInfo.Name; updateDefinitionList.Add(Builders<T>.Update.Set(filedName, value)); } else { var value = propertyInfo.GetValue(entity); if (propertyInfo.PropertyType == typeof(decimal)) value = value.ToString(); var filedName = propertyInfo.Name; updateDefinitionList.Add(Builders<T>.Update.Set(filedName, value)); } } return updateDefinitionList; } }
Lambda表达式更新封装
曾经用过其他ORM都清楚Lambda表达式使用是非常频繁的,MongoDB.Driver已经支持Lambda表达式的过滤条件,但没支持部分字段更新,因此由我们自己来写解析。下边是现实代码:
#region Mongo更新字段表达式解析 /// <summary> /// Mongo更新字段表达式解析 /// </summary> /// <typeparam name="T"></typeparam> public class MongoDbExpression<T> : ExpressionVisitor { #region 成员变量 /// <summary> /// 更新列表 /// </summary> internal List<UpdateDefinition<T>> UpdateDefinitionList = new List<UpdateDefinition<T>>(); private string _fieldname; #endregion #region 获取更新列表 /// <summary> /// 获取更新列表 /// </summary> /// <param name="expression"></param> /// <returns></returns> public static List<UpdateDefinition<T>> GetUpdateDefinition(Expression<Func<T, T>> expression) { var mongoDb = new MongoDbExpression<T>(); mongoDb.Resolve(expression); return mongoDb.UpdateDefinitionList; } #endregion #region 解析表达式 /// <summary> /// 解析表达式 /// </summary> /// <param name="expression"></param> private void Resolve(Expression<Func<T, T>> expression) { Visit(expression); } #endregion #region 访问对象初始化表达式 /// <summary> /// 访问对象初始化表达式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitMemberInit(MemberInitExpression node) { var bingdings = node.Bindings; foreach (var item in bingdings) { var memberAssignment = (MemberAssignment)item; _fieldname = item.Member.Name; if (memberAssignment.Expression.NodeType == ExpressionType.MemberInit) { var lambda = Expression.Lambda<Func<object>>(Expression.Convert(memberAssignment.Expression, typeof(object))); var value = lambda.Compile().Invoke(); UpdateDefinitionList.Add(Builders<T>.Update.Set(_fieldname, value)); } else { Visit(memberAssignment.Expression); } } return node; } #endregion #region 访问二元表达式 /// <summary> /// 访问二元表达式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitBinary(BinaryExpression node) { UpdateDefinition<T> updateDefinition; var value = ((ConstantExpression)node.Right).Value; if (node.Type == typeof(int)) { var realValue = (int)value; if (node.NodeType == ExpressionType.Decrement) realValue = -realValue; updateDefinition = Builders<T>.Update.Inc(_fieldname, realValue); } else if (node.Type == typeof(long)) { var realValue = (long)value; if (node.NodeType == ExpressionType.Decrement) realValue = -realValue; updateDefinition = Builders<T>.Update.Inc(_fieldname, realValue); } else if (node.Type == typeof(double)) { var realValue = (double)value; if (node.NodeType == ExpressionType.Decrement) realValue = -realValue; updateDefinition = Builders<T>.Update.Inc(_fieldname, realValue); } else if (node.Type == typeof(decimal)) { var realValue = (decimal)value; if (node.NodeType == ExpressionType.Decrement) realValue = -realValue; updateDefinition = Builders<T>.Update.Inc(_fieldname, realValue); } else if (node.Type == typeof(float)) { var realValue = (float)value; if (node.NodeType == ExpressionType.Decrement) realValue = -realValue; updateDefinition = Builders<T>.Update.Inc(_fieldname, realValue); } else { throw new Exception(_fieldname + "不支持该类型操作"); } UpdateDefinitionList.Add(updateDefinition); return node; } #endregion #region 访问数组表达式 /// <summary> /// 访问数组表达式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitNewArray(NewArrayExpression node) { var listLambda = Expression.Lambda<Func<IList>>(node); var list = listLambda.Compile().Invoke(); UpdateDefinitionList.Add(Builders<T>.Update.Set(_fieldname, list)); return node; } /// <summary> /// 访问集合表达式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitListInit(ListInitExpression node) { var listLambda = Expression.Lambda<Func<IList>>(node); var list = listLambda.Compile().Invoke(); UpdateDefinitionList.Add(Builders<T>.Update.Set(_fieldname, list)); return node; } #endregion #region 访问常量表达式 /// <summary> /// 访问常量表达式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitConstant(ConstantExpression node) { var value = node.Type.IsEnum ? (int)node.Value : node.Value; UpdateDefinitionList.Add(Builders<T>.Update.Set(_fieldname, value)); return node; } #endregion #region 访问成员表达式 /// <summary> /// 访问成员表达式 /// </summary> /// <param name="node"></param> /// <returns></returns> protected override Expression VisitMember(MemberExpression node) { if (node.Type.GetInterfaces().Any(a => a.Name == "IList")) { var lambda = Expression.Lambda<Func<IList>>(node); var value = lambda.Compile().Invoke(); UpdateDefinitionList.Add(Builders<T>.Update.Set(_fieldname, value)); } else { var lambda = Expression.Lambda<Func<object>>(Expression.Convert(node, typeof(object))); var value = lambda.Compile().Invoke(); if (node.Type.IsEnum) value = (int)value; UpdateDefinitionList.Add(Builders<T>.Update.Set(_fieldname, value)); } return node; } #endregion } #endregion
表达式树的解析
对于Lambda表达式的封装,我侧重讲一下。假如有一段这样的更新代码:
new MongoDbService().Update<User>(a => a._id == "d99ce40d7a0b49768b74735b91f2aa75", a => new User { AddressList = new List<string> { "number1", "number2" }, Age = 10, BirthDateTime = DateTime.Now, Name = "skychen", NumList = new List<int> { 1211,23344 }, Sex = Sex.Woman, Son = new User { Name = "xiaochenpi", Age = 1 } });
那么,我们可以调试监视看看(下图),我们可以得出两个重要信息:
1.Expression<Func<T, T>>解析出来Body的NodeType是MemberInit
2.Bindings里有需要修改的字段信息。
再调试进去看看Bindings的第一项,我们又可以了解了几个重要信息。
1.Bindings里的元素是MemberAssignment类型。
2.Member能取到Name属性,也就是字段名
3.Expression属性,使用 Expression.Lambda,进行Compile().Invoke()就能得到我们需要的值。
fileName和Value都能取到了,那么更新自然能解决了。
上图是源码的部分核心代码,奇怪的是,我并没有在VisitMemberInit里进行遍历Bindings后进行Update.Set,而是将item的Expression属性再一次访问。那是因为我需要针对不同的数据类型进行处理。例如:
常量,我可以定义一个object value进行去接收,如果遇到枚举我需要强转成整型。
集合与数组,假如草率的使用object类型,object value = Expression.Lambda<Func<object>>(node).Compile().Invoke(),那么更新到MongoDB里就会有bug,奇怪的_t,_v就会出现。以此我需要定义为IList才能解决这个问题。
此外,工作中还会遇到金额或者数量自增的情况。Amount = a.Amount+9.9M,Count =a.Count-1。 MongoDB.Driver提供了Builders<T>.Update.Inc方法,因此重写二元表达式进行封装。
附加
经过测试,官方驱动2.4.3和2.4.4版本对类型IList支持有问题,如下图,所以现在封装版本最高支持到2.4.2。
结束
不知道有多少朋友直接拖到文章尾部直接下载源码的。。。。。。
如果对您有用,麻烦您推荐一下。
此外还要感谢非非大哥哥,率先做了我的小白鼠给我提出了可贵的BUG,不然我还真不敢放出源码。
如果有什么问题和建议,可以在下方评论,我会及时回复。
双手奉上源码:https://github.com/SkyChenSky/Framework.MongoDB.git
作 者: 陈珙
出 处:http://www.cnblogs.com/skychen1218/
关于作者:专注于微软平台的项目开发。如有问题或建议,请多多赐教!
版权声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是作者坚持原创和持续写作的最大动力!