1. 前言

本问是根据网上很多文章的总结得到的。

2. 介绍

RSA加密算法是一种非对称加密算法。

对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到2016年为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。

1983年麻省理工学院在美国为RSA算法申请了专利。这个专利2000年9月21日失效。由于该算法在申请专利前就已经被发表了,在世界上大多数其它地区这个专利权不被承认。

具体介绍可以查看维基百科

https://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95

3. 开始

1)在线RSA加密,请选用PKCS#1来生成公钥与私钥

http://web.chacuo.net/netrsakeypair

在线生成非对称加密公钥私钥对、在线生成公私钥对、RSA Key pair create、生成RSA密钥对

点击【生成秘钥对RSA】就可以生成对应的非对称加密公钥与非对称加密似钥

2)前端JS框架

http://travistidwell.com/jsencrypt/

3)流程图

流程图

从上图可以看到,先从网站上生成publicKey与privateKey。

第一步返回publicKey前端,用来对password等敏感字段的加密。

第二步,前端进行password敏感字段的加密。

第三步post数据给后端。

第四步用publicKey与privateKey进行解密。

4.代码

这里的代码是简单的直接从前端访问后台,后台进行解密。逻辑根据读者的爱好编写。

前端代码

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
    <script src="http://passport.cnblogs.com/scripts/jsencrypt.min.js"></script>
    <script type="text/javascript">
        // 使用jsencrypt类库加密js方法,
        function encryptRequest(reqUrl, data, publicKey) {
            var encrypt = new JSEncrypt();
            encrypt.setPublicKey(publicKey);
            // ajax请求发送的数据对象
            var sendData = new Object();
            // 将data数组赋给ajax对象
            for (var key in data) {
                sendData[key] = encrypt.encrypt(data[key]);
            }

            $.ajax({
                url: reqUrl,
                type: 'post',
                data: sendData,
                dataType: 'json',
                //contentType: 'application/json; charset=utf-8',
                success: function (data) {
                    console.info(data);
                },
                error: function (xhr) {
                    //console.error('出错了');
                }
            });

        }

        // Call this code when the page is done loading.
        $(function () {

            $('#testme').click(function () {

                var data = [];
                data['username'] = $('#username').val();
                data['passwd'] = $('#passwd').val();

                var pkey = $('#pubkey').val();
                encryptRequest('/WebForm2.aspx', data, pkey);
            });
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <label for="pubkey">Public Key</label><br />
            <textarea id="pubkey" rows="15" cols="65">
                MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCDbrIgHK8qkz5IfK/A7At4SVZQ
                31TalDPsc4vzeDVjd5ao46hcf+eOEQNm8jmxxHTm6WPSTy7RDVXG/NI489L9okkd
                K++kVh2Z9GjBo5jw/n9EYojt8aYyEOc6cMHT2Fv+1smG+X/W2HeXXoJJjcFLSjBe
                CKx1SoCD4+B2ZiDQ8wIDAQAB
            </textarea><br />
            <label for="input">Text to encrypt:</label><br />
            name:<input id="username" name="username" type="text"></input><br />
            password:<input id="passwd" name="passwd" type="password"></input><br />
            <input id="testme" type="button" value="submit" /><br />
        </div>
    </form>
</body>
</html>

后端代码

解密

 private RSACrypto rsaCrypto = new RSACrypto(PublicAttribute.PrivateKey, PublicAttribute.PublicKey);

//获取参数
 string usernameEncode = Request["username"];
 string pwdEncode = Request["pwd"];

//解密 RSA
 string username = rsaCrypto.Decrypt(usernameEncode);
 string pwd = rsaCrypto.Decrypt(pwdEncode);

类 RSACrypto

public class RSACrypto
    {
        private RSACryptoServiceProvider _privateKeyRsaProvider;
        private RSACryptoServiceProvider _publicKeyRsaProvider;

        public RSACrypto(string privateKey, string publicKey = null)
        {
            if (!string.IsNullOrEmpty(privateKey))
            {
                _privateKeyRsaProvider = CreateRsaProviderFromPrivateKey(privateKey);
            }

            if (!string.IsNullOrEmpty(publicKey))
            {
                _publicKeyRsaProvider = CreateRsaProviderFromPublicKey(publicKey);
            }
        }

        public string Decrypt(string cipherText)
        {
            if (_privateKeyRsaProvider == null)
            {
                throw new Exception("_privateKeyRsaProvider is null");
            }
            return Encoding.UTF8.GetString(_privateKeyRsaProvider.Decrypt(System.Convert.FromBase64String(cipherText), false));
        }

        public string Encrypt(string text)
        {
            if (_publicKeyRsaProvider == null)
            {
                throw new Exception("_publicKeyRsaProvider is null");
            }
            return Convert.ToBase64String(_publicKeyRsaProvider.Encrypt(Encoding.UTF8.GetBytes(text), false));
        }

        private RSACryptoServiceProvider CreateRsaProviderFromPrivateKey(string privateKey)
        {
            var privateKeyBits = System.Convert.FromBase64String(privateKey);

            var RSA = new RSACryptoServiceProvider();
            var RSAparams = new RSAParameters();

            using (BinaryReader binr = new BinaryReader(new MemoryStream(privateKeyBits)))
            {
                byte bt = 0;
                ushort twobytes = 0;
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130)
                    binr.ReadByte();
                else if (twobytes == 0x8230)
                    binr.ReadInt16();
                else
                    throw new Exception("Unexpected value read binr.ReadUInt16()");

                twobytes = binr.ReadUInt16();
                if (twobytes != 0x0102)
                    throw new Exception("Unexpected version");

                bt = binr.ReadByte();
                if (bt != 0x00)
                    throw new Exception("Unexpected value read binr.ReadByte()");

                RSAparams.Modulus = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.Exponent = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.D = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.P = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.Q = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.DP = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.DQ = binr.ReadBytes(GetIntegerSize(binr));
                RSAparams.InverseQ = binr.ReadBytes(GetIntegerSize(binr));
            }

            RSA.ImportParameters(RSAparams);
            return RSA;
        }

        private int GetIntegerSize(BinaryReader binr)
        {
            byte bt = 0;
            byte lowbyte = 0x00;
            byte highbyte = 0x00;
            int count = 0;
            bt = binr.ReadByte();
            if (bt != 0x02)
                return 0;
            bt = binr.ReadByte();

            if (bt == 0x81)
                count = binr.ReadByte();
            else
                if (bt == 0x82)
            {
                highbyte = binr.ReadByte();
                lowbyte = binr.ReadByte();
                byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                count = BitConverter.ToInt32(modint, 0);
            }
            else
            {
                count = bt;
            }

            while (binr.ReadByte() == 0x00)
            {
                count -= 1;
            }
            binr.BaseStream.Seek(-1, SeekOrigin.Current);
            return count;
        }

        private RSACryptoServiceProvider CreateRsaProviderFromPublicKey(string publicKeyString)
        {
            // encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
            byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
            byte[] x509key;
            byte[] seq = new byte[15];
            int x509size;

            x509key = Convert.FromBase64String(publicKeyString);
            x509size = x509key.Length;

            // ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
            using (MemoryStream mem = new MemoryStream(x509key))
            {
                using (BinaryReader binr = new BinaryReader(mem))  //wrap Memory Stream with BinaryReader for easy reading
                {
                    byte bt = 0;
                    ushort twobytes = 0;

                    twobytes = binr.ReadUInt16();
                    if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                        binr.ReadByte();    //advance 1 byte
                    else if (twobytes == 0x8230)
                        binr.ReadInt16();   //advance 2 bytes
                    else
                        return null;

                    seq = binr.ReadBytes(15);       //read the Sequence OID
                    if (!CompareBytearrays(seq, SeqOID))    //make sure Sequence for OID is correct
                        return null;

                    twobytes = binr.ReadUInt16();
                    if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81)
                        binr.ReadByte();    //advance 1 byte
                    else if (twobytes == 0x8203)
                        binr.ReadInt16();   //advance 2 bytes
                    else
                        return null;

                    bt = binr.ReadByte();
                    if (bt != 0x00)     //expect null byte next
                        return null;

                    twobytes = binr.ReadUInt16();
                    if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                        binr.ReadByte();    //advance 1 byte
                    else if (twobytes == 0x8230)
                        binr.ReadInt16();   //advance 2 bytes
                    else
                        return null;

                    twobytes = binr.ReadUInt16();
                    byte lowbyte = 0x00;
                    byte highbyte = 0x00;

                    if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81)
                        lowbyte = binr.ReadByte();  // read next bytes which is bytes in modulus
                    else if (twobytes == 0x8202)
                    {
                        highbyte = binr.ReadByte(); //advance 2 bytes
                        lowbyte = binr.ReadByte();
                    }
                    else
                        return null;
                    byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };   //reverse byte order since asn.1 key uses big endian order
                    int modsize = BitConverter.ToInt32(modint, 0);

                    int firstbyte = binr.PeekChar();
                    if (firstbyte == 0x00)
                    {   //if first byte (highest order) of modulus is zero, don't include it
                        binr.ReadByte();    //skip this null byte
                        modsize -= 1;   //reduce modulus buffer size by 1
                    }

                    byte[] modulus = binr.ReadBytes(modsize);   //read the modulus bytes

                    if (binr.ReadByte() != 0x02)            //expect an Integer for the exponent data
                        return null;
                    int expbytes = (int)binr.ReadByte();        // should only need one byte for actual exponent data (for all useful values)
                    byte[] exponent = binr.ReadBytes(expbytes);

                    // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                    RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
                    RSAParameters RSAKeyInfo = new RSAParameters();
                    RSAKeyInfo.Modulus = modulus;
                    RSAKeyInfo.Exponent = exponent;
                    RSA.ImportParameters(RSAKeyInfo);

                    return RSA;
                }

            }
        }

        private bool CompareBytearrays(byte[] a, byte[] b)
        {
            if (a.Length != b.Length)
                return false;
            int i = 0;
            foreach (byte c in a)
            {
                if (c != b[i])
                    return false;
                i++;
            }
            return true;
        }
    }

到此结束了。

原文地址:加密–RSA前端与后台的加密&解密

互联网的本质就是一系列的网络协议,这个协议就叫OSI协议(一系列协议),按照功能不同,分工不同,人为的分层七层。实际上这个七层是不存在的。没有这七层的概念,只是人为的划分而已。区分出来的目的只是让你明白哪一层是干什么用的。

每一层都运行不同的协议。协议是干什么的,协议就是标准。

实际上还有人把它划成五层、四层。

七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

五层划分为:应用层、传输层、网络层、数据链路层、物理层

四层划分为:应用层、传输层、网络层、网络接口层

物理层:

字面意思解释:物理传输、硬件、物理特性。在深圳的你与北京的朋友聊天,你的电脑必须要能上网,物理体现是什么?是不是接一根网线,插个路由器,北京的朋友那边是不是也有根网线,也得插个路由器。也就是说计算机与计算机之间的通信,必须要有底层物理层方面的连通,就类似于你打电话,中间是不是必须得连电话线。

中间的物理链接可以是光缆、电缆、双绞线、无线电波。中间传的是电信号,即010101…这些二进制位。

底层传输的010010101001…这些二级制位怎么才能让它有意义呢?

要让这些010010101001…有意思,人为的分组再适合不过了,8位一组,发送及接收都按照8位一组来划分。接收到8位为一组的话,那么就可以按照这8位数来做运算。如果没有分组,对方接收的计算机根本就不知道从哪一位开始来做计算,也解析不了收到的数据。我发了16位你就按照16位来做计算吗?我发100位你就按照100位做计算吗?没什么意义是吧。因此要想让底层的电信号有意义,必须要把底层的电信号做分组。我做好8位一组,那么我收到数据,我就知道这几个8位做一组,这几个8位做一组。那么每个8位就可以得到一个确定的数。分组是谁干的活呢?物理层干不了,这个是数据链路层干的。

数据链路层

早期的时候,数据链路层就是来对电信号来做分组的。以前每个公司都有自己的分组方式,非常的乱,后来形成了统一的标准(标准就是协议),即以太网协议Ethernet。

Ethernet规定

一组电信号称之为一个数据包,或者叫做一个“帧”

  • 每一数据帧分成:报头head和数据data两部分

head包含:(固定18个字节)

  • 发送者(源地址,6个字节)
  • 接收者(目标地址,6个字节)
  • 数据类型(6个字节)

data包含:(最短46字节,最长1500字节)

  • 数据包的具体内容

head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送。

这就像写信,发送者的地址(源地址)就是你家的地址,接收者地址(目标地址)就是对方的收信地址,你家的路由器就相当于邮局。其实在计算机通信中的源地址和目标地址指的是mac地址

Mac地址的由来:

head中包含的源和目标地址由来:Ethernet规定接入Internet的设备都必须具备网卡,发送端的和接收端的地址便是指网卡的地址,即Mac地址。

每块网卡出厂时都被烧录上一个实际上唯一的Mac地址,长度为48位2进制,通常由12位16进制数表示,(前六位是厂商编码,后六位是流水线号)

有了mac地址以后,计算机就可以通信了,假设一个教室就是一个局域网(隔离的网络),这个教室里面有几台计算机,计算机的通信和人的通信是一个道理,把教室里面的人都比作一个个计算机,假设教室里面的人都是瞎子,其实计算机就是瞎子的,计算机通信基本靠吼,现在我要找教室里面的飞哥要战狼2的片,然后我就吼一声,说我要找飞哥要战狼2的片,战狼2的片就属于我的数据,但是我在发的时候我是不是要标识我是谁,我要找谁,我是谁就是我的mac地址,我要找谁就是飞哥的mac地址,这两个地址做数据包的头部,再加上数据战狼2的片就构成了一个数据帧。

这个数据包封装好以后就往外发,到物理层以后就全部转成二级制,往外发是怎么发的呢?就是靠吼。即“我是Edison,我找飞哥要战狼2的片”。这么吼了一嗓子以后,全屋子的人都能听到,这就是广播。

计算机底层,只要在一个教室里(一个局域网),都是靠广播的方式,吼。

局域网的理解:什么是互联网,互联网就是由一个个局域网组成,局域网内的计算机不管是对内还是对外都是靠吼,这就是数据链路层的工作方式—–广播。

广播出去以后,所有人都听得见,所有人都会拆开这个包,读发送者是谁,接收者是谁,只要接收者不是自己就丢弃掉。对计算机来说,它会看自己的Mac地址,飞哥收到以后,他就会把片发给我,发送回来同样采用广播的方式了,靠吼。

同一个教室(同一个局域网)的计算机靠吼来通信,那不同教室的计算机又如何?

比如说局域网1的pc1与局域网2的pc10如何通信?你在教室1(局域网1)吼,教室2(局域网2)的人肯定是听不见的。这就是跨网络进行通信,数据链路层就解决不了这个问题了,这就得靠网络层出面了。

在讲网络层之前,其实基于广播的这种通信就可以实现全世界通信了,你吼一声,如果全世界是一个局域网,全世界的计算机肯定可以听得见,从理论上似乎行得通,如果全世界的计算机都在吼,你想一想,这是不是一个灾难。因此,全世界不能是一个局域网。于是就有了网络层。

网络层:

网络层定义了一个IP协议,

你想,我是这个教室的一个学生,我想找隔壁教室一个叫老王的学生,我也不认识老王,那怎么办,我吼?老王在另外一个教室肯定是听不到的。找教室的负责人,这个教室的负责人就负责和隔壁教室的负责人说话,说我们教室的有个学生要找你们教室的老王。往外传的东西交给负责人就可以了,内部的话上面已经提到,通过广播的方式,对外的东西广播失效。教室的负责人就是网关,网关即网络关口的意思。

Mac地址是用来标识你这个教室的某个位置,IP地址是用来标识你在哪个教室(哪个局域网)。你要跨网络发包你是不是要知道对方的IP地址,比如你要访问百度,你肯定得知道百度服务器的IP地址。计算机在发包前,会判断你在哪个教室,对方在哪个教室,如果在一个教室,基于mac地址的广播发包就OK了;如果不在一个教室,即跨网络发包,那么就会把你的包交给教室负责人(网关)来转发。Mac地址及IP地址唯一标识了你在互联网中的位置。

数据链路层中会把网络层的数据包封装到数数据链路层的数据位置,然后再添加上自己的包头,再发给物理层,物理层发给网关,网关再发给对方教室的网关,对方教室的网关收到后在那个教室做广播。

在数据链路层看,数据封装了两层,跟玩俄罗斯套娃有点类似,一层套了一层。

最终变成

现在来看另一个问题,在吼之前怎么知道对方的Mac地址?这就得靠ARP协议。

ARP协议的由来:在你找飞哥要片之前,你的先干一件事,想办法知道飞哥的Mac地址。即你的机器必须先发一个ARP包出去,ARP也是靠广播的方式发,ARP发送广播包的方式如下:

局域网中怎么获取对方的Mac地址:

肯定要知道对方的IP地址,这是最基本的,就像你要访问百度,肯定得知道百度的域名,域名就是百度的IP地址。自己的IP可以轻松获得,自己的Mac也轻松获取,目标Mac为12个F,我们叫广播地址,表达的意思是我想要获取这个目标IP地址172.16.10.11的机器的Mac地址。Mac为12个F代表的是一种功能,这个功能就是获取对方的MAC地址,计算机的Mac永远不可能是12个F。假设是在本教室广播,一嗓子吼出去了,所有人开始解包,只有IP地址是172.16.10.11的这个人才会返回他的Mac地址,其他人全部丢弃。发回来源Mac改成飞哥自己的Mac地址,同时把飞哥的Mac地址放在数据部分。

跨网络怎么获取对方的Mac地址:

通过IP地址区分,计算机运算判断出飞哥不在同一个教室,目标IP就变成了网关的IP了。网关的IP在计算机上配死了,可以轻松获取。

这样网关就会把它的Mac地址返回给你,然后正常发包

网关帮你去找飞哥,但对用户来说,我们根本就感觉不到网关的存在。

传输层

传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,等多个应用程序,

那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序与网卡关联的编号。

传输层功能:建立端口到端口的通信

补充:端口范围0-65535,0-1023为系统占用端口

tcp协议:

可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

以太网头 ip 头 tcp头 数据

udp协议:

不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

以太网头 ip头 udp头 数据

应用层

应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式 。

应用层功能:规定应用程序的数据格式。

例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。

转:OSI七层协议大白话解读

为什么软件开发项目开发进度延迟、费用超出预算、软件质量不能保证等问题一直在困扰着我们?我从来不认为产生这些问题的主要问题在于项目管理方法掌握不够、在于软件工程理解不透或在于软件开发能力不强,主要问题在于我们常常忽视了软件开发队伍的建设。没有一支好的队伍,怎么可能圆满的完成软件开发项目?回顾多年的开发经历,下面总结一下软件开发队伍建设的一些经验,试图从各个方面来阐述如何打造一支积极主动、和谐稳定、高效创新的软件开发队伍,目的是抛砖引玉,希望大家结合自己的日常项目管理现状,补充、调整,并把你的意见告诉我,Email: unow2005@163.com

1、软件项目经理必须在软件开发团队中树立起自己的权威地位。**

软件项目经理是软件开发团队的领头羊,要成功的带领开发团队圆满完成软件开发任务,必须使全体开发人员信服你的知识结构、管理能力、协调能力和决策能力,所以软件项目经理必须具备相当的专业和管理素质并且勇于承担软件开发项目的成败的责任才能在软件开发团队中树立起自己的威信。

软件项目经理应该站在比开发人员更高的高度来认识所开发的系统和开发的过程,做到统领全局,需要知道但不必非要精通软件开发过程中采用的技术、使用的工具、开发过程中每一个环节的重点和难点,只有这样,才能在关键时候作出准确、正确的决定,从而在软件开发团队中树立起自己的权威地位。

我接触过的很多软件开发组织常犯的错误之一是研发经理经常干预项目经理的管理,其实软件项目经理在软件开发团队中威信的树立同时需要研发经理的支持,研发经理和项目经理要建立一种互信的汇报和指导的互动机制,即使项目经理在管理过程中出现差错,研发经理应该多加指导,避免直接插手软件开发过程,研发经理必须赋予项目经理充分的信任和自主的执行权利。

2、取得公司决策层对软件开发队伍的充分信任和支持。**

在软件开发过程中,特别是规模较大的软件项目,由于涉及面较广、开发周期较长,经常需要跟日常行政管理、市场部门、采购部门、技术支持部门、财务部门等打交道,如果没有公司决策层的支持,跨部门的协作难免会遇到障碍,而公司决策层的支持来自于他们对我们软件开发团队的充分信任,信任我们能够按软件产品的愿景和目标出色的完成开发任务。

公司决策层对我们软件开发团队的信任来自于研发经理和项目经理可信的专业背景和管理技能,来自于开发团队朝气蓬勃的工作精神,来自于开发团队齐心协力的协同工作态度,来自于开发团队强烈的使命感、荣誉感和责任心。

我们有些软件研发经理和项目经理经常在公司决策人员面前挑剔开发人员的不足之处,对开发人员怀抱各种不满,诉说人力资源的欠缺,其后果是公司决策层越来越对我们开发团队失去了信心和信任。研发经理和项目经理应该向公司决策层汇报每个开发人员的长处,汇报我们是如何通过弥补个别开发人员的不足之处来提升团队整体的协同开发能力,让公司决策层充分认识到我们作为一个集体的团结和力量,从而对我们充满信心、信任和支持。

3、在开发团队中建立一种信任和忠诚机制。**

只有我们项目经理对软件开发团队每个成员的真诚信任,才能获得开发人员对项目经理、对项目、对公司的忠诚,所以作为管理人员,首先不要带着成见去猜测开发人员的能力、道德观念、工作态度等,而是真诚的信任他们,真诚的与开发人员进行沟通。只有充分的信任每一个开发人员,相信他们能够出色的完成交给的任务,他们才会全身心的、充满热情的投入到自己的开发工作中去。信任还体现着一种开放的心态,只有信任他们的忠诚度,让每个开发人员理解项目的目标、了解软件的体系结构和功能需求、了解开发团队中其他成员的开发任务和进度,这样开发人员和项目经理之间、开发人员之间才会配合默契,才会感觉到工作是一种幸福而不是一种无奈。

信任意味着给予每个开发人员对自己的工作任务的足够的权限,忠诚意味着每个开发人员会对交给自己的工作任务的成功负责。在召开项目开始阶段的会议的时候,可以讨论并确定每个开发人员的权限,在召开项目开发过程的会议的时候,可以验证每个开发人员是否具备各方面的条件来执行分派的工作任务。不要将任务分派给一个没有权限执行此类任务的开发人员。

4、形成一种为解决问题而不断地召开简短会议的讨论氛围。**

中医有这样的一种说法,那就是“痛则不通,通则不痛”,体现在软件项目管理中就是要经常的沟通,消除所有团队成员之间就某一问题存在的冲突。在软件开发过程中不时地召开简短的讨论,是一种行之有效的方法。

项目经理要在软件开发过程中不断地与每个开发人员进行沟通,询问他们在开发过程中是否遇到了技术上的障碍,了解他们是否对需求或业务存在误解,检查开发人员之间是否就某一方面产生了不同的理解和冲突等。当发现上述问题的时候,项目经理应及时组织相关人员召开简短的会议,指定此问题的主要责任人准备好讨论主题、相关的资料以及演示的PPT,提前3小时递交会议参与者,要求参与讨论人员提前1小时将各自的意见和建议递交主要责任人汇总,并在会议上提出讨论,通过讨论最终达成一致的意见,形成会议纪要作为下一步的开发指导。

5、养成在公开场合肯定开发人员工作成果的好习惯。**

大型软件开发项目往往持续周期很长,在这漫长的开发过程中,如果只是在里程碑的时候给开发人员奖励是远远不够的。项目经理应该在每次的工作例会中表扬那些在此段时间内表现良好、工作效率高、实际化解了很多难题的开发人员,并且经常要邀请研发经理和公司决策层参与这样的表扬,这样开发人员的心理会得到极大的满足。对于在开发过程中存在问题的开发人员,在项目例会时尽量用探讨的方式指出不足之处,共同探讨如何通过改进解决存在的问题,避免在项目例会上当着所有开发人员的面来批评他们。对于个别开发人员情绪的波动,我们可以在私下进行沟通。

在软件开发过程中或在软件开发完成后,由于各种因素的影响,难免会需要对原有系统做变更。变更必然会遭遇开发人员的抵制,特别是那些不影响业务逻辑的变更,比如界面的调整,因为在开发人员的思维观念中会认为自己的开发是成功的、完美的。此时,项目经理首先要肯定开发人员的成果,肯定当前的变更是在他们以前的成功的基础上进行的,这样开发人员会乐意去接受变更。

6、积极培养软件开发队伍的集体荣誉感。**

我们提倡多一份荣誉感,少一份功利性,明确定义成功和失败,强调成功首先是个人的成功,然后才是团队的成功,强调失败首先是团队的失败,然后才是个人的失败。我们要倡导维护知识分子的一份尊严,尊严来自于开发出出色的软件产品,尊严来自于强大的团队凝聚力,尊严来自于公司领导和客户的认可。

一个没有荣誉感的团队是不可能有很好的责任心的,一个没有责任心的团队是不可能开发出优秀的软件产品的。在软件开发团队中要建立软件产品是开发团队集体智慧的结晶这样的理念,每个开发人员在开发过程中的付出很难做到象生产流水线的计件工作一样精确,在开发过程中通过开发人员之间的互帮互助,不但可以消除斤斤计较的不良风气,还能增强彼此的协作氛围、团队的凝聚力和集体的荣誉感。

7、充分识别、发挥、利用软件开发人员的个性,高效完成开发任务。**

软件开发是一种创造性的活动,创造性意味着需要个性的极大张扬,但这种张扬受制于企业的管理制度和软件开发规范,作为软件项目经理应该会同研发经理一起寻找并把握好规范和个性之间的平衡点。在软件开发过程中,项目经理要仔细的观察、分析每个开发人员的个性,不同的个性分配不同的开发内容,这样的任务分配能使每个开发人员感到自己被重视,这样的任务分配能让开发人员感到工作的愉快感,这样的任务分配能使每个开发人员全身心的投入到软件开发之中,压制个性的任务分配方法容易引起开发人员的抵触情绪和消极态度。

我们尊重开发人员的个性,不是宣扬个人英雄主义,其行为受到公司规章制度的制约,其风格受到编码规范的制约,其文档受到各种文档模板的制约。如何保证开发人员的个性能在各种制度的制约下得到最大的发挥,关键在于项目经理与开发人员之间的充分、坦诚、友善的沟通。让开发人员做到自动自发,其结果必然是高效、高质量的完成软件开发任务。

8、建立一种开发人员之间、开发小组之间互相协作的工作氛围。**

在软件开发过程中,难免会遇到各种障碍和困难,从而影响开发进度,管理上的障碍通常需要公司层面进行协调,但对于技术障碍,项目经理应及时组织相关开发人员进行攻关,此时资深开发人员应利用他们的经验和知识积累发挥应有的作用。当某个开发小组或某个开发人员的开发进度有出现延迟的趋势时,项目经理应及时与开发小组或开发人员沟通,并组织其他已经或快要完成任务的小组或人员一起讨论,尽量分担部分工作任务,这种互帮互助的工作氛围将极大的增强团队的凝聚力。

通过分享开发人员各自的知识积累和开发经验,建立开发人员、开发小组之间的互相协作的工作氛围能加快开发人员的成长过程;通过协作克服了开发中遇到的困难能增强开发团队的自信心。

9、研发经理和项目经理必须为团队成员争取利益最大化和提升空间最大化。**

项目经理必须象CEO经营企业一样来经验自己的软件开发团队。我们要把利益让给开发人员,把荣誉让给开发人员,通过出色的完成开发任务来得到公司决策层对项目团队的毫无争议的奖励,从而为团队成员争取最大的经济利益和荣誉。在软件开发项目立项过程中,我们可以明确的提出奖惩措施,同时给出项目的评估方法,公司决策层根据评估方法对项目的开发过程(里程碑)和开发成果(软件产品)进行评估,评估结果作为项目奖惩的依据。

作为研发经理必须观察每一个开发人员,对于工作勤奋、责任心强、具备管理素质、善于沟通的开发人员,要及时提请研发经理和公司决策层得以提升。当开发人员感到领导对自己的工作的认可时,会更加充满激情地投入到开发工作之中。

10、软件开发团队每个成员对软件产品的愿景和要实现的目标拥有一致的理解。**

我接触过的很多软件开发组织为了规避软件项目过分依赖开发人员而带来的技术流失风险,往往只有项目经理拥有设计文档的全部权限,开发人员只允许查阅跟自己要开发的那部分工作有关的设计文档。在这样的开发团队中,一方面开发人员感觉不到公司领导对他们的信任感,往往导致工作积极性不高,严重的产生彼此的信任危机,另一方面由于开发人员对所开发的软件缺乏整体的认识,往往导致软件产品质量得不到保证。

我们强调每个开发人员尽早参与到项目的设计中来,即使没有参与系统设计,我们也会邀请他们一起参与软件系统的规划讨论和旁听系统架构和系统设计的评审。只有每个开发人员对开发的软件产品的目标有一致的理解,他们才会在软件开发过程中各自作出正确的决定。

原文地址:软件项目管理之开发队伍建设篇

先来看一份 docker-compose.yml 文件,不用管这是干嘛的,只是有个格式方便后文解说:

version: '2'
services:
  web:
    image: dockercloud/hello-world
    ports:
      - 8080
    networks:
      - front-tier
      - back-tier

  redis:
    image: redis
    links:
      - web
    networks:
      - back-tier

  lb:
    image: dockercloud/haproxy
    ports:
      - 80:80
    links:
      - web
    networks:
      - front-tier
      - back-tier
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock 

networks:
  front-tier:
    driver: bridge
  back-tier:
driver: bridge

可以看到一份标准配置文件应该包含 version、services、networks 三大部分,其中最关键的就是 services 和 networks 两个部分,下面先来看 services 的书写规则。

1. image

services:
  web:
    image: hello-world

在 services 标签下的第二级标签是 web,这个名字是用户自己自定义,它就是服务名称。
image 则是指定服务的镜像名称或镜像 ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。
例如下面这些格式都是可以的:

image: redis
image: ubuntu:14.04
image: tutum/influxdb
image: example-registry.com:4000/postgresql
image: a4bc65fd

2. build

服务除了可以基于指定的镜像,还可以基于一份 Dockerfile,在使用 up 启动之时执行构建任务,这个构建标签就是 build,它可以指定 Dockerfile 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器。

build: /path/to/build/dir

也可以是相对路径,只要上下文确定就可以读取到 Dockerfile。

build: ./dir

设定上下文根目录,然后以该目录为准指定 Dockerfile。

build:
  context: ../
  dockerfile: path/of/Dockerfile

注意 build 都是一个目录,如果你要指定 Dockerfile 文件需要在 build 标签的子级标签中使用 dockerfile 标签指定,如上面的例子。
如果你同时指定了 image 和 build 两个标签,那么 Compose 会构建镜像并且把镜像命名为 image 后面的那个名字。

build: ./dir
image: webapp:tag

既然可以在 docker-compose.yml 中定义构建任务,那么一定少不了 arg 这个标签,就像 Dockerfile 中的 ARG 指令,它可以在构建过程中指定环境变量,但是在构建成功后取消,在 docker-compose.yml 文件中也支持这样的写法:

build:
  context: .
  args:
    buildno: 1
    password: secret

下面这种写法也是支持的,一般来说下面的写法更适合阅读。

build:
  context: .
  args:
    - buildno=1
    - password=secret

与 ENV 不同的是,ARG 是允许空值的。例如:

args:
  - buildno
  - password

这样构建过程可以向它们赋值。

注意:YAML 的布尔值(true, false, yes, no, on, off)必须要使用引号引起来(单引号、双引号均可),否则会当成字符串解析。

3. command

使用 command 可以覆盖容器启动后默认执行的命令。

command: bundle exec thin -p 3000

也可以写成类似 Dockerfile 中的格式:

command: [bundle, exec, thin, -p, 3000]

4.container_name

前面说过 Compose 的容器名称格式是:<项目名称><服务名称><序号>
虽然可以自定义项目名称、服务名称,但是如果你想完全控制容器的命名,可以使用这个标签指定:

container_name: app

这样容器的名字就指定为 app 了。

5.depends_on

在使用 Compose 时,最大的好处就是少打启动命令,但是一般项目容器启动的顺序是有要求的,如果直接从上到下启动容器,必然会因为容器依赖问题而启动失败。
例如在没启动数据库容器的时候启动了应用容器,这时候应用容器会因为找不到数据库而退出,为了避免这种情况我们需要加入一个标签,就是 depends_on,这个标签解决了容器的依赖、启动先后的问题。
例如下面容器会先启动 redis 和 db 两个服务,最后才启动 web 服务:

version: '2'
services:
  web:
    build: .
    depends_on:
      - db
      - redis
  redis:
    image: redis
  db:
    image: postgres

注意的是,默认情况下使用 docker-compose up web 这样的方式启动 web 服务时,也会启动 redis 和 db 两个服务,因为在配置文件中定义了依赖关系。

6.dns

和 –dns 参数一样用途,格式如下:

dns: 8.8.8.8

也可以是一个列表:

dns:
  - 8.8.8.8
  - 9.9.9.9

此外 dns_search 的配置也类似:

dns_search: example.com
dns_search:
  - dc1.example.com
  - dc2.example.com

7. tmpfs

挂载临时目录到容器内部,与 run 的参数一样效果:

tmpfs: /run
tmpfs:
  - /run
  - /tmp

8. entrypoint

在 Dockerfile 中有一个指令叫做 ENTRYPOINT 指令,用于指定接入点,第四章有对比过与 CMD 的区别。
在 docker-compose.yml 中可以定义接入点,覆盖 Dockerfile 中的定义:

entrypoint: /code/entrypoint.sh

格式和 Docker 类似,不过还可以写成这样:

entrypoint:
    - php
    - -d
    - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so
    - -d
    - memory_limit=-1
    - vendor/bin/phpunit

9.env_file

还记得前面提到的 .env 文件吧,这个文件可以设置 Compose 的变量。而在 docker-compose.yml 中可以定义一个专门存放变量的文件。
如果通过 docker-compose -f FILE 指定了配置文件,则 env_file 中路径会使用配置文件路径。

如果有变量名称与 environment 指令冲突,则以后者为准。格式如下:

env_file: .env

或者根据 docker-compose.yml 设置多个:

env_file:
  - ./common.env
  - ./apps/web.env
  - /opt/secrets.env

注意的是这里所说的环境变量是对宿主机的 Compose 而言的,如果在配置文件中有 build 操作,这些变量并不会进入构建过程中,如果要在构建中使用变量还是首选前面刚讲的 arg 标签。

10. environment

与上面的 env_file 标签完全不同,反而和 arg 有几分类似,这个标签的作用是设置镜像变量,它可以保存变量到镜像里面,也就是说启动的容器也会包含这些变量设置,这是与 arg 最大的不同。
一般 arg 标签的变量仅用在构建过程中。而 environment 和 Dockerfile 中的 ENV 指令一样会把变量一直保存在镜像、容器中,类似 docker run -e 的效果。

environment:
  RACK_ENV: development
  SHOW: 'true'
  SESSION_SECRET:

environment:
  - RACK_ENV=development
  - SHOW=true
  - SESSION_SECRET

11. expose

这个标签与Dockerfile中的EXPOSE指令一样,用于指定暴露的端口,但是只是作为一种参考,实际上docker-compose.yml的端口映射还得ports这样的标签。

expose:
 - "3000"
 - "8000"

在使用Docker过程中,我们会有许多单独使用docker run启动的容器,为了使Compose能够连接这些不在docker-compose.yml中定义的容器,我们需要一个特殊的标签,就是external_links,它可以让Compose项目里面的容器连接到那些项目配置外部的容器(前提是外部容器中必须至少有一个容器是连接到与项目内的服务的同一个网络里面)。
格式如下:

external_links:
 - redis_1
 - project_db_1:mysql
 - project_db_1:postgresql

13. extra_hosts

添加主机名的标签,就是往/etc/hosts文件中添加一些记录,与Docker client的–add-host类似:

extra_hosts:
 - "somehost:162.242.195.82"
 - "otherhost:50.31.209.229"

启动之后查看容器内部hosts:

162.242.195.82  somehost
50.31.209.229   otherhost

14. labels

向容器添加元数据,和Dockerfile的LABEL指令一个意思,格式如下:

labels:
  com.example.description: "Accounting webapp"
  com.example.department: "Finance"
  com.example.label-with-empty-value: ""
labels:
  - "com.example.description=Accounting webapp"
  - "com.example.department=Finance"
  - "com.example.label-with-empty-value"

还记得上面的depends_on吧,那个标签解决的是启动顺序问题,这个标签解决的是容器连接问题,与Docker client的–link一样效果,会连接到其它服务中的容器。
格式如下:

links:
 - db
 - db:database
 - redis

使用的别名将会自动在服务容器中的/etc/hosts里创建。例如:

172.12.2.186  db
172.12.2.186  database
172.12.2.187  redis

相应的环境变量也将被创建。

16. logging

这个标签用于配置日志服务。格式如下:

logging:
  driver: syslog
  options:
    syslog-address: "tcp://192.168.0.42:123"

默认的driver是json-file。只有json-file和journald可以通过docker-compose logs显示日志,其他方式有其他日志查看方式,但目前Compose不支持。对于可选值可以使用options指定。
有关更多这方面的信息可以阅读官方文档:
https://docs.docker.com/engine/admin/logging/overview/

17. pid

pid: "host"

将PID模式设置为主机PID模式,跟主机系统共享进程命名空间。容器使用这个标签将能够访问和操纵其他容器和宿主机的名称空间。

18. ports

映射端口的标签。
使用HOST:CONTAINER格式或者只是指定容器的端口,宿主机会随机映射端口。

ports:
 - "3000"
 - "8000:8000"
 - "49100:22"
 - "127.0.0.1:8001:8001"

注意:当使用HOST:CONTAINER格式来映射端口时,如果你使用的容器端口小于60你可能会得到错误得结果,因为YAML将会解析xx:yy这种数字格式为60进制。所以建议采用字符串格式。

19. security_opt

为每个容器覆盖默认的标签。简单说来就是管理全部服务的标签。比如设置全部服务的user标签值为USER。

security_opt:
  - label:user:USER
  - label:role:ROLE

20. stop_signal

设置另一个信号来停止容器。在默认情况下使用的是SIGTERM停止容器。设置另一个信号可以使用stop_signal标签。

stop_signal: SIGUSR1

21. volumes

挂载一个目录或者一个已存在的数据卷容器,可以直接使用 [HOST:CONTAINER] 这样的格式,或者使用 [HOST:CONTAINER:ro] 这样的格式,后者对于容器来说,数据卷是只读的,这样可以有效保护宿主机的文件系统。
Compose的数据卷指定路径可以是相对路径,使用 . 或者 .. 来指定相对目录。
数据卷的格式可以是下面多种形式:

volumes:
  // 只是指定一个路径,Docker 会自动在创建一个数据卷(这个路径是容器内部的)。
  - /var/lib/mysql

  // 使用绝对路径挂载数据卷
  - /opt/data:/var/lib/mysql

  // 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器。
  - ./cache:/tmp/cache

  // 使用用户的相对路径(~/ 表示的目录是 /home/<用户目录>/ 或者 /root/)。
  - ~/configs:/etc/configs/:ro

  // 已经存在的命名的数据卷。
  - datavolume:/var/lib/mysql

如果你不使用宿主机的路径,你可以指定一个volume_driver。

volume_driver: mydriver

22. volumes_from

从其它容器或者服务挂载数据卷,可选的参数是 :ro或者 :rw,前者表示容器只读,后者表示容器对数据卷是可读可写的。默认情况下是可读可写的。

volumes_from:
  - service_name
  - service_name:ro
  - container:container_name
  - container:container_name:rw

23. cap_add, cap_drop

添加或删除容器的内核功能。详细信息在前面容器章节有讲解,此处不再赘述。

cap_add:
  - ALL

cap_drop:
  - NET_ADMIN
  - SYS_ADMIN

24. cgroup_parent

指定一个容器的父级cgroup。

cgroup_parent: m-executor-abcd

25. devices

设备映射列表。与Docker client的–device参数类似。

devices:
  - "/dev/ttyUSB0:/dev/ttyUSB0"

26. extends

这个标签可以扩展另一个服务,扩展内容可以是来自在当前文件,也可以是来自其他文件,相同服务的情况下,后来者会有选择地覆盖原有配置。

extends:
  file: common.yml
  service: webapp

用户可以在任何地方使用这个标签,只要标签内容包含file和service两个值就可以了。file的值可以是相对或者绝对路径,如果不指定file的值,那么Compose会读取当前YML文件的信息。
更多的操作细节在后面的12.3.4小节有介绍。

27. network_mode

网络模式,与Docker client的–net参数类似,只是相对多了一个service:[service name] 的格式。
例如:

network_mode: "bridge"
network_mode: "host"
network_mode: "none"
network_mode: "service:[service name]"
network_mode: "container:[container name/id]"

可以指定使用服务或者容器的网络。

28. networks

加入指定网络,格式如下:

services:
  some-service:
    networks:
     - some-network
     - other-network

关于这个标签还有一个特别的子标签aliases,这是一个用来设置服务别名的标签,例如:

services:
  some-service:
    networks:
      some-network:
        aliases:
         - alias1
         - alias3
      other-network:
        aliases:
         - alias2

相同的服务可以在不同的网络有不同的别名。

29. 其它

还有这些标签:cpu_shares, cpu_quota, cpuset, domainname, hostname, ipc, mac_address, mem_limit, memswap_limit, privileged, read_only, restart, shm_size, stdin_open, tty, user, working_dir
上面这些都是一个单值的标签,类似于使用docker run的效果。

cpu_shares: 73
cpu_quota: 50000
cpuset: 0,1

user: postgresql
working_dir: /code

domainname: foo.com
hostname: foo
ipc: host
mac_address: 02:42:ac:11:65:43

mem_limit: 1000000000
memswap_limit: 2000000000
privileged: true

restart: always

read_only: true
shm_size: 64M
stdin_open: true
tty: true

关于配置文件的扩展写法会在有空的时候补上,写得太长了。_(xз」∠)_

原文地址:Docker Compose 配置文件详解

前言

不知道你是否遇到过这样的情况,去小卖铺买东西,付了钱,但是店主因为处理了一些其他事,居然忘记你付了钱,又叫你重新付。又或者在网上购物明明已经扣款,但是却告诉我没有发生交易。这一系列情况都是因为没有事务导致的。这说明了事务在生活中的一些重要性。有了事务,你去小卖铺买东西,那就是一手交钱一手交货。有了事务,你去网上购物,扣款即产生订单交易。

事务的具体定义

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。

数据库本地事务

ACID

说到数据库事务就不得不说,数据库事务中的四大特性,ACID:

  • A:原子性(Atomicity)

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

就像你买东西要么交钱收货一起都执行,要么要是发不出货,就退钱。

  • C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

打个比方,你买东西这个事情,是不影响其他人的。

  • D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。

InnoDB实现原理

InnoDB是mysql的一个存储引擎,大部分人对mysql都比较熟悉,这里简单介绍一下数据库事务实现的一些基本原理,在本地事务中,服务和资源在事务的包裹下可以看做是一体的:

我们的本地事务由资源管理器进行管理:

而事务的ACID是通过InnoDB日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过redo log(重做日志)来实现,原子性和一致性通过Undo log来实现。UndoLog的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。 和Undo Log相反,RedoLog记录的是新数据的备份。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。 对具体实现过程有兴趣的同学可以去自行搜索扩展。

分布式事务

什么是分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务产生的原因

从上面本地事务来看,我们可以看为两块,一个是service产生多个节点,另一个是resource产生多个节点。

service多个节点

随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护

这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。

resource多个节点

同样的,互联网发展得太快了,我们的Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。

分布式事务的基础

从上面来看分布式事务是随着互联网高速发展应运而生的,这是一个必然的我们之前说过数据库的ACID四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论:

CAP

CAP定理,又被叫作布鲁尔定理。对于设计分布式系统来说(不仅仅是分布式事务)的架构师来说,CAP就是你的入门理论。

  • C (一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
  • A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。
  • P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

熟悉CAP的人都知道,三者不能共有,如果感兴趣可以搜索CAP的证明,在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。

对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。

顺便一提,CAP理论中是忽略网络延迟,也就是当事务提交时,从节点A复制到节点B,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。同时CAP中选择两个,比如你选择了CP,并不是叫你放弃A。因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA。就算分区出现了你也要为后来的A做准备,比如通过一些日志的手段,是其他机器回复至可用。

BASE

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展

  1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  2. 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
  3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务解决方案

有了上面的理论基础后,这里介绍开始介绍几种常见的分布式事务的解决方案。

是否真的要分布式事务

在说方案之前,首先你一定要明确你是否真的需要分布式事务?

上面说过出现分布式事务的两个原因,其中有个原因是因为微服务过多。我见过太多团队一个人维护几个微服务,太多团队过度设计,搞得所有人疲劳不堪,而微服务过多就会引出分布式事务,这个时候我不会建议你去采用下面任何一种方案,而是请把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。

如果你确定需要引入分布式事务可以看看下面几种常见的方案。

2PC

说到2PC就不得不聊数据库分布式事务中的 XA Transactions。

在XA协议中分为两阶段:

第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.

第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。

优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。

缺点:

  • 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
  • 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,XA协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。

TCC

关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC事务机制相比于上面介绍的XA,解决了其几个缺点: 1.解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。 2.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。 3.数据一致性,有了补偿机制之后,由业务活动管理器控制一致性

对于TCC的解释:

  • Try阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性)

  • Confirm阶段:确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具备幂等设计,Confirm失败后需要进行重试。

  • Cancel阶段:取消执行,释放Try阶段预留的业务资源 Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。

举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。

如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。

如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)

对于TCC来说适合一些:

  • 强隔离性,严格一致性要求的活动业务。
  • 执行时间较短的业务

实现参考:ByteTCC:https://github.com/liuyangming/ByteTCC/

本地消息表

本地消息表这个方案最初是ebay提出的 ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128。

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。

1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。

2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。

3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。

4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。

MQ事务

在RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,下面简单介绍一下MQ事务,如果想对其详细了解可以参考: https://www.jianshu.com/p/453c6e7ff81c。
基本流程如下: 第一阶段Prepared消息,会拿到消息的地址。

第二阶段执行本地事务。

第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。

如果确认消息失败,在RocketMq Broker中提供了定时扫描没有更新状态的消息,如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在rocketmq中是以listener的形式给发送者,用来处理。

如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这个就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失

Saga事务

Saga是30年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 Saga的组成:

每个Saga由一系列sub-transaction Ti 组成 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果,这里的每个T,都是一个本地事务。 可以看到,和TCC相比,Saga没有“预留 try”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

T1, T2, T3, …, Tn

T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n Saga定义了两种恢复策略:

向后恢复,即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。 向前恢复,适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。

这里要注意的是,在saga模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。

还是拿100元买一瓶水的例子来说,这里定义

T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水

C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水

我们一次进行T1,T2,T3如果发生问题,就执行发生问题的C操作的反向。 上面说到的隔离性的问题会出现在,如果执行到T3这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题

可以看见saga模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。也可以在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。

具体实例:可以参考华为的servicecomb

最后

还是那句话,能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。上面对解决方案只是一些简单介绍,如果真正的想要落地,其实每种方案需要思考的地方都非常多,复杂度都比较大,所以最后再次提醒一定要判断好是否使用分布式事务。最后在总结一些问题,大家可以下来自己从文章找寻答案:

  1. ACID和CAP的 CA是一样的吗?
  2. 分布式事务常用的解决方案的优缺点是什么?适用于什么场景?
  3. 分布式事务出现的原因?用来解决什么痛点?

原文地址:再有人问你分布式事务,把这篇扔给他

顶级微服务面试问题

根据Gartner的说法,微服务是云开发的新应用平台。微服务是独立部署和管理的,一旦在容器内实现,它们与底层操作系统的交互很少。 因此,如果您计划在微服务中开始您的职业生涯,那么现在正是潜入技术处于新生状态的时候。因此,为了帮助您准备面试,我提出了微服务面试问题和答案博客。

在这个微服务面试问题博客中,我收集了面试官最常问的问题。这些问题是在咨询微服务和相关技术领域的顶级行业专家后收集的。

如果您最近参加过任何微服务面试,请将这些面试问题粘贴到评论部分,我们会尽快回答。如果您有任何疑问,也可以在下面发表评论,这可能会在您的微服务面试中遇到。

您可以浏览微服务面试问题和答案的录音,我们的讲师已经详细解释了这些主题,并提供了一些示例,可帮助您更好地理解这一概念。

Q1。您对微服务有何了解?

微服务,又称微服务 _架构_,是一种架构风格,它将应用程序构建为以业务领域为模型的小型自治服务集合

通俗地说,你必须看到蜜蜂如何通过对齐六角形蜡细胞来构建它们的蜂窝状物。他们最初从使用各种材料的小部分开始,并继续从中构建一个大型蜂箱。这些细胞形成图案,产生坚固的结构,将蜂窝的特定部分固定在一起。这里,每个细胞独立于另一个细胞,但它也与其他细胞相关。这意味着对一个细胞的损害不会损害其他细胞,因此,蜜蜂可以在不影响完整蜂箱的情况下重建这些细胞。

图1:微服务的蜂窝表示 – 微服务访谈问题

请参考上图。这里,每个六边形形状代表单独的服务组件。与蜜蜂的工作类似,每个敏捷团队都使用可用的框架和所选的技术堆栈构建单独的服务组件。就像在蜂箱中一样,每个服务组件形成一个强大的微服务架构,以提供更好的可扩展性。此外,敏捷团队可以单独处理每个服务组件的问题,而对整个应用程序没有影响或影响最小。

Q2。微服务架构有哪些优势?

图2:微服务的 优点 – 微服务访谈问题

  • 独立开发 – 所有微服务都可以根据各自的功能轻松开发
  • 独立部署 – 基于其服务,可以在任何应用程序中单独部署它们
  • 故障隔离 – 即使应用程序的一项服务不起作用,系统仍可继续运行
  • 混合技术堆栈 – 可以使用不同的语言和技术来构建同一应用程序的不同服务
  • 粒度缩放 – 单个组件可根据需要进行缩放,无需将所有组件缩放在一起

Q3。微服务有哪些特点?

图3:微服务的 特点 – 微服务访谈问题

  • 解耦 – 系统内的服务很大程度上是分离的。因此,整个应用程序可以轻松构建,更改和扩展
  • 组件化 – 微服务被视为可以轻松更换和升级的独立组件
  • 业务能力 – 微服务非常简单,专注于单一功能
  • 自治 – 开发人员和团队可以彼此独立工作,从而提高速度
  • 持续交付 – 通过软件创建,测试和批准的系统自动化,允许频繁发布软件
  • 责任 – 微服务不关注应用程序作为项目。相反,他们将应用程序视为他们负责的产品
  • 分散治理 – 重点是使用正确的工具来做正确的工作。这意味着没有标准化模式或任何技术模式。开发人员可以自由选择最有用的工具来解决他们的问题
  • 敏捷 – 微服务支持敏捷开发。任何新功能都可以快速开发并再次丢弃

Q4。设计微服务的最佳实践是什么?

以下是设计微服务的最佳实践:

图4:设计微服务的最佳实践 – 微服务访谈问题

Q5。微服务架构如何运作?

微服务架构具有以下组件:

图5:微服务 架构 – 微服务面试问题

  • 客户端 – 来自不同设备的不同用户发送请求。
  • 身份提供商 – 验证用户或客户身份并颁发安全令牌。
  • API网关 – 处理客户端请求。
  • 静态内容 – 容纳系统的所有内容。
  • 管理 – 在节点上平衡服务并识别故障。
  • 服务发现 – 查找微服务之间通信路径的指南。
  • 内容交付网络 – 代理服务器及其数据中心的分布式网络。
  • 远程服务 – 启用驻留在IT设备网络上的远程访问信息。

Q6。微服务架构的优缺点是什么?

微服务架构的优点

微服务架构的缺点

自由使用不同的技术

增加故障排除挑战

每个微服务都侧重于单一功能

由于远程呼叫而增加延迟

支持单个可部署单元

增加了配置和其他操作的工作量

允许经常发布软件

难以保持交易安全

确保每项服务的安全性

艰难地跨越各种边界跟踪数据

多个服务是并行开发和部署的

难以在服务之间进行编码

Q7。单片,SOA和微服务架构有什么区别?

图6: 单片SOA和微服务之间的比较 – 微服务访谈问题

  • 单片架构类似于大容器,其中应用程序的所有软件组件组装在一起并紧密封装。
  • 一个面向服务的架构是一种相互通信服务的集合。通信可以涉及简单的数据传递,也可以涉及两个或多个协调某些活动的服务。
  • 微服务架构是一种架构风格,它将应用程序构建为以业务域为模型的小型自治服务集合。

Q8。在使用微服务架构时,您面临哪些挑战?

开发一些较小的微服务听起来很容易,但开发它们时经常遇到的挑战如下。

  • 自动化组件:难以自动化,因为有许多较小的组件。因此,对于每个组件,我们必须遵循Build,Deploy和Monitor的各个阶段。
  • 易感性:将大量组件维护在一起变得难以部署,维护,监控和识别问题。它需要在所有组件周围具有很好的感知能力。
  • 配置管理:有时在各种环境中维护组件的配置变得困难。
  • 调试:很难找到错误的每一项服务。维护集中式日志记录和仪表板以调试问题至关重要。

Q9。SOA和微服务架构之间的主要区别是什么?

SOA和微服务之间的主要区别如下:

SOA

微服务

遵循“ 尽可能多的共享 ”架构方法

遵循“ 尽可能少分享 ”的架构方法

重要性在于 业务功能 重用

重要性在于“ 有界背景 ” 的概念

他们有 共同的 治理 和标准

他们专注于 人们的 合作 和其他选择的自由

使用 企业服务总线(ESB) 进行通信

简单的消息系统

它们支持 多种消息协议

他们使用 轻量级协议 ,如 HTTP / REST 等。

多线程, 有更多的开销来处理I / O.

单线程 通常使用Event Loop功能进行非锁定I / O处理

最大化应用程序服务可重用性

专注于 解耦

传统的关系数据库 更常用

现代 关系数据库 更常用

系统的变化需要修改整体

系统的变化是创造一种新的服务

DevOps / Continuous Delivery正在变得流行,但还不是主流

专注于DevOps /持续交付

Q10。微服务有什么特点?

您可以列出微服务的特征,如下所示:

图7:微服务的特征 – 微服务访谈问题

Q11。什么是领域驱动设计?

图8: DDD原理 – 微服务面试问题

Q12。为什么需要域驱动设计(DDD)?

图9:我们需要DDD的因素 – 微服务面试问题

Q13。什么是无所不在的语言?

如果您必须定义泛在语言(UL),那么它是特定域的开发人员和用户使用的通用语言,通过该语言可以轻松解释域。

无处不在的语言必须非常清晰,以便它将所有团队成员放在同一页面上,并以机器可以理解的方式进行翻译。

Q14。什么是凝聚力?

模块内部元素所属的程度被认为是凝聚力

Q15。什么是耦合?

组件之间依赖关系强度的度量被认为是耦合。一个好的设计总是被认为具有高内聚力低耦合性

Q16。什么是REST / RESTful以及它的用途是什么?

Representational State Transfer(REST)/ RESTful Web服务是一种帮助计算机系统通过Internet进行通信的架构风格。这使得微服务更容易理解和实现。

微服务可以使用或不使用RESTful API实现,但使用RESTful API构建松散耦合的微服务总是更容易。

Q17。你对Spring Boot有什么了解?

事实上,随着新功能的增加,弹簧变得越来越复杂。如果必须启动新的spring项目,则必须添加构建路径或添加maven依赖项,配置应用程序服务器,添加spring配置。所以一切都必须从头开始。

Spring Boot是解决这个问题的方法。使用spring boot可以避免所有样板代码和配置。因此,基本上认为自己就好像你正在烘烤蛋糕一样,春天就像制作蛋糕所需的成分一样,弹簧靴就是你手中的完整蛋糕。

图10: Spring Boot的因素 – 微服务面试问题

Q18。什么是Spring引导的执行器?

Spring Boot执行程序提供了restful Web服务,以访问生产环境中运行应用程序的当前状态。在执行器的帮助下,您可以检查各种指标并监控您的应用程序。

Q19。什么是Spring Cloud?

根据Spring Cloud的官方网站,Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智能路由,领导选举,分布式会话,集群状态)。

Q20。Spring Cloud解决了哪些问题?

在使用Spring Boot开发分布式微服务时,我们面临的问题很少由Spring Cloud解决。

  • 与分布式系统相关的复杂性 – 包括网络问题,延迟开销,带宽问题,安全问题。
  • 处理服务发现的能力 – 服务发现允许集群中的进程和服务找到彼此并进行通信。
  • 解决冗余问题 – 冗余问题经常发生在分布式系统中。
  • 负载平衡 – 改进跨多个计算资源(例如计算机集群,网络链接,中央处理单元)的工作负载分布。
  • 减少性能问题 – 减少因各种操作开销导致的性能问题。

Q21。在Spring MVC应用程序中使用WebMvcTest注释有什么用处?

在测试目标只关注Spring MVC组件的情况下,WebMvcTest注释用于单元测试Spring MVC应用程序。在上面显示的快照中,我们只想启动ToTestController。执行此单元测试时,不会启动所有其他控制器和映射。

Q22。你能否给出关于休息和微服务的要点?

休息

虽然您可以通过多种方式实现微服务,但REST over HTTP是实现微服务的一种方式。REST还可用于其他应用程序,如Web应用程序,API设计和MVC应用程序,以提供业务数据。

微服务

微服务是一种体系结构,其中系统的所有组件都被放入单独的组件中,这些组件可以单独构建,部署和扩展。微服务的某些原则和最佳实践有助于构建弹性应用程序。

简而言之,您可以说REST是构建微服务的媒介。

Q23。什么是不同类型的微服务测试?

在使用微服务时,由于有多个微服务协同工作,测试变得非常复杂。因此,测试分为不同的级别。

  • 底层,我们有面向技术的测试,如单元测试和性能测试。这些是完全自动化的。
  • 中间层面,我们进行了诸如压力测试和可用性测试之类的探索性测试。
  • 顶层, 我们的 验收测试数量很少。这些验收测试有助于利益相关者理解和验证软件功能。

Q24。您对Distributed Transaction有何了解?

分布式事务是指单个事件导致两个或多个不能以原子方式提交的单独数据源的突变的任何情况。在微服务的世界中,它变得更加复杂,因为每个服务都是一个工作单元,并且大多数时候多个服务必须协同工作才能使业务成功。

Q25。什么是Idempotence以及它在哪里使用?

幂等性是能够以这样的方式做两次事情的特性,即最终结果将保持不变,即好像它只做了一次。

用法:在远程服务或数据源中使用 Idempotence,这样当它多次接收指令时,它只处理指令一次。

Q26。什么是有界上下文?

有界上下文是域驱动设计的核心模式。DDD战略设计部门的重点是处理大型模型和团队。DDD通过将大型模型划分为不同的有界上下文并明确其相互关系来处理大型模型。

Q27。什么是双因素身份验证?

双因素身份验证为帐户登录过程启用第二级身份验证。

图11: 双因素认证的表示 – 微服务访谈问题

因此,假设用户必须只输入用户名和密码,那么这被认为是单因素身份验证。

Q28。双因素身份验证的凭据类型有哪些?

这三种凭证是:

图12: 双因素认证的证书类型 – 微服务面试问题

Q29。什么是客户证书?

客户端系统用于向远程服务器发出经过身份验证的请求的一种数字证书称为客户端证书。客户端证书在许多相互认证设计中起着非常重要的作用,为请求者的身份提供了强有力的保证。

Q30。PACT在微服务架构中的用途是什么?

PACT是一个开源工具,允许测试服务提供者和消费者之间的交互,与合同隔离,从而提高微服务集成的可靠性。

微服务中的用法:

  • 用于在微服务中实现消费者驱动的合同。
  • 测试微服务的消费者和提供者之间的消费者驱动的合同。

查看即将到来的批次

Q31。什么是OAuth?

OAuth 代表开放授权协议。这允许通过在HTTP服务上启用客户端应用程序(例如第三方提供商Facebook,GitHub等)来访问资源所有者的资源。因此,您可以在不使用其凭据的情况下与另一个站点共享存储在一个站点上的资源。

Q32。康威定律是什么?

“任何设计系统的组织(广泛定义)都将产生一种设计,其结构是组织通信结构的副本。” – Mel Conway

图13: Conway定律的表示 – 微服务访谈问题

该法律基本上试图传达这样一个事实:为了使软件模块起作用,整个团队应该进行良好的沟通。因此,系统的结构反映了产生它的组织的社会边界。

Q33。合同测试你懂什么?

根据Martin Flower的说法,合同测试是在外部服务边界进行的测试,用于验证其是否符合消费服务预期的合同。

此外,合同测试不会深入测试服务的行为。更确切地说,它测试该服务调用的输入&输出包含所需的属性和所述响应延迟,吞吐量是允许的限度内。

Q34。什么是端到端微服务测试?

端到端测试验证了工作流中的每个流程都正常运行。这可确保系统作为一个整体协同工作并满足所有要求。

通俗地说,你可以说端到端测试是一种测试,在特定时期后测试所有东西。

图14:测试层次 – 微服务面试问题

Q35。Container在微服务中的用途是什么?

容器是管理基于微服务的应用程序以便单独开发和部署它们的好方法_。_您可以将微服务封装在容器映像及其依赖项中,然后可以使用它来滚动按需实例的微服务,而无需任何额外的工作。

图15: 容器的表示及其在微服务中的使用方式 – 微服务访谈问题

Q36。什么是微服务架构中的DRY?

DRY代表不要重复自己。它基本上促进了重用代码的概念。这导致开发和共享库,这反过来导致紧密耦合。

Q37。什么是消费者驱动的合同(CDC)?

这基本上是用于开发微服务的模式,以便它们可以被外部系统使用。当我们处理微服务时,有一个特定的提供者构建它,并且有一个或多个使用微服务的消费者。

通常,提供程序在XML文档中指定接口。但在消费者驱动的合同中,每个服务消费者都传达了提供商期望的接口。

Q38Web,RESTful API在微服务中的作用是什么?

微服务架构基于一个概念,其中所有服务应该能够彼此交互以构建业务功能。因此,要实现这一点,每个微服务必须具有接口。这使得Web API成为微服务的一个非常重要的推动者。RESTful API基于Web的开放网络原则,为构建微服务架构的各个组件之间的接口提供了最合理的模型。

Q39。您对微服务架构中的语义监控有何了解?

语义监控,也称为 综合监控, 将自动化测试与监控应用程序相结合,以检测业务失败因素。

Q40。我们如何进行跨功能测试?

跨功能测试是对非功能性需求的验证,即那些无法像普通功能那样实现的需求。

Q41。我们如何在测试中消除非决定论?

非确定性测试(NDT)基本上是不可靠的测试。所以,有时可能会发生它们通过,显然有时它们也可能会失败。当它们失败时,它们会重新运行通过。

从测试中删除非确定性的一些方法如下:

  1. 隔离
  2. 异步
  3. 远程服务
  4. 隔离
  5. 时间
  6. 资源泄漏

Q42。Mock或Stub有什么区别?

存根

  • 一个有助于运行测试的虚拟对象。
  • 在某些可以硬编码的条件下提供固定行为。
  • 永远不会测试存根的任何其他行为。

例如,对于空堆栈,您可以创建一个只为empty()方法返回true的存根。因此,这并不关心堆栈中是否存在元素。

嘲笑

  • 一个虚拟对象,其中最初设置了某些属性。
  • 此对象的行为取决于set属性。
  • 也可以测试对象的行为。

例如,对于Customer对象,您可以通过设置名称和年龄来模拟它。您可以将age设置为12,然后测试isAdult()方法,该方法将在年龄大于18时返回true。因此,您的Mock Customer对象适用于指定的条件。

Q43。您对Mike Cohn的测试金字塔了解多少?

Mike Cohn 提供了一个名为Test Pyramid的模型这描述了软件开发所需的自动化测试类型。

图16: Mike Cohn的测试金字塔 – 微服务面试问题

根据金字塔,第一层的测试数量应该最高。在服务层,测试次数应小于单元测试级别,但应大于端到端级别。

Q44。Docker的目的是什么?

Docker提供了一个可用于托管任何应用程序的容器环境。在此,软件应用程序和支持它的依赖项紧密打包在一起。

因此,这个打包的产品被称为Container,因为它是由Docker完成的,所以它被称为Docker容器!

Q45。什么是金丝雀释放?

Canary Releasing是一种降低在生产中引入新软件版本的风险的技术。这是通过将变更缓慢地推广到一小部分用户,然后将其发布到整个基础架构,即将其提供给每个人来完成的。

Q46。什么是持续集成(CI)?

持续集成(CI)是每次团队成员提交版本控制更改时自动构建和测试代码的过程。这鼓励开发人员通过在每个小任务完成后将更改合并到共享版本控制存储库来共享代码和单元测试。

Q47。什么是持续监测?

持续监控深入监控覆盖范围,从浏览器内前端性能指标,到应用程序性能,再到主机虚拟化基础架构指标。

Q48。架构师在微服务架构中的角色是什么?

微服务架构中的架构师扮演以下角色:

  • 决定整个软件系统的布局。
  • 帮助确定组件的分区。因此,他们确保组件相互粘合,但不紧密耦合。
  • 与开发人员共同编写代码,了解日常生活中面临的挑战。
  • 为开发微服务的团队提供某些工具和技术的建议。
  • 提供技术治理,以便技术开发团队遵循微服务原则。
  • 这里推荐一下我的Java后端技术群: 834962734,群里有(分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、等学习资源)进群免费送给每一位Java小伙伴,不管你是转行,还是工作中想提升自己能力都可以!

Q49。我们可以用微服务创建状态机吗?

我们知道拥有自己的数据库的每个微服务都是一个可独立部署的程序单元,这反过来又让我们可以创建一个状态机。因此,我们可以为特定的微服务指定不同的状态和事件。

例如,我们可以定义Order微服务。订单可以具有不同的状态。Order状态的转换可以是Order微服务中的独立事件。

Q50。什么是微服务中的反应性扩展?

Reactive Extensions也称为Rx。这是一种设计方法,我们通过调用多个服务来收集结果,然后编译组合响应。这些调用可以是同步或异步,阻塞或非阻塞。Rx是分布式系统中非常流行的工具,与传统流程相反。

原文地址:进大厂必须掌握的50个微服务面试问题!

版权声明:本文为原博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/xinyflove/article/details/83178876

原始标题:crontab的语法规则格式(每分钟、每小时、每天、每周、每月、每年定时执行 规则)

crontab的语法规则格式:

代表意义 分钟 小时 日期 月份 命令
数字范围 0~59 0~23 1~31 1~12 0~7 需要执行的命令

周的数字为 0 或 7 时,都代表“星期天”的意思。

另外,还有一些辅助的字符,大概有下面这些:

特殊字符 代表意义
*(星号) 代表任何时刻都接受的意思。举例来说,0 12 * * * command 日、月、周都是*,就代表着不论何月、何日的礼拜几的12:00都执行后续命令的意思。
,(逗号) 代表分隔时段的意思。举例来说,如果要执行的工作是3:00与6:00时,就会是:0 3,6 * * * command时间还是有五列,不过第二列是 3,6 ,代表3与6都适用
-(减号) 代表一段时间范围内,举例来说,8点到12点之间的每小时的20分都进行一项工作:20 8-12 * * * command仔细看到第二列变成8-12.代表 8,9,10,11,12 都适用的意思
/n(斜线) 那个n代表数字,即是每隔n单位间隔的意思,例如每五分钟进行一次,则:*/5 * * * * command用*与/5来搭配,也可以写成0-59/5,意思相同

在这里插入图片描述

1.每分钟定时执行一次规则:

每1分钟执行: */1 * * * *或者* * * * *

每5分钟执行: */5 * * * *

2.每小时定时执行一次规则:

每小时执行: 0 * * * *或者0 */1 * * *

每天上午7点执行:0 7 * * *

每天上午7点10分执行:10 7 * * *

3.每天定时执行一次规则:

每天执行 0 0 * * *

4.每周定时执行一次规则:

每周执行 0 0 * * 0

5.每月定时执行一次规则:

每月执行 0 0 1 * *

6.每年定时执行一次规则:

每年执行 0 0 1 1 *

7.其他例子

5 * * * * 指定每小时的第5分钟执行一次ls命令

30 5 * * * ls 指定每天的 5:30 执行ls命令

30 7 8 * * ls 指定每月8号的7:30分执行ls命令

30 5 8 6 * ls 指定每年的6月8日5:30执行ls命令

30 6 * * 0 ls 指定每星期日的6:30执行ls命令[注:0表示星期天,1表示星期1,以此类推,也可以用英文来表示,sun表示星期天,mon表示星期一等。]

30 3 10,20 * * ls 每月10号及20号的3:30执行ls命令[注:“,”用来连接多个不连续的时段]

25 8-11 * * * ls 每天8-11点的第25分钟执行ls命令[注:“-”用来连接连续的时段]

*/15 * * * * ls 每15分钟执行一次ls命令 [即每个小时的第0 15 30 45 60分钟执行ls命令 ]

30 6 */10 * * ls 每个月中,每隔10天6:30执行一次ls命令[即每月的1、11、21、31日是的6:30执行一次ls命令。 ]

简介

此项目为自学Python手记,每个支持点都有代码案例说明。

如果你想系统的学习一下Python那么来看下面的文章就对了,深入浅出,带你进入Python的世界。

开发环境

  • Windows 10
  • Python 3.6
  • Visual Studio Code(IDE)

开发环境搭建

本文配置的python是python2和python3并存的,并且pip也是可以用的,python2和python3都配置可让开发者的选择更多一些,当然使用者可根据自己的需求,自由选择安装项目。

windows环境

目录

一、下载安装python

二、配置环境python变量

三、python2和python3并存配置

四、使用vscode运行python程序

正文

一、下载安装python

点击进入,下载地址:https://www.python.org/downloads/

分别下载python2、python3进行安装,一直点击下一步即可。

二、配置环境python变量

编辑系统变量(我的电脑右键属性 => 高级 => 环境变量 => 系统变量),在系统的path变量中添加4个变量目录,假设python2和python3的安装目录分别为:C:\Python27 | C:\Python36,分别添加:“C:\Python27”、“C:\Python27\Scripts”、“C:\Python36”、“C:\Python36\Scripts”.

三、python2和python3并存配置

进入python2安装目录把python.exe 修改成 python2.exe;

进入python3安装目录把python.exe 修改成 python3.exe;

安装python2和python3的pip,使用命令:

python2 -m pip install -U pip

python3 -m pip install -U pip

这样设置之后,就可以分别使用:pip2 / pip3和python2 / python3了.

四、使用vscode运行python程序

1、打开vscode安装插件“python”重启vscode.

2、设置vscode中调试的python路径,用户设置(文件 => 首选项 => 设置)添加:

“python.pythonPath”: “C:/Python36/python3”

3、进入调试界面 => 添加配置 => 选中python(模块),选中配置名:Integrated Terminal/Console

4、F5直接运行代码

基础


python语法特征

空格和冒号是python的重要表达符号,使用空格形成缩进来表示代码块的,示例如下:

1
2
3
4
if True:
print("真")
else:
print("假")

多变量赋值

1
2
3
4
5
x, y, z = 10, 15, 20
print(y) # output:15

x = y = z = 20
print(y) # output:20

变量交换

假设x=2,y=4,现在需要交换x和y的值,其他的编程语言的做法是使用一个中间变量y来辅助,x、y的值交换,但python不需要,交换代码如下:

1
2
3
4
x, y = 2, 4
x, y = y, x
print("x:%d y:%d" % (x, y))
# 输出:x: 4 y: 2

使用input等待用户输入

1
2
msg = input("请输入内容")
print(msg)

查看变量类型

使用内置方法type()查看变量类型,示例如下:

1
2
3
4
5
6
7
8
print(type("老王是个好人"))
# 输出:<class 'str'>

print(type(11))
# 输出:<class 'int'>

print(type(11.0))
# 输出:<class 'float'>

关于++i和i++

python是不支持++i和i++等操作的,可以用+=代替,示例如下:

1
2
3
4
age = 18
age += 1
print(age)
# 输出:19

字符串首字母大写
使用python中的title()内置方法,可以自动把首字母大写,示例如下:

1
2
3
name = "jack"
print(name.title())
# 输出:Jack

使用range()生成列表

python可以使用range()生成列表,格式:range([开始数],结束数,[步长]),开始数不必填默认0,生成的元素不包含结束数,可以指定步长非必填参数,普通示例如下:

1
2
3
4
5
6
7
for item in range(1, 5):
print(item)
# 输出如下:
# 1
# 2
# 3
# 4

指定步长,示例如下:

1
2
3
4
5
for item in range(1, 5, 2):
print(item)
# 输出:
# 1
# 3

随机数

python使用random模块生成随机数。

random.random() :生成0-1之内的随机数

random.randint(0, 2):生成0-2的随机数,包含0和2

示例如下:

1
2
3
4
5
import random

# 循坏10次1-10的随机数
for item in range(10):
print(str(random.randint(1, 10)))

占位符

python中的占位符可以用:%s(替代字符)、%d(替代数字)、%f(替代浮点)来占位,示例如下:

1
2
3
4
5
info = "姓名:%s \n年龄:%d" % ("老王", 18)
print(info)
# 输出如下:
# 姓名:老王
# 年龄:18

format

format和占位符很像,只不过不需要指定占位的类型,格式:

“…{}…{}”.format(“老王”,18)

示例代码如下:

1
2
3
4
#coding=utf-8

info = "姓名:{} 年龄:{}".format("老王", 18)
print(info) #输出:姓名:老王 年龄:18

程序计时器

使用场景:有时候我们需要计算程序的运行时长,可以使用以下代码计算:

1
2
3
4
5
6
7
8
9
10
11
12
import datetime
import time

#开始计时
startTime = datetime.datetime.now()

time.sleep(1)

#结束计时
endTime = datetime.datetime.now()
print(endTime - startTime)
#输出:0:00:01.000791

打印日志

使用关键字“print”输入日志信息,示例如下:

1
print("输出日志")

注释

单行注释:使用“#”作为注释开头

1
2
3
number = 1
# number = 2 (此行没有运行)
print(number) # output:1

多行注释:使用三个单引号(‘)或三个双引号(“)开始,三个单引号(‘)或三个双引号(“)结束

1
2
3
4
5
6
7
8
9
10
11
'''
方式一
这里面也是多行注释
更多注释信息...
'''

"""
方式二
这里面也是多行注释
更多注释信息...
"""

变量


使用

变量格式: 名称 = 值

1
2
3
name = "老王"
print(name)
# 输出:老王

变量命名规则

变量支持:字母、数字、下划线组合。

注意:变量不能使用数字开头,变量区分大小写,以下划线开头的变量标识私有变量,不能被其他模块使用。

变量赋值

变量赋值支持多变量赋值,示例如下:

1
2
3
4
5
6
7
8
9
10
11
name = "老王"
print(name)
# 输出:老王

age, sex = "你猜", "男"
print(age)
# 输出:你猜

x = y = 10
print(x)
# 输出:10

数据类型


所有类型

int(整型)
float(浮点)
complex(复数)
bool(布尔)
str(字符串)
list(列表)
tuple(元祖)
dict(字典)

查看数据类型

在ptyhon中可以使用type()方法返回元素数据类型,示例如下:

1
2
3
4
5
6
7
8
print(type(1))
# 输出:<class 'int'>

print(type(1.0))
# 输出:<class 'float'>

print(type("1.0"))
# 输出:<class 'str'>

数字

1
2
3
x = 3
y = 2
print(x/y) # output:1.5

数字的乘方

python中使用2个乘号(*)来计算数字的乘方,比如3**2,表示3的平方。

1
print(3**2)  # output:9

取模运算符

取模运算符(%),返回两个相除数的余数,实例如下:

1
2
print(4 % 3)  # output:1
print(5 % 3) # output:2

int类型转换

python不支持模糊的数据类型,比如数字加字符,比如:

1
2
3
x = 18
y = "2"
print(x+y)

程序运行报错:TypeError: unsupported operand type(s) for +: ‘int’ and ‘str’

这时候需要把非数字类型转换为数字类型,使用语法”int(str)”,实例如下:

1
2
3
x = 18
y = "2"
print(x+int(y)) # output:20

bool

bool的值为:True | False,注意首字母都要大写。
在python中0,、空字符、空列表[]、空元祖()、空字典都等于False。

字符串

python中可以使用单引号(‘)、双引号(“)、三引号(‘’’或”””)表示。
去除空格:lstrip()、rstrip()、strip()=>去首部、尾部、首尾部,实例如下:

1
2
3
4
5
6
7
8
9
10
11
# lstrip()去字符开始空格
name = ' 老王 '
print("开始"+name.lstrip()+"结束") # output:开始老王 结束

# rstrip()去字符串结束空格
name = ' 老王 '
print("开始"+name.rstrip()+"结束") # output:开始 老王结束

# strip() 去看首尾两端空格
name = ' 老王 '
print("开始"+name.strip()+"结束") # output:开始老王结束

类型转换为字符
使用str(),把非字符类型转换为字符类型,实例如下:

1
print(str(1)+str(2))  # output:12

全部转换大、小写
使用upper()、lower()把字符全部转换成大、小写,实例如下:

1
2
3
name = "Laowang"
print(name.upper()) # output:LAOWANG
print(name.lower()) # output:laowang


列表

python中使用中括号([])表示列表,并用逗号分隔其元素,实例如下:

1
2
list = [111, 222, 333]
print(list) # output:[111, 222, 333]

获取列表长度
使用len()获取列表长度,实例如下:

1
2
list = [111, 222, 333]
print(len(list)) # output:3

获取某个元素
使用下标来获取元素list[0],第一个元素的下标为0,使用-1可以获得最后一个元素,实例如下:

1
2
3
4
list = [111, 222, 333, 444]
print(list[0]) # output:111
print(list[-1]) # output:444
print(list[1:3]) # output:[222, 333]

新增元素
方式一:使用append()添加元素到数组最后,实例如下:

1
2
3
4
list = ["hello", "world"]
list.append("!")
print(len(list)) # output:3
print(list) # output:['hello', 'world', '!']

方式二:insert(n,object)添加元素到指定位置,实例如下:

1
2
3
list = ["hello", "world"]
list.insert(0, "!")
print(list) # output:['!', 'hello', 'world']

删除元素
方式一:使用del(index)关键字,实例如下:

1
2
3
list = ["hello", "world", "!"]
del list[0]
print(list) # output:['world', '!']

方式二:使用pop()删除元素最后一项,实例如下:

1
2
3
list = ["hello", "world", "!"]
list.pop()
print(list) # output:['hello', 'world']

方式三:如果你知道要删除的值,可以使用remove删除,实例如下:

1
2
3
list = ["hello", "world", "!"]
list.remove("hello")
print(list) # output:['world', '!']

in查询
查询元素是否存在数组使用in查询,实例如下,

1
2
3
4
5
6
7
list = ["hello", "world", "!"]
dstr = "hello2"
if dstr in list:
print("存在")
else:
print("不存在")
# output:不存在

最大值、最小值、求和
使用min(),max(),sum(),实例如下:

1
2
3
4
list = [10, 4, 6, 8]
print(min(list)) # output:4
print(max(list)) # output:10
print(sum(list)) # output:28

数组排序
使用sort()对列表永久性排序,实例如下:

1
2
3
list = ["ahref", "focus", "mouse", "click"]
list.sort()
print(list) # output:['ahref', 'click', 'focus', 'mouse']

如果你需要一个相反的排序,使用sort(reverse=True)即可,实例如下:

1
2
3
list = ["ahref", "focus", "mouse", "click"]
list.sort(reverse=True)
print(list) # output:['mouse', 'focus', 'click', 'ahref']

使用sorted(),对列表临时排序,实例如下:

1
2
3
list = ["ahref", "focus", "mouse", "click"]
print(sorted(list)) # output:['ahref', 'click', 'focus', 'mouse']
print(list) # output:['ahref', 'focus', 'mouse', 'click']

颠倒列表使用reverse(),实例如下:

1
2
3
4
5
6
7
list = ["ahref", "focus", "mouse", "click"]
list.reverse()
print(list) # output:['click', 'mouse', 'focus', 'ahref']

list = [222, 111, 333]
list.reverse()
print(list) # output:[333, 111, 222]

注意:reverse()不是按序排序之后再倒叙,而是直接颠倒列表,如果要按序排序在颠倒顺序使用,sort(reverse=True).

切片
你可以处理列表中的部分元素,python中称之为切片.
例如你需要列表中的前两位元素,可以这样使用:

1
2
list = ["ahref", "focus", "mouse", "click"]
print(list[0:2]) # output:['ahref', 'focus']

[x:y]其中x表示开始下标,截取包含开始下标,y表示结束下标,截取时不包含结束下标,可以使用负数,代表列表的倒数几位,实例如下:

1
2
3
4
list = ["ahref", "focus", "mouse", "click"]
print(list[:2]) # output:['ahref', 'focus']
print(list[:]) # output:['ahref', 'focus', 'mouse', 'click']
print(list[:-1]) # output:['ahref', 'focus', 'mouse']


元祖(tuple)

python中有括号()表示元祖,是一种只读的列表类型,元素值不能被修改。
实例如下:

1
2
3
4
list = ("ahref", "focus", "mouse", "click")
print(list[1]) # output:focus

list[1] = "myfocuse" # output:报错,元祖的元素不能被修改

注意:元祖的元素虽然不能修改,但元祖的接受变量是运行修改的,实例如下:

1
2
3
4
5
list = ("ahref", "focus", "mouse", "click")
print(list) # output:('ahref', 'focus', 'mouse', 'click')

list = ("dbclick", "keyup")
print(list) # output:('dbclick', 'keyup')

上面的代码表示把元素(“ahref”, “focus”, “mouse”, “click”)赋值给list变量,但是list变量值的修改是合法的。


字典

python中用{}表示字典,字典是用键值对表示的,和列表相比,字典是无序的对象集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
dict = {}
dict["name"] = "laowang"
dict["age"] = 18

print(dict) # output:{'name': 'laowang', 'age': 18}
print(dict["name"]) # laowang
print(dict.keys()) # output:dict_keys(['name', 'age'])
print(dict.values()) # output:dict_values(['laowang', 18])

for key, value in dict.items():
print("key:"+key)
# output:key:name
# output:key:age


类型转换方法集合

chr(i) 把一个ASCII数值,变成字符
ord(i) 把一个字符或者unicode字符,变成ASCII数值
oct(x) 把整数x变成八进制表示的字符串
hex(x) 把整数x变成十六进制表示的字符串
str(obj) 得到obj的字符串描述
list(seq) 转换成列表   
tuple(seq) 转换成元祖  
dict() 转换成字典   
int(x) 转换成一个整数
float(x) 转换成一个浮点数
complex(x) 转换成复数

条件判断和循环


条件判断

条件判断的重要值是True和False,注意首字母大写,示例如下:

1
2
3
4
5
if True:
print("真")
else:
print("假")
# 输出:真

非真判断

非真判断使用not关键字,示例如下:

1
2
3
4
5
if not True:
print("True")
else:
print("False")
# 输出:False

多情况判断

多情况判断使用if/elif/else,示例如下:

1
2
3
4
5
6
7
8
9
10
age = 18
if age < 16:
print("青少年")
elif age < 18:
print("青年")
elif age < 60:
print("成人")
else:
print("老年")
# 输出:成人

python用空格缩进代表代码块,所以要主要代码缩进.

满足多条件

使用and关键字,示例如下:

1
2
3
4
5
6
7
age = 18
name = "laowang"
if age == 18 and name == "laowang":
print("良好少年")
else:
print("不良少年")
# 输出:良好少年

至少满足一种条件

使用or关键字,示例如下:

1
2
3
4
5
6
7
age = 18
name = "laowang"
if age == 18 or name == "xiaoli":
print("良好少年")
else:
print("不良少年")
# 输出:良好少年

False值
python中0、空字符串、空列表、空元祖值、空字典都为false.

循环

for循环
基础示例如下:

1
2
3
4
5
6
list = ["focus", "mouse", "click"]
for item in list:
print(item)
# 输出:focus
# 输出:mouse
# 输出:click

break跳出循环,实例如下:

1
2
3
4
5
6
list = ["focus", "mouse", "click"]
for item in list:
if item == "mouse":
break
print(item)
# 输出:focus

continue跳过该次循环,实例如下:

1
2
3
4
5
6
7
list = ["focus", "mouse", "click"]
for item in list:
if item == "mouse":
continue
print(item)
# 输出:focus
# 输出:click

使用enumerate获取下标

1
2
3
4
5
6
7
list = ["focus", "mouse", "click"]
for index, item in enumerate(list):
print("index:{} item:{}".format(index, item))
# 输出如下:
# index:0 item:focus
# index:1 item:mouse
# index:2 item:click

while循环
基础示例如下:

1
2
3
4
5
6
num = 1
while num < 3:
print(num)
num = num+1
# 输出:1
# 输出:2

在while中break和continue同样有效,和上文for循环作用相同,请参考上文。

函数和类


函数
函数(有些语言称之为方法)是组织好的,可以复用的功能代码段。
python定义函数的格式:

1
2
def 函数名(参数):
函数体

基础示例如下:

1
2
3
4
5
6
7
# 自定义数值相加函数
def mySum(x, y):
return x+y


print(mySum(4, 4)) # output:8
print(mySum(3, 1)) # output:4

传递可变对象
传递可变对象指的是传递了对象,在函数体被重新赋值后原来的值也发生了改变,示例如下:

1
2
3
4
5
6
7
8
9
def updateList(ls):
ls.append("laowang")
return ls


list = ["hello"]
updateList(list)
print(list)
# 输出:['hello', 'laowang']

原来的列表的值也被修改了,如果不想改变原来值的情况下,可使用[:],传递切片副本,示例如下:

1
2
3
4
5
6
7
8
9
def updateList(ls):
ls.append("laowang")
return ls


list = ["hello"]
updateList(list[:])
print(list)
# 输出:['hello']

缺省参数
缺省参数表示调用函数的时候是可以不传递当前参数值,而使用默认参数值的,示例如下:

1
2
3
4
5
6
def showInfo(name, sex="男", age=18):
print("姓名:%s\t性别:%s\t年龄:%d" % (name, sex, age))


showInfo(name="老王") # 输出:姓名:老王 性别:男 年龄:18
showInfo(name="老王", age=19) # 输出:姓名:老王 性别:男 年龄:19

注意:有默认值的参数一定要放在无默认值参数的后面。

指定参数名称
调用函数的时候,可以指定参数名称,从而用户可以不用关注调用参数的位置顺序,示例如下:

1
2
3
4
5
def showInfo(name, sex):
print("姓名:%s 性别:%s" % (name, sex))


showInfo(sex="男", name="老王") # 输出:姓名:老王 性别:男

注意:如果要指定参数的名称,那么所有的非缺省参数都必须全部指定参数名称,python不支持,部分指定参数名的使用方式。

不定参数
不定参数使用*参数名来表示,示例如下:

1
2
3
4
5
6
7
8
def doPrint(name, *list):
print(name)
for item in list:
print(item)


doPrint("老王", "你好") # 输出:老王 你好
doPrint("老王", "你好", "大家好") # 输出:老王 你好 大家好

同时存在缺省参数和不定参数示例:

1
2
3
4
5
6
7
8
9
10
11
12
def doPrint(name, age=18, *list):
print("姓名:%s 年龄:%d" % (name, age))
for item in list:
print(item)


doPrint("老王", 19, "你好", "世界")

# 输出如下:
# 姓名:老王 年龄:19
# 你好
# 世界

注意:不定参数任何情况下都要放在参数最后方。


python中用class关键声明类,推荐使用驼峰命名法,首字母大写。
python中的类格式:

1
2
3
4
class 类名():
def __init__(self,参数):
self.参数 = 参数
其他方法体

示例如下:

1
2
3
4
5
6
7
8
9
10
11
class Car():
def __init__(self, name):
self.name = name

def printInfo(self, color):
print("车型:%s 颜色:%s" % (self.name, color))


mycar = Car("福特")
mycar.printInfo("白色")
# 输出:车型:福特 颜色:白色

注意:构造函数为init(self,参数)此方法是不能省略的。

类继承,方法重写
类继承语法格式:

1
class 子类名称(父类名称):

类继承&重写方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car():
def __init__(self, name):
self.name = name

def printInfo(self, color):
print("车型:%s 颜色:%s" % (self.name, color))


class WhiteCar(Car):
def __init__(self, name):
# 初始化父类构造函数
super().__init__(name)

def printInfo(self, mileage):
print("车型:%s 颜色:白色 行驶里程:%dKM" % (self.name, mileage))


wCar = WhiteCar("福特")
wCar.printInfo(1000)

注意:super().init(参数)初始化父类构造函数不能省。

模块

python代码存储的文件叫做模块。

import语句

import模块引入
首先,定义一个简单cat.py文件,示例如下:

1
2
3
4
5
# coding=utf-8


def sayHi():
print("喵喵喵~")

在其他地方使用sayHi()方法,示例如下:

1
2
3
4
import cat

cat.sayHi()
# 输出:喵喵喵~

from…import语句

from…import模块引入的格式是:

1
from 模块 import 函数集合...

首先,定义一个简单cat.py文件,示例如下:

1
2
3
4
5
6
7
8
9
# coding=utf-8


def sayHi():
print("喵喵喵~")


def showColor():
print("白色")

在其他地方调用sayHi()方法和showColor()方法,示例如下:

1
2
3
4
5
6
7
from cat import showColor as color, sayHi as hi

hi()
# 输出:喵喵喵

color()
# 输出:白色

小技巧:
1.使用as可重命名函数名。
2.导入一个模块的所有方法可以使用:from 模块 import *

文件操作


读取文件

读取文件使用python内置方法open()打开文件,使用.read()读取全部内容,示例如下:

1
2
3
4
path = "c:\py.txt"
fi = open(path, "r")
print(fi.read())
fi.close()

with语法

with是python2.5引入的自动释放资源的语法模式,确保使用过程中不管是否发生了异常,都会释放资源.
使用with读取文件,是不需要自己手动close的,示例如下:

1
2
with open(path) as fi:
print(fi.read())

逐行读取文件
.read()是读取文件的全部内容,使用.readlines(),示例如下:

1
2
3
with open(path, "r") as fi:
lines = fi.readlines()
print(len(lines))

open方法的模式

上面的示例可以看出来,open(目录,操作模式)的时候必须指定操作模式,open的操作模式:

读取模式:’r’(默认模式) | 写入模式:’w’ | 附加模式:’a’.

python模式是读取,所以”r”可以省略,示例如下:

1
2
3
4
path = "c:\py.txt"
fi = open(path)
print(fi.read())
fi.close()

文件写入

文件写入分为两种方式,一种是覆盖(w),另一种是追加(a)。

文件覆盖的写入,代码如下:

1
2
with open(path, "w") as fi:
fi.write("第一行\n第二行\n第三行")

文件的追加,代码如下:

1
2
with open(path, "a") as fi:
fi.write("\n第1行\n第2行\n第3行")

注意:文件写入,如果文件不存在会重建,不会报错。

更多文件操作

os.getcwd():获取当前运行目录

os.listdir(path):获取指定目录下的列表

os.path.exists(path):检查是否存在文件或文件夹

os.mkdir(path):创建文件夹,已经存在文件会报错

os.remove(path):删除文件夹,只能删除文件夹

os.rename(old, new):文件重命名

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os

# 获取当前程序运行目录
print(os.getcwd())

# 获取指定目录下的列表
print(os.listdir("E:\\server"))

# 检查是否存在文件或文件夹
print(os.path.exists("E:\\test\\a.txt"))

# 创建文件夹,已经存在文件会报错
os.mkdir("E:\\test1")

# 删除文件夹,只能删除文件夹
os.remove("E:\\test1")

# 文件重命名
os.rename("E:\\test\\a.txt", "E:\\test\\b.txt")

异常处理

python中使用try/except/finally关键字处理异常.

语法格式:

1
2
3
4
5
6
7
8
try:
代码
except 异常类型1:
处理
except 异常类型2:
处理
finally:
代码(没有异常)

基础示例:

1
2
3
4
5
6
7
8
9
10
11
12
path = "c:\pytest.txt"
try:
fi = open(path)
try:
fi.read()
finally:
fi.close()
print("最后都会执行")
except FileNotFoundError:
print("找不到相应的文件")
except IOError:
print("文件操作失败IOError")

异常类型

BaseException 所有异常的基类

SystemExit 解释器请求退出

KeyboardInterrupt 用户中断执行(通常是输入^C)

Exception 常规错误的基类

StopIteration 迭代器没有更多的值

GeneratorExit 生成器(generator)发生异常来通知退出

StandardError 所有的内建标准异常的基类

ArithmeticError 所有数值计算错误的基类

FloatingPointError 浮点计算错误

OverflowError 数值运算超出最大限制

ZeroDivisionError 除(或取模)零 (所有数据类型)

AssertionError 断言语句失败

AttributeError 对象没有这个属性

EOFError 没有内建输入,到达EOF 标记

EnvironmentError 操作系统错误的基类

IOError 输入/输出操作失败

OSError 操作系统错误

WindowsError 系统调用失败

ImportError 导入模块/对象失败

LookupError 无效数据查询的基类

IndexError 序列中没有此索引(index)

KeyError 映射中没有这个键

MemoryError 内存溢出错误(对于Python 解释器不是致命的)

NameError 未声明/初始化对象 (没有属性)

UnboundLocalError 访问未初始化的本地变量

ReferenceError 弱引用(Weak reference)试图访问已经垃圾回收了的对象

RuntimeError 一般的运行时错误

NotImplementedError 尚未实现的方法

SyntaxError Python 语法错误

IndentationError 缩进错误

TabError Tab 和空格混用

SystemError 一般的解释器系统错误

TypeError 对类型无效的操作

ValueError 传入无效的参数

UnicodeError Unicode 相关的错误

UnicodeDecodeError Unicode 解码时的错误

UnicodeEncodeError Unicode 编码时错误

UnicodeTranslateError Unicode 转换时错误

Warning 警告的基类

DeprecationWarning 关于被弃用的特征的警告

FutureWarning 关于构造将来语义会有改变的警告

OverflowWarning 旧的关于自动提升为长整型(long)的警告

PendingDeprecationWarning 关于特性将会被废弃的警告

RuntimeWarning 可疑的运行时行为(runtime behavior)的警告

SyntaxWarning 可疑的语法的警告

UserWarning 用户代码生成的警告

垃圾回收gc


python的垃圾收回机制不像c和c++是开发者自己管理维护内存的,python的垃圾回收是系统自己处理的,所以作为普通的开发者,我们不需要关注垃圾回收部分的内容,如果想要深层次理解python请继续看下文。

python垃圾回收机制
Python的GC模块主要运用了引用计数来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”解决容器对象可能产生的循环引用的问题。通过分代回收以空间换取时间进一步提高垃圾回收的效率。

引用计数

原理:当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1,当对象的引用计数减少为0时,就意味着对象已经再没有被使用了,可以将其内存释放掉。

优点:引用计数有一个很大的优点,即实时性,任何内存,一旦没有指向它的引用,就会被立即回收,而其他的垃圾收集技术必须在某种特殊条件下才能进行无效内存的回收。

缺点:但是它也有弱点,引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的,这显然比其它那些垃圾收集技术所带来的额外操作只是与待回收的内存数量有关的效率要低。同时,引用技术还存在另外一个很大的问题-循环引用,因为对象之间相互引用,每个对象的引用都不会为0,所以这些对象所占用的内存始终都不会被释放掉。

标记-清除

标记-清除只关注那些可能会产生循环引用的对象,显然,像是PyIntObject、PyStringObject这些不可变对象是不可能产生循环引用的,因为它们内部不可能持有其它对象的引用。Python中的循环引用总是发生在container对象之间,也就是能够在内部持有其它对象的对象,比如list、dict、class等等。这也使得该方法带来的开销只依赖于container对象的的数量。

原理:

  1. 寻找跟对象(root object)的集合作为垃圾检测动作的起点,跟对象也就是一些全局引用和函数栈中的引用,这些引用所指向的对象是不可被删除的;
  2. 从root object集合出发,沿着root object集合中的每一个引用,如果能够到达某个对象,则说明这个对象是可达的,那么就不会被删除,这个过程就是垃圾检测阶段;
  3. 当检测阶段结束以后,所有的对象就分成可达和不可达两部分,所有的可达对象都进行保留,其它的不可达对象所占用的内存将会被回收,这就是垃圾回收阶段。(底层采用的是链表将这些集合的对象连接在一起);

缺点:标记和清除的过程效率不高。

分代回收

原理:将系统中的所有内存块根据其存活时间划分为不同的集合,每一个集合就成为一个“代”,Python默认定义了三代对象集合,垃圾收集的频率随着“代”的存活时间的增大而减小。也就是说,活得越长的对象,就越不可能是垃圾,就应该减少对它的垃圾收集频率。那么如何来衡量这个存活时间:通常是利用几次垃圾收集动作来衡量,如果一个对象经过的垃圾收集次数越多,可以得出:该对象存活时间就越长。

多线程

python是一门解析性语言,python的解析器默认也是单线程的,但python3提供几个用于多线程编程的模块,_thread、threading

python2中的thread已经废弃,为了兼容性在python3中使用_thread代替,_thread提供了原始的线程操作和简单的锁,推荐使用threading模块。

_thread模块

_thread模块实现多线程,使用_thread.start_new_thread()方法实现,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import _thread
import time


def sayHi(name):
num = 0
while num < 5:
num += 1
time.sleep(1)
print("%s执行——现在时间:%s\n" % (name, time.strftime(
"%Y-%m-%d %H:%M:%S", time.localtime(time.time()))))


try:
_thread.start_new_thread(sayHi, ("线程1",))
_thread.start_new_thread(sayHi, ("线程2",))
except:
print("线程启动失败")

# 保持主线程10s后退出
time.sleep(10)

# 输出结果如下:
# 线程1执行——现在时间:2018-04-12 14: 30: 38
# 线程2执行——现在时间:2018-04-12 14: 30: 38
# 线程1执行——现在时间:2018-04-12 14: 30: 39
# 线程2执行——现在时间:2018-04-12 14: 30: 39
# 线程2执行——现在时间:2018-04-12 14: 30: 40
# 线程1执行——现在时间:2018-04-12 14: 30: 40
# 线程1执行——现在时间:2018-04-12 14: 30: 41
# 线程2执行——现在时间:2018-04-12 14: 30: 41
# 线程2执行——现在时间:2018-04-12 14: 30: 42
# 线程1执行——现在时间:2018-04-12 14: 30: 42

threading模块

threading除了包含_thread的所有方法外,还提供了其他方法:

threading.currentThread():返回当前线程变量

threading.enumerate():返回正在运行的线程列表

threading.activeCount():返回当前正在运行的线程数量和len(threading.enumerate())效果相同

run():用于表示线程活动的方法

start():启动线程活动

join([time]):等待至线程结束

isAlive():线程是否是活跃的

getName():返回线程名

setName():设置线程名

threading基础使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
import threading


class myThreading(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name

def run(self):
time.sleep(1)
print("%s 时间:%s" %
(self.name, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))))


td1 = myThreading("threading1")
td2 = myThreading("threading2")
td1.start()
td2.start()

td1.join()
td2.join()

# 输出:
# Thread-1 时间:2018-04-14 11:19:54
# Thread-2 时间:2018-04-14 11:19:54

threading线程同步

threading使用threading.Lock()来同步线程,threading.Lock()包含锁定的方法acquire()和解锁的方法release()来同步线程,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
import threading


class myThreading(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name

def run(self):
# 上锁
threadLock.acquire()
time.sleep(1)
print("%s 时间:%s" %
(self.name, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))))
# 解锁
threadLock.release()


# 声明线程同步锁
threadLock = threading.Lock()

td1 = myThreading("threading1")
td2 = myThreading("threading2")
td1.start()
td2.start()

td1.join()
td2.join()

时间模块

python中使用时间需要导入time模块,使用time.time()方法获取当前时间戳,示例如下:

1
2
3
4
5
# 导入time模块
import time

print(time.time())
# 输出:1523584077.842348

格式化时间

格式化时间,使用time中的strftime(),示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 导入time模块
import time

print(time.time())
# 输出:1523584077.842348

print(time.localtime(time.time()))
# 输出:time.struct_time(tm_year=2018, tm_mon=4, tm_mday=13, tm_hour=9, tm_min=50, tm_sec=12, tm_wday=4, tm_yday=103, tm_isdst=0)

# 时间格式化
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))
# 输出:2018-04-13 09:52:10

程序计时器

使用场景:有时候我们需要计算程序的运行时长,使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
import datetime
import time

#开始计时
startTime = datetime.datetime.now()

time.sleep(1)

#结束计时
endTime = datetime.datetime.now()
print(endTime - startTime)
#输出:0:00:01.000791

格式化符号说明

%y 两位数的年份表示(00-99)

%Y 四位数的年份表示(000-9999)

%m 月份(01-12)

%d 月内中的一天(0-31)

%H 24小时制小时数(0-23)

%I 12小时制小时数(01-12)

%M 分钟数(00=59)

%S 秒(00-59)

%a 本地简化星期名称

%A 本地完整星期名称

%b 本地简化的月份名称

%B 本地完整的月份名称

%c 本地相应的日期表示和时间表示

%j 年内的一天(001-366)

%p 本地A.M.或P.M.的等价符

%U 一年中的星期数(00-53)星期天为星期的开始

%w 星期(0-6),星期天为星期的开始

%W 一年中的星期数(00-53)星期一为星期的开始

%x 本地相应的日期表示

%X 本地相应的时间表示

%Z 当前时区的名称

%% %号本身

http模块

python中的http/https请求使用urllib库,使用urllib的request模块的发送get和post请求。

get请求

请求网页地址并返回网页html内容,示例如下:

1
2
3
4
5
6
7
8
9
10
from urllib import request


def getHtml(url):
with request.urlopen(url) as r:
data = r.read()
return data.decode("utf-8")


print(getHtml("http://vipstone.cnblogs.com"))

对返回的数据进行编码处理data.decode(“utf-8”)即可。

post请求

post请求并传递参数,对参数进行encode处理,示例如下:

1
2
3
4
5
6
7
8
9
10
from urllib import request, parse


params = parse.urlencode([("name", "老王"), ("pwd", "123456")])
req = request.Request("http://127.0.0.1:8360/video/login")
req.add_header("User-Agent", "Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25")

with request.urlopen(req, data=params.encode("utf-8")) as r:
data = r.read()
print(data.decode("utf-8"))

如上所示,需要使用urllib的parse对参数进行编码处理,也可以给http头添加内容。

常用内置模块

常用内置模块列表:

  • os
  • sys
  • json

os模块

os.getcwd() #获取当前程序目录

os.listdir(‘dirname’) #列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印

os.remove() #删除一个文件

os.rename(“oldname”,”newname”) #重命名文件/目录

os.path.isfile(path) #如果path是一个存在的文件,返回True,否则返回False

os.path.exists(path) #如果path存在,返回True;如果path不存在,返回False

os.path.getatime(path) #返回path所指向的文件或者目录的最后存取时间

os.path.getmtime(path) #返回path所指向的文件或者目录的最后修改时间

sys模块

sys.exit(n) #退出程序,正常退出时exit(0)

sys.version #获取Python解释程序的版本信息

sys.maxint #最大的Int值

sys.platform #返回操作系统平台名称

json模块

json模块用于字符串 和 python数据类型间进行转换

json模块提供了四个功能:dumps、dump、loads、load

dumps、dump #把对象转换成str

loads、load #把str转换成json

原文地址:自学Python手记

最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下:

我一直觉得不要盲目相信权威,比如不能一谈起领域驱动设计,就一定认为国外的那个Eric Evans写的那本书中的一些概念就一定是正确的,认为领域驱动设计就一定是聚合,聚合根,实体,值对象等概念。我们要有自己的思想,要有自己判断真正的领域模型该是什么样子的勇气和追求。

  1. “领域驱动设计” = “问题域模型驱动领域建模” + “领域建模驱动软件实现”
  2. 问题域建模的过程就是业务领域分析的过程,对于企业而言就是业务架构的分析和建立过程,这里不包含任何OO的设计成分,主要从组织、流程和业务能力三个维度来分析业务。
  3. 记住很多模式没有什么用处,带着问题在模式中寻找答案才是正确的使用方式,让那种解决方案的思想融入到你的模型当中,然后彻底地忘掉那些所谓的模式名词。
  4. 好的领域建模应该具有柔性,能够伴随着用户一起成长。
  5. 这让我意识到业务建模应该回归自然:一谈起来建模技术,就离不开国外提出的OO、EDA之类的东西,其实我们的老祖宗早就有了“摸脉”的说法,现在的SOA、ESB之类的东西是不是就像打造一个企业的“神经脉络”,而“OO”是不是就像“神经元”,它们之间的通讯就是靠生物电脉冲,这就是消息驱动。
  6. 《领域驱动设计》一书中只是强调了业务的水平分割,然而在大项目里还有垂直分割,注意垂直分割不完全等同于包的划分。目前有一种非常错误的做法,就是一上来就开始对象建模,然后再进行归类划分模块;正确的做法应该是前期以确认领域边界功能为主,后期以确认领域内的对象模型为主。关于领域的切分,《领域驱动设计》没有过多谈及,其实方法就是不断对企业业务知识的学习和分析。当你对一个业务认识不清的时候,最好的办法就是不同企业环境下去分析这个业务,那这个业务的所有发展变化就清楚了,这就像那些生物学家总是喜欢通过长期的野外考查来学习知识。这个工作做好了,项目就成功了50%。
  7. 领域的边界就是服务,也是对外提供服务的唯一入口。领域服务和领域对象模型是一个业务领域的2个不同侧面。领域服务强调是从外向内看,反映了“外部对业务领域的使用功能”;领域对象模型强调业务领域就像一个独立的具有一定自主能力的生命体,反映了“业务领域的内部运行机制”。领域对象模型的功能是不能对外暴露的,不然会造成外部对领域对象的耦合。
  8. 不要一说“面向关系设计”就是错的。因为用户的角度看,业务本来就是面向关系和过程的,这非常自然;而从设计看,业务是不同主体在相互作用。这就是为什么越靠近用户的地方面向关系和过程的设计味道越浓的原因了。
  9. “类和职责”的叫法让我总感觉比较僵化,像是没有生命的死细胞一样,觉得这是一种西医模式。我更崇尚一种中医模式,强调建模是动态的、基于场景交互的,应该用更自然的还原业务本来面目的眼光去审视建模过程,也就是说“有机的业务建模”实际上就是“技术建模”的问题域建模,而“技术建模”只是“业务建模”的技术落地而已。
  10. 关于建模工具,像“用例图,流程图,状态图之类”的并不是我理想的建模工具,虽然他们确实能表达一些东西。我理想的业务建模工具应该是能从角色(组织或者人)、流程、业务能力三个维度立体地、动态地分析描述业务模型,希望可以是一个动态的3D视图和流图,并可以按不同的维度分析展开。
  11. 那对于我们做企业业务建模,终极目标应该是个什么样子呢?我认为这个终极目标一定是非常复杂的(也就是我原来常说的大项目场景),因为只有在复杂的场景下才能真正检验各种建模技术的偏颇。想想看,我们的建模目标应该向谁学习。论坛也有人说过,“自然的就是最好的”。是啊,经历过亿万年才进化出来的模型难道不值得我们学习吗,难道不是我们的目标模型吗!呵呵,答案已经呼之欲出了,就是“仿生物建模”。是的,没错,如果说利用我们的建模技术能够去构建出一个复杂的仿真人,具备人的一些特征和功能的话,那这种建模技术就是完美的。
  12. 企业的业务建模可以分为两个层面,”宏观建模”和“微观建模”。“宏观建模”是指首先要对企业做一个整体的信息化规划,对企业进行整体的的业务架构建模,其成果就是业务组件。其中的方法论可以参照IBM的CBM,不过IBM好像也只是咨询,真正的落地还要靠自己对CBM的领悟。Evans的DDD主要属于“微观建模”部分。对于“微观建模”,我认为分为2个方面:“结构化建模”和“行为建模”,这是一体两面。我觉得Evans对DDD总结了几个关键的要素:实体、值对象、聚合、工厂和存储,但其中还少了一个非常重要和关键的要素:“事件”。
  13. 众所周知,人体是由很多细胞构成的,那细胞之间是如何作用的呢,其实就是“刺激”和“响应”。其中“刺激”就是“事件”,所以“事件”是业务模型本来就应该具备的要素,而不是什么技术层面的东西。从“事件”角度看,“职责的本质就是事件的响应”。
  14. “结构化建模”是指建模中除了静态的实体和值对象的结构关系外,从“事件”角度看,实体或者值对象还具备一些“本能的反应”,比如”手指会弯曲”。而“行为建模”是指通过神经中枢(消息总线)来控制不同对象的本能反应来完成一个复杂的组合,比如”用手弹钢琴”。
  15. “一上来先识别类,然后就考虑怎么分配职责”的做法是一种本末倒置的僵化的静态建模。这种方式往往让人落入一种只注重“形”而忽略“意”(业务本来的面目)的怪圈。我认为建模分为2个阶段,第一个是“有机的业务建模”,第二个是“技术建模”。“业务建模”和“技术建模”的区别是:“业务建模”完全是业务语言,不含任何技术成分,比如“类、值对象和职责”的概念,在“技术建模”中才涉及那些概念。“业务建模”做好了,“技术建模”就成了自然而然的东西了。“有机的业务建模”的一个非常重要的特征就是“有机”,就是强调业务模型中的每一个“业务主体”都是一个生命体。这些业务主体都有“生命特征”,在一定条件下受到外界的刺激就会发生一定的行为。这些业务主体构成了整个业务模型。当然,这些业务主体的规模和层次不同:比较大的业务主体就是“领域主体”,他有明确的领域边界;而领域主体的对外界响应其实是通过内部的“业务核心主体”的协作来完成的,有的看见的见摸得着————具有业务实体的特点,有的比较抽象————控制规则和流程,当然有的“业务核心主体”可能是个多核细胞。“有机的业务建模”有一个非常重要的工作就是“检查业务模型的生命健康状况”。需要注意的是这个业务建模可能是通过不完善的用户需求建立起来的,所以我们需要构建多种场景通过不同的刺激去看这个领域主体内部的响应中是否有“异常情况”,如果有那就要开刀了。关于“技术建模”,我认为“对内功能和对外功能要分开”,领域对象不建议同时承担对内和对外的功能,但小项目除外。服务对外要封装和隐藏领域内部的东西,提供的是服务接口;同时服务要负责领域内部对象的行为组装。从另外一个层面看,服务是需求,领域对象模型是实现。值对象的本质就是反映业务主体的不同业务特征,可能是业务主体的临时状态(比如用户的发帖数)或者属于另一业务主体的状态值。从微观看,聚合是交互最紧密的业务对象的封装,从宏观看,聚合是一个具有明确业务边界的独立业务核心主体。所以聚合只是形式,业务模型的正确建立才是决定要素。
  16. 相互作用原则全面、深刻地揭示了事物之间的因果联系,是因果关系在逻辑上的充分展开。在客观世界的普遍联系链条中,原因和结果经常互移其位、相互转化。受原因作用的事物在发生变化的同时也反作用于原因,从而把因果性关系转变为相互作用的关系。其中每一方都作为另一方的原因并同时又作为对立面的反作用的结果表现出来。整个物质世界就是各种物质存在普遍相互作用的统一整体,相互作用是事物的真正的终极原因,在它之外没有也不可能有使它运动和发展的原因。相互作用也是系统内部诸要素的关系和联系的形式。要素之间相互作用的方式构成系统存在的基础,系统中要素的相互作用是决定系统发展方向的因素。相互作用只有借助于特殊的物质载体才能实现,相互作用的内容取决于组成要素的物质层次和性质。例如,现代生物学把相互作用划分为分子的、细胞的、器官的、机体的、种的、生物圈等不同水平的形式。社会生活是最复杂的相互作用的形式。 相互作用是客观的、普遍的。具体的相互作用是整个物质世界相互作用链条的环节和部分,相互作用的普遍性和绝对性通过无限多样的具体的相互作用而体现出来。相互作用是事物的属性、结构、规律存在和发展的条件。相互作用范畴具有重要的方法论意义。认识事物意味着认识它们的相互作用,要揭示事物的本质属性,就必须研究事物之间具体的相互作用的特殊性。相互作用的实质是矛盾以及矛盾诸方面的相互依存和斗争。在诸多因素的相互作用中,必有一种起着主导的决定的作用。在实际工作中,只有认清事物之间相互作用的特点和规律性,才能认识和把握事物的本质。 “事件”这个词的确有技术范畴的嫌疑,但说到“相互作用”、“输入和输出”,大家都会明白的。过去的OO过于僵化、教条,偏重于静态场景,忽略了客观世界运动和相互作用的本质规律,而最近出现新技术和对OO的反思“正在回归自然”。
  17. “世界是有事物及其活动组成”或者“世界由事物及其相互作用组成”是非常朴素、直观的世界观。
  18. 建议学习“经过亿万年进化而来的客观存在的生物控制模型”,我们都知道“人体是通过神经网络来控制不同的肌肉、骨骼来做出各种复杂的动作的”。“刺激和响应”是客观存在的,而“场景”只是我们对客观世界的主观认识。“刺激和响应”,举个例子,人的最外层的皮肤就如同“门面”,用来接受外界的“刺激”,内部再由“大脑”下达指令,指挥各个“肢体”进行“响应”。 “神经网络”是被这层皮肤覆盖加以保护。 “神经元”向“皮肤”注册可能完成的“响应”,而“皮肤”接受“刺激”后由大脑进行反馈而回调“神经元”,“神经元”再完成各个指令动作。
  19. 业务建模应该遵守客观规律。
  20. DDD强调“充血模型”。在复杂业务中,这很有用,可以提高对象的可复用性。但是要注意,“充血模型”虽然很好,但其背后却隐藏玄机。复杂场景中,如果所有的行为都放到了对象中,那领域对象就会变得沉重无比,带来各种副作用。同时,描述状态的值对象往往不是一蹴而就,而是充满着变化,难以把握。当业务不明时,”贫血模型”容易把握,这也就是为什么现在”充血模型“不多,而”贫血模型”大行其道的原因。
  21. DDD认为”领域对象应该是有行为的”,大家都认可。如果说”贫血模型”造成了对象和行为的分离的话;那么”充血模型”也是有问题的,因为DDD中简单地认为把行为放到对象中就行了,殊不知这其实是关于对象和行为之间的一种静态地僵化的看法。而DCI对这种思想进行了修正,认为对象在场景中才具有行为。对于DCI中的对象(数据),我认为DCI中的对象(数据)是一种裸对象,但却不是简单意义上的”贫血对象”。DCI中的对象和行为是一种动态关系,是依赖于场景而存在的。这也就是说对象不会无缘无故的产生行为,(呵呵,那是神经病),对象具有行为一定是有意义的。也许有人担心场景会变成”贫血对象”模式下service那样的噩梦,造成对象和行为的分离。其实不用担心,在DCI中可以用”事件”把对象和行为联系在一起就避免了”贫血对象”的问题。呵呵,这是不是应了中国的一句老话:”分久必合,合久必分”。
  22. 对于DDD中的聚合,我也有不同的理解。其实DDD提出聚合的概念是为了保证领域内对象之间的一致性问题。但是我对DDD中对”聚合”这个概念的落地形式表示质疑,DDD特别强调聚合根的封装性,然而这可能会导致领域内对象之间的逻辑强耦合。也许有人说领域内部的对象是高内聚的,这样做没关系。但是在实战中,领域模型内部的值对象往往存在着变数,这是我们认识客观世界的必然规律。然而这会导致领域模型的不稳定性。所以我认为及时领域内部的对象也应该注意低耦合,这个问题同样需要靠事件来解决,事件才是保证领域对象一致性的关键。
  23. “一致性”究竟是什么呢,我认为“一致性”是事物相互作用的本质内在联系,也就是在一定场景下外界刺激在沿着一定路径传递而导致一系列对象的变化。所以“外界不可以绕过根实体直接修改状态”并不能反应这一本质,因为外界刺激并不全都是先作用在根对象上面的。在我看来,这种非本质的封装反而会造成耦合,尤其是采用“直接调用”的形式。应该说,“直接调用”是造成对象耦合最大根源,因为“直接调用”是在强调对象的上下级关系,这很生硬。如果我们换一种方式,用一种平等的心态去看待对象间作用关系,用“告诉我做什么”的方式而让对象间解耦。
  24. 在思考语言范式时,我曾这样想过,面向对象,行为与属性绑得太紧,面向过程,行为与属性放得的太松。但这里不是仅仅选择“分”或“合”那么简单,“贫血模型”与“充血模型”实际上与“面向过程”与“面向类(对象)”的矛盾是相似的。“贫血对象”是将“行为”与“属性”完全放开的一种表达,而“充血对象”则又矫枉过正,把“行为与属性”绑得太紧。类是表达共性的概念,而对象则是充满个性,而且这些个性是依赖场景的,离开场景将失去意义。所以,在“充血模型”中,用类表达对象时,实际是将“个性”统统视为“共性”,在任何具体的场景中,对象的角色或职责都已经定义好了,这显然是不合适,因为一个对象可以多种角色参与不同的活动或场景(可能使得类继承体系非常庞大和复杂),而且在参与新的活动或者场景时,以“类”及“继承”的方式定义对象则更是力不从心。而在“贫血模型”中,则将“共性”统统视为“个性”,这是抹掉“共性”的做法,与“充血模型”抹掉“个性”的做法刚好相反。前者是“白马非马”,后者则是“白马即马”。都没有协调好“共性”与“个性”的关系。因此真正自然的“领域模型”应该是这样的,如果对象的某些行为在任何场景都是通用的,那么就放在领域中去,将其绑定,这是尊重“共性”的约束;如果对象的某些依赖于具体的场景,那么则在具体的场景中注入相应的行为,赋予对象相应的角色,这是尊重“个性”的自由。所以,对象的行为该不该放入“领域模型”,我们要先分析一下这些行为是对象所固有的,还是依赖于场景的,如果是固有的,即是共性的,就放入领域模型(domain),如果不是则延迟在具体的场景(service)中注入,赋予其角色的个性(DCI)。
  25. 设计模式可以在领域模型中使用(domain),也可以在具体业务场景(service)中使用。设计模式是在局部、微观层面的一种支持变化的机制,在具体业务场景中使用再合适不过了。将来可能会出现的现象是,在领域层(domain)各个模型中用的更多是“结构型”模式,而在业务层或服务层(service)的各个场景中用得更多的是“行为型”模式,两者都可以使用“创建型”模式。
  26. 不要把业务和领域等同,业务是客观存在,领域是主观认识。
  27. 领域和场景是不同的。领域是宏观层面的,场景是微观层面的,虽然它们有相似点。
    如果可以用一个领域模型来描述“企业”的所有业务,我当然希望是这样的,但问题是有谁可以做到,那就是共产主义了,完全可以凭此打败微软、IBM之类的公司成为老大了。领域“有界无边”,说起来拗口,简单说领域虽然有界,但其边界时可以扩展的,领域虽然有界,但其边界是无形的。比如说音乐界不断发展,但界还是存在,虽然界也是一种主观认识的划分。因为音乐界不能独立于人类的所有活动。
  28. 领域的边界是由用户的需求来决定的,用户的需求是动态可扩展,所以领域的边界也如此。所谓领域语言,就是从有所需的用户心中提炼出来的。用户需求相对于领域来说是一个“客观存在”。
  29. 领域虽然是对客观存在主观上一种划分,但不是一成不变的。一个领域模型是根据业务(用户需求)对领域的一种素描,业务不同,素描也就不同。
  30. 我们应该学会逆向思考:
    1.服务是什么?自己在过去的项目中用过服务吗?如果不用,会怎么样?如果用了,用好了吗,出现了DDD指出的问题吗?
    2.聚合是什么?为什么要用聚合?自己用过聚合吗?DDD的聚合采取的形式是什么?除了DDD中的聚合,还有别的聚合吗?
    对DDD其他的要素可以以此类推。其实DDD不太适合初学者和小项目,DDD是经历过很多项目的失败和成功才总结出来的。所以我是反对把DDD当成一种理论学习的,在实战中不懂得东西不要乱用,但是一定要感悟,也就是说你可以先不去用DDD,但可以在实践中感悟DDD的思想和要素,然后明悟真伪。所以我发了这么多帖子,希望大家从自己的实战去印证,共鸣。这里不是没事无聊的闲谈,而是基于实践的关于DDD的灵魂的交流。
  31. DDD是对一些复杂IT项目成功要素的总结而形成的一种理论。正确地理解DDD是关键,只要成功运用DDD的思想和要素就可以了,绝不是照搬。就算没见过DDD,也可能在实践中运用过DDD的思想和要素。
  32. 领域建模与设计模式的思想不是只有高手才能领悟,就像数学与哲学思想不是只由数学家和哲学家才能领悟。思想与知识是两个相关又不同的范畴,建模思想与技术知识/经验同样如此。不要因为新手知识的不足,而认为新手就不能领悟思想。如果出现这种情况,我只有两种理解:a)这个老手不懂;b)这个老手懂,但不想告诉你,可能出于自私,也可能觉得你还不适合。
  33. 不同于设计模式的关注点在如何设计模型以支持需求的变化,分析模式的关注点如何将需求转化为模型。Martin Fowler的《分析模式–可复用的对象模型》、Peter Coad的《对象模型–策略、模式、应用》等等关于分析模式的著作,都尚未拜读,加上经验实在匮乏,所以本不想说。可是对于Peter Coad的《Java Modeling in Color with UML》一书中的四色模型,一见如故,极其简单的四色模型,在我心里已经将其作为自己关于分析模式的核心认知,这里说说自己对四色原型的理解(再次感谢Jdon让我遇见了四色模型)。
    四色模型中有四个最核心的概念(刚好设计模式也是运用了OO的四个基本概念,无巧不成书呀),分别是MI(Moment Interval)、Role、PPT(Party Place Thing)、Desc(Description), 不同设计模式运用的OO的Abstraction、Encapsulation、Inheritance、Polymorphism四个基本概念在中文有很好的翻译:抽象、封装、继承、多态。这四个词,直译起来,似乎没什么感觉:瞬间-间隔时间、角色、聚会场所-物件、描述。查阅一些资料,其描述我不是十分满意,这里我尝试用简短的言语解释其本质:MI描述活动,Role代表参与活动的事物;PPT代表未参与活动的事物,Desc描述事物。举个例子,比如一件商品,商品本身就是PPT,商品的类别信息、厂家等商品的元信息就是Desc,如果这件商品为顾客所购买,此件商品就变成了Role(当然顾客此时也是Role),而顾客购买商品这个活动就是MI。
    世界可以简单理解为由事物及其运动(MI)构成,事物又有动静之分,动即参与活动之事物(Role),静即未参与活动之事物(PPT),此外,可能还需要描述事物的信息(Desc)。为了将原型形象化,我们可以根据色彩的冷暖动静之感,将活动(MI)涂上活跃的红色,将参与活动的事物(Role)涂以暖色黄色,将未参于活动的事物(PPT)涂以安静的绿色,将描述事物的信息(Desc)涂以冷色蓝色。这样四色原型就诞生了,通过四色原型,我们可以像孩子拼接积木一样,一块一块地拼接出需求模型的原型了。
  34. 分析模式将需求转化为原型,设计模式支持需求模型的变化,而实现模式为何而生?实现模式关注的是代码本身,我们在分析和设计上付出的努力,终究要落实在代码上。分析和设计再好的模型,也可能被充满smell的代码掩盖或破坏。Kent Beck在《实现模式》一书提出了77个实现模式,6条原则和3种价值观。77个实现模式涉及一些具体的代码规范的建议,暂且不论;6条原则,局部化影响、最小化重复、将逻辑与数据捆绑、对称性、声明式表达和变化率,也暂且不议;3种价值观:沟通、简单、灵活,我也只想说说其中的一点:沟通,因为个人认为这是实现模式最根本的意义所在。Kent Beck在书中说:过了20年,把别人当作空气一样的编程方式才在我眼中褪尽了颜色。作为一位programmer,代码的作用不仅在于与机器交流,更重要的是与别的programmer交流,将“沟通”的价值观贯彻到代码的编写中,是值得每个programmer坚持去做的事情。上面所说的三本很薄的书,可以作为关于分析、设计、实现模式的认知基础,值得收藏。
  35. “领域相对于用户需求是主观的”,这句话隐含了一个假设或参照,我们对领域认识的来源于用户需求,可是用户需求来自哪里呢?用户需求同样来自于领域,来自于对领域的诉求!之前我的另一个观点,“领域是对用户需求的素描”,这句话同样隐含了一个前提,用户需求接近于领域的本质,我们对领域的认识可以通过对用户需求进行素描而实现。这里我重新确定领域、领域建模、用户需求三者的关系。
    1)领域是客观的。
    2)领域模型是主观的,体现了程序员对领域的认识,是程序员心中对领域的素描。
    3)用户需求是主观的,体现了用户对领域的认识,是用户心中对领域的素描。
    4)这点非常重要,就是“领域模型”与“用户需求”的关系,要展开来讲。“用户需求”是对领域的“素描”,用户的需求来自对领域的“诉求”,这些诉求往往是深刻的,因为其来源于用户对领域长期观察和使用的经验,比起我们程序员,一般更完整、更真实地接近领域的本质。我们对“用户需求进行素描”,就是“借鉴用户的宝贵经验”,可以更快、更好地素描客观领域,这可以说是一条认识未知领域的捷径。但是当用户需求不明朗或不清晰时,我们需要超越“用户需求”,对领域进行深入的摸索,去寻求更清晰的视角,对领域进行刻画。
  36. 关于人(参与者)是否应该包含在领域模型之内?领域模型(程序员对领域的认识)要包容用户需求(用户对领域的认识),这就像一个杯子要装满一杯水,我们在制作杯子时,制作的是空杯子,即要把水倒出来,之后才能装下水;再比如,一座房子要住人,我们在建造房子时,建造的房子是空的,唯有空的才能容乃人的居住。从这个意义上来说,领域模型要将用户(参与者)排除在外,这是《老子》的“无之以为用”观点的一种应用。
  37. 对于对象职责的分配以前看过一本书<<UML和模式应用>>这本书将的非常好。里面有GRASP原则–通用职责分配软件模式(General Responsibility Assignment Software patterns),它里面讲了以下几个模式:
    1)创建者(Creator) :
    决定对象应该有谁来创建的问题。一般情况下是包含类创建被包含的类,比如Order创建OrderLine等。
    2)信息专家(Information expert):
    用此模式来确定如何给对象分配职责的问题。一般把职责分配给那些包含此职责有关信息的对象。这样也体现了高内聚性模式。这里面其实也是将行为和对象的数据统一起来,分配某个对象职责,就看看当前的对象里面有没有完成此职责的数据,如果有就可以分配职责。
    3)低耦合(Low coupling)
    面向对象的设计,讲究对外封装,对内解耦,一个模块或者一个类,我们暴漏给外界的接口是粗粒度的,经过封装的,而模块或者对象内部需要进行细分,进行解耦,类与类,模块与模块之间耦合度越低,那么可扩展性就越好,维护起来也容易,不会造成牵一发而动全身的局面。
    如何使得类于类之间低耦合呢,我觉得迪米特法则非常重要,迪米特法则就是通常说的“不要和陌生人说话”,一般类的行为主要可以由以下几种形式来实现:
    A. 方法参数中的对象,通过调用方法参数中对象方法来实现职责
    B. 对象自身的方法,通过对象自身的方法实现职责
    C. 对象内部聚合的对象,通过对象内部聚合的方法实现职责。
    D. 方法创建或者实例化的对象方法,通过对象自身创建的对象的方法实现职责
    以上四种方式都是我们可以接受的,下面这种方式,我们一般要避免,比如a.methodA().methodB(),调用者对象调用了methodA方法返回的对象的方法,这样就会使得调用对象和methodB所在对象发生了耦合,此时我们可以完全将methodB的调用封装在a里,那么此时调用者就只与a通信,而不是和a,以及methodB所在对象都耦合在一起。
    4)控制器(Controller).
    控制器模式主要解决将系统事件处理的职责分配谁的问题,控制器模式指出将处理系统事件的职责分配给控制器类,比如MVC模式中的C就属于这种模式,还比如Struts中的ActionServlet这个Front controller(前段控制器 J2EE核心模式一书).
    5)高内聚(High Cohesion)
    一个类如果是高内聚的,那么类和类之间也将变的松耦合,如果类和类之间耦合太多,那么势必类就丧失了内聚性,因此我们在分配职责的时候,高内聚和低耦合是相辅相成的。
    6)多态性(polymorphism)
    多态性就是OO中的多态,多态其实也是一种具体依赖抽象原则的体现,比如我们有一个产品有个打折策略,而打折策略非常多,那么这个时候怎么办呢?我们当然是定义一个抽象的打折接口,然后将不同的打折策略实现这些接口,而不是让每个不同的打折策略具有不同的接口。
    7)纯虚构(pure fabrication)
    我们在面向对象的分析和设计当中,难免会遇到一些行为不能很好的分配给实体或者domain model,那么这个时候就需要虚构出一些概念,比如DDD中的Repository就属于一种纯虚构的东东。
    8)间接性(indirection)
    间接性从表面就可以知道它是为了解耦的,比如RBAC,通过引入Role将User和Permission解耦,这样通过引入一个间接的对象,使得原来紧耦合的对象变的松耦合,这样也方便维护,同时也是为低耦合原则服务的。
    9)防止变异(protected variations)
    这个原则我们也经常遇到,按照我的理解,它应该是封装,类似于DDD中的防腐层,软件系统中的子模块或者子系统应该将不确定的因素封装在内部,而不要因为子模块而影响到外部系统。其实这也是一种封装思想的体现。

原文地址:关于DDD领域驱动设计的理论知识收集汇总