数据结构:图Graph

图的简介

图(Graph)结构是一种非线性的数据结构,图在实际生活中有很多例子,比如交通运输网,地铁网络,社交网络,计算机中的状态执行(自动机)等等都可以抽象成图结构。图结构比树结构复杂的非线性结构。

图结构构成

1.顶点(vertex):图中的数据元素,如图一。

2.边(edge):图中连接这些顶点的线,如图一。

1 

图一

所有的顶点构成一个顶点集合,所有的边构成边的集合,一个完整的图结构就是由顶点集合和边集合组成。图结构在数学上记为以下形式:

G=(V,E)   或者   G=(V(G),E(G))

其中 V(G)表示图结构所有顶点的集合,顶点可以用不同的数字或者字母来表示。E(G)是图结构中所有边的集合,每条边由所连接的两个顶点来表示。

图结构中顶点集合V(G)不能为空,必须包含一个顶点,而图结构边集合可以为空,表示没有边。

图的基本概念

1.无向图(undirected graph)

       如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。典型的无向图,如图二所示。由于无向图中的边没有方向性,这样我们在表示边的时候对两个顶点的顺序没有要求。例如顶点VI和顶点V5之间的边,可以表示为(V2, V6),也可以表示为(V6,V2)。

2

图二  无向图

对于图二无向图,对应的顶点集合和边集合如下:

V(G)= {V1,V2,V3,V4,V5,V6}

E(G)= {(V1,V2),(V1,V3),(V2,V6),(V2,V5),(V2,V4),(V4,V3),(V3,V5),(V5,V6)}

2.有向图(directed graph)

一个图结构中,边是有方向性的,那么这种图就称为有向图,如图三所示。由于图的边有方向性,我们在表示边的时候对两个顶点的顺序就有要求。我们采用尖括号表示有向边,例如<V2,V6>表示从顶点V2到顶点V6,而<V6,V2>表示顶点V6到顶点V2。

3

图三  有向图

对于图三有向图,对应的顶点集合和边集合如下:

V(G)= {V1,V2,V3,V4,V5,V6}

E(G)= {<V2,V1>,<V3,V1>,<V4,V3>,<V4,V2>,<V3,V5>,<V5,V3>,<V2,V5>,<V6,V5>,<V2,V6>,<V6,V2>}

*注意:无向图也可以理解成一个特殊的有向图,就是边互相指向对方节点,A指向B,B又指向A。

3.混合图(mixed graph)

一个图结构中,边同时有的是有方向性有的是无方向型的图。

在生活中混合图这种情况比较常见,比如城市道路中有些道路是单向通行,有的是双向通行。

4.顶点的度

连接顶点的边的数量称为该顶点的度。顶点的度在有向图和无向图中具有不同的表示。对于无向图,一个顶点V的度比较简单,其是连接该顶点的边的数量,记为D(V)。 例如,图二所示的无向图中,顶点V5的度为3。而V6的度为2。

     对于有向图要稍复杂些,根据连接顶点V的边的方向性,一个顶点的度有入度出度之分。

  •  入度是以该顶点为端点的入边数量, 记为ID(V)。
  •  出度是以该顶点为端点的出边数量, 记为OD(V)。

     这样,有向图中,一个顶点V的总度便是入度和出度之和,即D(V) = ID(V) + OD(V)。例如,图三所示的有向图中,顶点V5的入度为3,出度为1,因此,顶点V5的总度为4。

5.邻接顶点

邻接顶点是指图结构中一条边的两个顶点。 邻接顶点在有向图和无向图中具有不同的表示。对于无向图,邻接顶点比较简单。例如,在图二所示的无向图中,顶点V2和顶点V6互为邻接顶点,顶点V2和顶点V5互为邻接顶点等。

对于有向图要稍复杂些,根据连接顶点V的边的方向性,两个顶点分别称为起始顶点(起点或始点)和结束顶点(终点)。有向图的邻接顶点分为两类:

  • 入边邻接顶点:连接该顶点的边中的起始顶点。例如,对于组成<V2,V6>这条边的两个顶点,V2是V6的入边邻接顶点。
  • 出边邻接顶点:连接该顶点的边中的结束顶点。例如,对于组成<V2,V6>这条边的两个顶点,V6是V2的出边邻接顶点。

6.无向完全图

如果在一个无向图中, 每两个顶点之间都存在条边,那么这种图结构称为无向完全图。典型的无向完全图,如图四所示。
4

图四  无向完全图

理论上可以证明,对于一个包含n个顶点的无向完全图,其总边数为n(n-1)/2

比如图四总边数就是5,例子:5*(5-1)/2=10。

7.有向完全图

如果在一个有向图中,每两个顶点之间都存在方向相反的两条边,那么这种图结构称为有向完全图。典型的有向完全图,如图五所示。

5

图五 有向完全图

 

理论上可以证明,对于一个包含n的顶点的有向完全图,其总的边数为:n(n-1)

这是无向完全图的两倍,这个也很好理解,因为每两个顶点之间需要两条边。

8.有向无环图(DAG图)

如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图。有向无环图可以利用在区块链技术中。

9.无权图和有权图

6

这里的权可以理解成一个数值,就是说节点与节点之间这个边是否有一个数值与它对应,对于无权图来说这个边不需要具体的值。对于有权图节点与节点之间的关系可能需要某个值来表示,比如这个数值能代表两个顶点间的距离,或者从一个顶点到另一个顶点的时间,所以这时候这个边的值就是代表着两个节点之间的关系,这种图被称为有权图;

10.图的连通性

图的每个节点不一定每个节点都会被边连接起来,所以这就涉及到图的连通性,如下图:

7

可以发现上面这个图不是完全连通的。

11.简单图 ( Simple Graph)

对于节点与节点之间存在两种边,这两种边相对比较特殊

1.自环边(self-loop):节点自身的边,自己指向自己。

2.平行边(parallel-edges):两个节点之间存在多个边相连接。

8

这两种边都是有意义的,比如从A城市到B城市可能不仅仅有一条路,比如有三条路,这样平行边就可以用到这种情况。不过这两种边在算法设计上会加大实现的难度。而简单图就是不考虑这两种边。

@Feign通过URI指定请求地址

@FeignClient使用起来非常方便,只需要启用配置即可使用,但是有时候需要请求不通地址的服务,这时可以指定一个URI参数。

配置:

@EnableFeignClients(basePackages = "com.test.openapi.rpc")
@Configuration
public class FeignConfig extends Compact implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {

    }
}

@FeignClient接口

@FeignClient(
        name = "testCloudService",
        url = "https://api.test.com"
)
@RequestMapping(
        value = "/api",
        headers = {
                "clientId=MY CLIENTID",
                "version=1.0"
        }
)
public interface ITestCloudService {

    @RequestMapping(value = "/call", method = RequestMethod.GET)
    ResultResp<Map<String, Object>> region(URI uri, @RequestHeader("token") String token);

    @RequestMapping(value = "/test", method = RequestMethod.POST)
    ResultResp<Map<String, Object>> login(URI uri, @RequestBody Map body, @RequestHeader("token") String token);
}

如果是GET方法,需要指定一连串参数,可以使用注解@SpringQueryMap如:

@RequestMapping(value = "/user", method = RequestMethod.GET)
ResultResp<ApiUserDevice> userDevice(URI uri, @SpringQueryMap Map queryMap, @RequestHeader("token") String token);

 

 

MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Com

之前没有遇到过MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. 错误信息,但是通过信息应该是磁盘的问题。

确认磁盘空间充足、内存也充足的情况下,网上找了一下解决方案:

有建议设置参数“config set stop-writes-on-bgsave-error no”。但这样子只是暂时性的,问题依旧没有解决。

Linux内核参数之 overcommit_memory

/etc/sysctl.conf
vm.overcommit_memory=1
或者
sysctl vm.overcommit_memory=1
或者
echo 1 > /proc/sys/vm/overcommit_memory

内核参数说明如下:

overcommit_memory文件指定了内核针对内存分配的策略,其值可以是0、1、2。

  •   0, 表示内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。
  • 1, 表示内核允许分配所有的物理内存,而不管当前的内存状态如何。
  • 2, 表示内核允许分配超过所有物理内存和交换空间总和的内存

 

为什么系统明明还剩2GB的内存,Redis会说内存不够呢?

这里有一个帖子 ,分析很到位:http://www.linuxidc.com/Linux/2012-07/66079.htm

 

碰到一个悲催的事情:一台Redis服务器,4核,16G内存且没有任何硬件上的问题。持续高压运行了大约3个月,保存了大约14G的数据,设置了比较完备的Save参数。而就是这台主机,在一次重起之后,丢失了大量的数据,14G的数据最终只恢复了几百兆而已。

正常情况下,像Redis这样定期回写磁盘的内存数据库,丢失几个数据也是在情理之中,可超过80%数据丢失率实在太离谱。排除了误操作的可能性之后,开始寻找原因。

重启动时的日志:

[26641] 21 Dec 09:46:34 * Slave ask for synchronization

[26641] 21 Dec 09:46:34 * Starting BGSAVE for SYNC

[26641] 21 Dec 09:46:34 # Can’t save in background: fork: Cannot allocate memory

[26641] 21 Dec 09:46:34 * Replication failed, can’t BGSAVE

[26641] 21 Dec 09:46:34 # Received SIGTERM, scheduling shutdown…

[26641] 21 Dec 09:46:34 # User requested shutdown…

很明显的一个问题,系统不能在后台保存,fork进程失败。

翻查了几个月的日志,发觉系统在频繁报错:

[26641] 18 Dec 04:02:14 * 1 changes in 900 seconds. Saving…

[26641] 18 Dec 04:02:14 # Can’t save in background: fork: Cannot allocate memory

系统不能在后台保存,fork进程时无法指定内存。

对源码进行跟踪,在src/rdb.c中定位了这个报错:

int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;

    if (server.bgsavechildpid != -1) return REDIS_ERR;
    if (server.vm_enabled) waitEmptyIOJobsQueue();
    server.dirty_before_bgsave = server.dirty;
    start = ustime();
    if ((childpid = fork()) == 0) {
        /* Child */
        if (server.vm_enabled) vmReopenSwapFile();
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);
        if (rdbSave(filename) == REDIS_OK) {
            _exit(0);
        } else {
            _exit(1);
        }
    } else {
        /* Parent */
        server.stat_fork_time = ustime()-start;
        if (childpid == -1) {
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
        server.bgsavechildpid = childpid;
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

数据丢失的问题总算搞清楚了!

Redis的数据回写机制分同步和异步两种,

  1. 同步回写即SAVE命令,主进程直接向磁盘回写数据。在数据大的情况下会导致系统假死很长时间,所以一般不是推荐的。
  2. 异步回写即BGSAVE命令,主进程fork后,复制自身并通过这个新的进程回写磁盘,回写结束后新进程自行关闭。由于这样做不需要主进程阻塞,系统不会假死,一般默认会采用这个方法。

个人感觉方法2采用fork主进程的方式很拙劣,但似乎是唯一的方法。内存中的热数据随时可能修改,要在磁盘上保存某个时间的内存镜像必须要冻结。冻结就会导致假死。fork一个新的进程之后等于复制了当时的一个内存镜像,这样主进程上就不需要冻结,只要子进程上操作就可以了。

在小内存的进程上做一个fork,不需要太多资源,但当这个进程的内存空间以G为单位时,fork就成为一件很恐怖的操作。何况在16G内存的主机上fork 14G内存的进程呢?肯定会报内存无法分配的。更可气的是,越是改动频繁的主机上fork也越频繁,fork操作本身的代价恐怕也不会比假死好多少。

找到原因之后,直接修改内核参数vm.overcommit_memory = 1

Linux内核会根据参数vm.overcommit_memory参数的设置决定是否放行。

  1.  如果 vm.overcommit_memory = 1,直接放行
  2. vm.overcommit_memory = 0:则比较 此次请求分配的虚拟内存大小和系统当前空闲的物理内存加上swap,决定是否放行。
  3. vm.overcommit_memory = 2:则会比较 进程所有已分配的虚拟内存加上此次请求分配的虚拟内存和系统当前的空闲物理内存加上swap,决定是否放行。

 

feign调服务错误:feign.RetryableException: Connection refused

2021/01/07 13:37:42.214 ERROR [http-nio-0.0.0.0-7200-exec-35] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/api] threw exception [Request processing failed; nested exception is feign.RetryableException: Connection refused (Connection refused) executing POST http://NJ-DEVICECLOUD-SERVER/njdcd/dispatch] with root cause
java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method) ~[na:1.8.0_171]
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[na:1.8.0_171]
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[na:1.8.0_171]
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) ~[na:1.8.0_171]
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) ~[na:1.8.0_171]
at java.net.Socket.connect(Socket.java:589) ~[na:1.8.0_171]
at sun.net.NetworkClient.doConnect(NetworkClient.java:175) ~[na:1.8.0_171]
at sun.net.www.http.HttpClient.openServer(HttpClient.java:463) ~[na:1.8.0_171]
at sun.net.www.http.HttpClient.openServer(HttpClient.java:558) ~[na:1.8.0_171]

配置的问题,改成IP就可以了

eureka:
  instance:
    appname: ${spring.application.name}
    virtual-host-name: ${spring.application.name}
    secure-virtual-host-name: ${spring.application.name}
    instance-id: ${server.address2}:${spring.application.name}-peer:${server.port}
    hostname: konke.app.eureka
    #    non-secure-port: 6002 #非安全通信端口
    #    non-secure-port-enabled: true #是否启用非安全端口接受请求
    #    secure-port: 6445 #安全通信端口
    #    secure-port-enabled: true #是否启用安全端口接受请求
    prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false
    lease-renewal-interval-in-seconds: 30 #eureka节点定时续约时间,默认30
    lease-expiration-duration-in-seconds: 90 #eureka节点剔除时间,默认90
  #    metadata-map:
  #      zone: shanghai

prefer-ip-address: true #是否优先使用IP地址作为主机名的标识,默认false

15678932
 
Copyright © 2008-2021 lanxinbase.com Rights Reserved. | 粤ICP备14086738号-3 |