./img

[TOC]

Nginx

# 支持stream 的nginx
cd 
wget http://nginx.org/download/nginx-1.16.1.tar.gz
tar -zxf nginx-1.16.1.tar.gz -C /usr/local
cd /usr/local/nginx-1.16.1
./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-stream
make && make install 
/usr/local/nginx/sbin/nginx -v

stream {
    upstream myapp{
        server IP:9000;
    }
    server {
        listen 20000;
        proxy_connect_timeout 5s;
        proxy_timeout 5s;
        proxy_pass myapp;
    }
}
/usr/local/nginx/sbin/nginx -s reload

大数据

Hadoop

安装

Docker Install

echo -e "===prepare workspace==="
if [ ! -d "workspace" ]; then
echo "create new workspace"
mkdir workspace
fi
cd workspace

echo -e "===goto current space==="
version=$[$(ls | sort -n | tail -n 1)+1]
mkdir $version
cd $version
echo "Version: $version"
echo "Space: $(pwd)"

cp ../../Dockerfile Dockerfile

cat>core-site.xml<<EOF
<configuration>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://localhost:9000</value>
    </property>
    <property>
        <name>hadoop.tmp.dir</name>
        <value>/data/hadoop</value>
    </property>
</configuration>
EOF

cat>mapred-site.xml<<EOF
<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>mapreduce.application.classpath</name>
        <value>\$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*:\$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*</value>
    </property>
</configuration>
EOF

cat>yarn-site.xml<<EOF
<configuration>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <property>
        <name>yarn.nodemanager.env-whitelist</name>
        <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOME</value>
    </property>
    <property>
        <name>yarn.resourcemanager.address</name>
        <value>localhost:8032</value>
      </property>
      <property>
        <name>yarn.resourcemanager.scheduler.address</name>
        <value>localhost:8030</value>
      </property>
      <property>
        <name>yarn.resourcemanager.resource-tracker.address</name>
        <value>localhost:8031</value>
      </property>
</configuration>
EOF

cat>entrypoint.sh<<EOF
/usr/sbin/sshd
if [ ! -d "/data/hadoop" ]; then
hdfs namenode -format
fi
hdfs --daemon start datanode
hdfs --daemon start namenode
\$HADOOP_HOME/sbin/start-yarn.sh
echo "done!"
while true; do sleep 30; done;
EOF

docker build -t hadoop:$version .

docker rm -f hadoop || true
docker run -idt --rm \
  -p 9870:9870 \
  -p 8088:8088 \
  -v /data/hadoop:/data \
  --name hadoop \
  hadoop:$version
docker logs hadoop -f
FROM centos:centos8

# install ssh
RUN \
yum install openssh-server openssh-clients passwd  -y; \
sed -i "s/^UsePAM yes/UsePAM no/g" /etc/ssh/sshd_config; \
echo 123456 | passwd root --stdin; \
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa; \
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys; \
chmod 0600 ~/.ssh/authorized_keys; \
ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key; \
ssh-keygen -q -N "" -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key; \
ssh-keygen -q -N "" -t ed25519 -f /etc/ssh/ssh_host_ed25519_key;

# install java
RUN \
yum install wget -y; \
wget https://download.java.net/java/early_access/jdk16/27/GPL/openjdk-16-ea+27_linux-x64_bin.tar.gz ;\
tar -zxf openjdk-16-ea+27_linux-x64_bin.tar.gz -C /usr/local/; 

# env java
ENV JAVA_HOME /usr/local/jdk-16
ENV PATH $PATH:$JAVA_HOME/bin

# install hadoop
RUN \
yum install wget -y; \
wget https://mirror.bit.edu.cn/apache/hadoop/common/hadoop-3.3.0/hadoop-3.3.0.tar.gz; \
tar -zxf hadoop-3.3.0.tar.gz -C /usr/local/; 

# env hadoop
ENV HADOOP_MAPRED_HOME /usr/local/hadoop-3.3.0
ENV HADOOP_HOME /usr/local/hadoop-3.3.0
ENV PATH $PATH:$HADOOP_HOME/bin
ENV HDFS_NAMENODE_USER root
ENV HDFS_DATANODE_USER root
ENV HDFS_SECONDARYNAMENODE_USER root
ENV YARN_RESOURCEMANAGER_USER root
ENV YARN_NODEMANAGER_USER root

RUN \
sed '1 iexport JAVA_HOME=/usr/local/jdk-16' \
	-i $HADOOP_HOME/etc/hadoop/hadoop-env.sh; \
sed '1 iexport HADOOP_HOME=/usr/local/hadoop-3.3.0' \
	-i $HADOOP_HOME/etc/hadoop/hadoop-env.sh;
COPY core-site.xml $HADOOP_HOME/etc/hadoop/core-site.xml
COPY mapred-site.xml $HADOOP_HOME/etc/hadoop/mapred-site.xml
COPY yarn-site.xml $HADOOP_HOME/etc/hadoop/yarn-site.xml
COPY entrypoint.sh /entrypoint.sh

CMD ["sh", "/entrypoint.sh"]

K8S Install

cd .
mkdir hadoop || true
cd hadoop

cat>hadoop-deployment.yaml<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hadoop-deployment
  labels:
    app: hadoop
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hadoop
  template:
    metadata:
      labels:
        app: hadoop
    spec:
      containers:
      - name: hadoop
        image: sequenceiq/hadoop-docker:latest
        command: ["/etc/bootstrap.sh"]
        args: ["-d"]
        ports:
        - containerPort: 50070
EOF

cat>hadoop-service.yaml<<EOF
apiVersion: v1
kind: Service
metadata:
  name: hadoop-service
spec:
  type: NodePort
  selector:
    app: hadoop
  ports:
    - port: 50070
      targetPort: 50070
      nodePort: 30000
EOF

kubectl apply -f hadoop-deployment.yaml
kubectl apply -f hadoop-service.yaml

Hadoop概述

优点

  • 高可靠
  • 高拓展
  • 高效性
  • 高容错

HDFS

HDFS是分布式文件系统,包含NameNode, DataNode和Secondary NameNode

组件 功能
NameNode 储存文件的元数据
DataNode 储存文件块,校验和
Secondary NameNode 协助NameNode处理元数据

YARN

YARN是分布式资源调度器,包含了ResourceManager, NodeManager, ApplicationMaster, Container

组件 功能
ResourceManager 处理客户端请求,监控NodeManager, 启动ApplicationMaster, 分配和调度资源
NodeManager 管理单个节点上的资源,处理ResourceManager和ApplicationMaster的命令
ApplicationMaster 切分数据,为应用程序申请资源,并分配给任务,处理任务的监控和容错
Container 代表节点的CPU,内存,磁盘,网络

MapReduce

MapReduce是一个计算模型, Map阶段并行处理数据,Reduce阶段对Map汇总

HDFS

HDFS全称为Hadoop Distributed File System,适用于一次写入,多次读出,不支持修改

Block

文件分块存储,默认是128M,block的寻址时间一般为10ms,寻址时间为传输时间的1%的时候,达到最佳状态,机械硬盘的速率是100M/s, 所以文件分块的大小为100M 较佳,近似到128M. $$ 10ms / 1% \to 1s\ 1s / 100MB/s \to 100MB \ 100MB \to 128MB $$

block太小增加寻址时间,太大会导致数据传输的时间过长,所以block的大小取决于磁盘的传输速率

HDFS shell

https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/FileSystemShell.html

HDFS读写文件

image-20201215142156156

image-20201215142242967

NameNode工作机制

NameNode将内存数据持久化到磁盘中,分为fsimage和edits两个文件,fsimage是老的内存镜像,edits是追加格式的日志,表示着内存的变化情况,随着NameNode工作,edits会越来越大,这时候SecondaryNameNode会协助NameNode将edits与fsimage合并为新的fsimage。 注意下图紫色部分的流程即可

image-20201215143055795

集群安全模式

当NameNode不是第一次启动的时候,会加载Fsimage,并执行Edits日志,最后合并,此后开始监听DataNode请求,这个过程中NameNode一直是安全模式,文件系统处于只读状态,如果满足最小副本数,NameNode会在30秒后退出安全模式

NameNode多目录配置

<property>
	<name>dfs.namenode.name.dir</name>
    <value>file:///xxx,file:///xxx</value>
</property>

当我们配置了多个目录以后, NameNode的数据会同时存储在这些目录中,这些目录中的内容是一样的,如果一个目录损坏,NameNode会通过另一个目录恢复

DataNode工作机制

image-20201216093831785

超时时间是2 * dfs.namenode.heartbeat.recheck-interval + 10 * dfs.hertbeat.interval

MapReduce

全过程

(input) <k1, v1> -> map -> <k2, v2> -> combine -> <k2, v2> -> reduce -> <k3, v3> (output)

InputFormat

InputFormat是执行MapReduce的第一步,他主要用于在从HDFS文件系统输入到MapTask的过程

名词 解释
数据块 Block是HDFS物理上把数据分成的多块
数据切片 Split是逻辑上对数据的分片,每个Split由一个MapTask处理
切片方法 备注
TextInputFormat 按照大小切片,kv分别是行偏移量和行的具体数据
KeyValueTextInputFormat 按照大小切片,kv是每一行由分隔符分割的左右两部分
NLineInputFormat 按照行数切片,kv分别是行偏移量和行的具体数据
CombineTextInputFormat 按照大小拆分大文件,合并小文件,kv分别是行偏移量和行的具体数据
Map

当经过了InputFormat以后,数据就进入到了Map阶段,

在这个阶段,Map框架会对每一对KV进行并行处理,并输出为新的KV,

把新的KV写入环形缓冲区,一端写索引,另一端写数据,

直到环形缓冲区达到80%(80MB),Map框架将缓冲区数据排序并写入磁盘文件进行分区

直到文件数量达到一定上限,Map框架将文件排序合并,并进行分区

image-20201216095559712

Partition

Map后,需要将数据写入不同的分区,

  • ReduceTask数大于分区数,则最后几个Reduce为空

  • ReduceTask小于分区数大于1,则异常

  • ReduceTask=1,则只有一个输出文件

默认的分区是HashPartitioner

MapReduce中的排序

MapReduce两个阶段都会进行排序,不管实际是否需要

  • Map: 环形缓冲区 -> 达到80% -> 快排 -> 写入文件 -> Map完成 -> 所有文件归并排序
  • Reduce: 远程拷贝文件到内存-> 达到内存阈值-> 写入一个磁盘文件-> 磁盘文件个数达到阈值-> 合并文件 -> 拷贝完成-> 所有数据(磁盘+内存)归并排序
排序的方法
  • 部分排序: 输出是多个文件,保证每个文件有序
  • 全排序: 输出是一个文件,保证这个文件有序
  • 辅助排序:
  • 二次排序: 排序中有两个判断条件
如何排序

实现WriteableCompable接口即可

Combiner

Combiner就是一个局部的Reduce,他不一定必要,并不通用于所有的MR程序,比如求平均值,但是在局部Reduce不影响全局Reduce的情况下它可以降低网络传输压力

job.setCombinerClass(IntSumReducer.class);
Shuffle

往往我们称Map之后,Reduce之前的操作为Shuffle

Reduce

image-20201216132901786

OutputFormat
OUTPUTFORMAT 描述
TextOutputFormat 把结果写成文本行
SequenceFileOutputFormat 写成二进制文件

Join

在Reduce端Join: 用同一个key即可

在Map端Join: 用字典手动Join

Compress

支持gzip,bzip,Lzo等压缩方式,可以用于输入文件的解压缩,输出文件的压缩,mapreduce中间文件的压缩

Map端压缩

configuration.setBoolean("mapreduce.map.output.compress",true);
configuration.setClass("mapreduce.map.output.compress.codec",
                          BZip2Codec.class, CompressionCodec.class)

Reduce压缩

FileOutputFormat.setCompressOutput(job,true);
FileOutputormat.setOutputCompressorClass(job,GzipCodec.class);

MR速度慢的原因

计算机性能: CPU、内存、磁盘、网络

IO: 数据倾斜,小文件多,不可分块的超大文件多,spill次数多,merge次数多

MR优化
  • 输入阶段:合并小文件
  • Maper阶段:调整环形缓冲区大小和溢写比例
  • Maper阶段:调整合并文件的文件个数阈值
  • Maper阶段:使用Combiner
  • Reduce阶段:合理设置Map Reduce个数
  • Reduce阶段:调整slowstart.completedmaps,提前申请Reduce资源
  • Reduce阶段:MapTask机器文件 -> ReduceTask机器Buffer -> 磁盘 -> Reduce, 调整Buffer,让Buffer中保留一定的数据,直接传给Reduce
  • 压缩数据
  • 开启JVM重用

Yarn

流程

image-20201216134336961

调度器

  • FIFIO调度器: 先进先出
  • 容量调度器(默认):支持多个队列,每个队列 有一定的资源,各自采用FIFO,对同一个用户的作业所占资源进行限制,安装任务和资源比例分配新的任务,按照任务优先级、提交时间、用户的资源限制、内存限制对队列中的任务排序
  • 公平调度器(并发度非常高): 多个队列,每个队列中的job可以并发运行,可以每个job都有资源,哪个job还缺的资源最多,就给哪个job分配资源

任务推测执行

当前Job已完成的Task达到5%, 且某任务执行较慢,则开始备份任务 $$ 当前任务完成时刻 = 当前时刻 +(当前时刻 - 任务开始时刻)/ 任务运行进度\ 备份任务完成时刻 = 当前时刻 + 所有任务平均花费时间 $$ 每个任务最多一个备份任务,每个作业也有备份任务上限

HA

HDFS HA

YARN HA

Impala

impala提供对HDFS、Hbase数据的高性能、低延迟的交互式SQL查询功能。基于Hive使用内存计算,兼顾数据仓库、具有实时、批处理、多并发等优点。

Impala的优点

  • 基于内存计算

  • 不使用MR

  • C++编写计算层,Java编写编译层

  • 兼容大部分HiveSQL

  • 支持数据本地计算

  • 可以使用Impala JDBC访问

Impala的缺点

  • 对内存依赖很大
  • 完全依赖Hive
  • 只能读取文本文件,不能读取二进制文件
  • 在Impala更新的数据会同步到Hive,但是在Hive更新的数据不会自动同步到Impala

Impala和关系型数据库的异同

  • Impala不支持事务和索引
  • Impala可以管理PB级数据,但是关系型数据库只能管理TB

Impala和Hive的异同

  • 使用HDFS,HBase储存数据
  • 使用相同的元数据
  • 使用类似的SQL词法分析生成执行计划
  • Impala生成执行计划树,Hive会生成MR模型
  • Impala使用拉的方式,后续节点主动拉取前面节点的数据,是流, Hive使用推的方式,前面的节点执行完成后会将数据主动推送给后面的节点

Impala的架构

Impala集群有三个重要的组件,他们分别是Impala Daemon, Impala Statestore和Impala Metastore

image-20201126141624051

Impala Daemon

Impala Daemon(Impalad)在安装Impala的每个节点上运行, 接受来着各种接口的查询,当一个查询提交到某个Impala Daemon的时候,这个节点就充当协调器,将任务分发到集群

Impala State

Impala State负责检测每个Impalad的运行状况,如果某个Impala Daemon发生了故障,则这个消息会被通知到所有其他Impla Daemon

Impala Matestore

Impala Matestore储存表的元数据信息

Impala语法

  • 时间函数【时间差】

    datediff(now(),to_timestamp(strleft(ftime,10), 'yyyy-MM-dd')) <= 7
    
  • 字符串求和

    sum(cast(time as bigint))
    

Elasticsearch

文档

https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro.html

Install ES

Docker Install ES

docker run -d \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
elasticsearch:7.10.1; \

docker run -d \
--link elasticsearch:elasticsearch \
-p 5601:5601 \
kibana:7.10.1

K8s Install ES

echo -e "===prepare workspace==="
if [ ! -d "workspace" ]; then
echo "create new workspace"
mkdir workspace
fi
cd workspace

echo -e "===goto current space==="
version=$[$(ls | sort -n | tail -n 1)+1]
mkdir $version
cd $version
echo "Version: $version"
echo "Space: $(pwd)"

echo -e "===deploy to k8s==="
mkdir deploy
cd deploy
cat>elasticsearch-deployment.yaml<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: elasticsearch-deployment
  labels:
    app: elasticsearch
spec:
  replicas: 1
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      containers:
      - name: elasticsearch
        image: elasticsearch:7.5.1
        imagePullPolicy: IfNotPresent
        env: 
        - name: "discovery.type"
          value: "single-node"
        ports:
        - containerPort: 9200
        - containerPort: 9300
        resources:
          limits: 
            cpu: 0.3
            memory: 2000Mi
          requests:
            cpu: 0.3
            memory: 300Mi
       # livenessProbe:
       #   httpGet:
       #     path: /
        #    port: 9200
       #   initialDelaySeconds: 10
       #   periodSeconds: 3
      - name: kibana
        image: kibana:7.5.1
        imagePullPolicy: IfNotPresent
        env:
        - name: "ELASTICSEARCH_HOSTS"
          value: "http://127.0.0.1:9200"
        ports:
        - containerPort: 5601
        resources:
          limits: 
            cpu: 0.3
            memory: 1000Mi
          requests:
            cpu: 0.3
            memory: 300Mi
        #livenessProbe:
        #  httpGet:
        #   port: 5601
        #  initialDelaySeconds: 10
        #  periodSeconds: 3
EOF

cat>elasticsearch-service.yaml<<EOF
apiVersion: v1
kind: Service
metadata:
  name: elasticsearch-service
spec:
  type: NodePort
  selector:
    app: elasticsearch
  ports:
    - port: 5601
      targetPort: 5601
      nodePort: 5601
      name: kibana-web
    - port: 9200
      targetPort: 9200
      nodePort: 9200
      name: es-http
    - port: 9300
      targetPort: 9300
      nodePort: 9300
      name: es-tcp
EOF

kubectl apply -f elasticsearch-deployment.yaml
kubectl apply -f elasticsearch-service.yaml
cd ..

Chrome Head Plugin

插件地址

image-20201217093657333

Shell 连接

curl -u 'password' IP:9200

Linux

换源

https://zhuanlan.zhihu.com/p/61228593

ele

firefox

sudo dpkg --remove --force-remove-reinstreq google-chrome-stable
apt-get install firefox

XXD

这是一个16进制查看工具

# 查看帮助
xxd -h

# 查看文件前100个字节
xxd -l 100 file.bin

SSH

Install

# 必须安装passwd
yum install openssh-server openssh-clients passwd  -y; \
sed -i "s/^UsePAM yes/UsePAM no/g" /etc/ssh/sshd_config; \
echo 123456 | passwd root --stdin; \
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa; \
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys; \
chmod 0600 ~/.ssh/authorized_keys; \
ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key; \
ssh-keygen -q -N "" -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key; \
ssh-keygen -q -N "" -t ed25519 -f /etc/ssh/ssh_host_ed25519_key; \
/usr/sbin/sshd

Problem

Docker Install

  • Dockerfile
FROM centos:centos8
RUN yum install openssh-server openssh-clients passwd  -y; \
sed -i "s/^UsePAM yes/UsePAM no/g" /etc/ssh/sshd_config; \
echo 123456 | passwd root --stdin; \
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa; \
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys; \
chmod 0600 ~/.ssh/authorized_keys; \
ssh-keygen -q -N "" -t rsa -f /etc/ssh/ssh_host_rsa_key; \
ssh-keygen -q -N "" -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key; \
ssh-keygen -q -N "" -t ed25519 -f /etc/ssh/ssh_host_ed25519_key;
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
  • Build
docker build -t sshd:centos8 .
  • Run
docker run -itd -p 2222:22 sshd:centos8
  • Connect
ssh localhost -p 2222

USE

ssh root@9.135.10.2 -p 36000 -P
xxxx

Git

docker run -it --rm \
	-v ${HOME}:/root \
	-v $(pwd):/git \
	alpine/git \
	clone https://github.com/alpine-docker/git.git

源码安装

wget https://github.com/git/git/archive/v2.29.2.tar.gz; \
tar -zxf v2.29.2.tar.gz; \
rm v2.29.2.tar.gz; \
cd /git-2.29.2; \
yum install -y curl-devel expat-devel gettext-devel \
    openssl-devel zlib-devel gcc perl-ExtUtils-MakeMaker; \
make prefix=/usr/local/git all; \
make prefix=/usr/local/git install; \
echo "GIT_HOME=/usr/local/git" >> ~/.bashrc; \
echo "PATH=\$GIT_HOME/bin:\$PATH" >> ~/.bashrc; \
source ~/.bashrc; \
git --version; 

修改Git默认编辑器

 git config --global core.editor "vim"

GIT提交规范

feat:新功能(feature)
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
test:增加测试
chore:构建过程或辅助工具的变动

提交Tag

git push origin --tags

vim

git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim
 
echo >~/.vimrc<<EOF 
" Vundle set nocompatible
filetype off
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()
Plugin 'VundleVim/Vundle.vim'
Plugin 'The-NERD-Tree'
Plugin 'gdbmgr'
Plugin 'mbbill/undotree'
Plugin 'majutsushi/tagbar'
Plugin 'vim-airline/vim-airline' " 状态栏
Plugin 'vim-airline/vim-airline-themes' "状态栏
Plugin 'cohlin/vim-colorschemes' " 主题
Plugin 'tomasr/molokai' " molokai
Plugin 'jiangmiao/auto-pairs' " 括号补全
Plugin 'plasticboy/vim-markdown'
Plugin 'iamcco/mathjax-support-for-mkdp' " 数学公式
Plugin 'iamcco/markdown-preview.vim' " markdown预览
"Plugin 'Valloric/YouCompleteMe'
"Plugin 'zxqfl/tabnine-vim'
Plugin 'w0rp/ale' " 语法纠错
Plugin 'octol/vim-cpp-enhanced-highlight' " c++语法高亮
Plugin 'Shougo/echodoc.vim' " c++函数提示
Plugin 'Chiel92/vim-autoformat' " c++代码格式化
Plugin 'scrooloose/nerdcommenter' " c++代码注释
Plugin 'ashfinal/vim-colors-violet' " 配色
Plugin 'terryma/vim-multiple-cursors' " vim 多行编辑
Plugin 'mhinz/vim-startify'
call vundle#end()
filetype plugin indent on


set et "tab用空格替换

set tabstop=2
set expandtab
" Tab键的宽度

set softtabstop=2
set shiftwidth=2
"  统一缩进为2

set number
" 显示行号

set history=10000
" 历史纪录数

set hlsearch
set incsearch
" 搜索逐字符高亮

set encoding=utf-8
set fileencodings=utf-8,ucs-bom,shift-jis,gb18030,gbk,gb2312,cp936,utf-16,big5,euc-jp,latin1
" 编码设置

" set mouse=a
" use mouse

set langmenu=zn_CN.UTF-8
set helplang=cn
" 语言设置

set laststatus=2
" 总是显示状态行 就是那些显示 --insert-- 的怪东西

set showcmd
" 在状态行显示目前所执行的命令,未完成的指令片段亦会显示出来

set scrolloff=3
" 光标移动到buffer的顶部和底部时保持3行距离

set showmatch
" 高亮显示对应的括号

set matchtime=1
" 对应括号高亮的时间(单位是十分之一秒)

colorscheme molokai
EOF

vim +PluginInstall +qall

Net Commond

Centos8 IP网络配置

/etc/sysconfig/network-scripts/*

Centos8重新载入网络设置

nmcli c reload
nmcli c up ens32

Mysql

查看表的定义

show create table table_name;

时间函数

date_add(date(imp_date), interval 1 week)

Docker

修改源

cat>/etc/docker/daemon.json<<EOF
{
    "registry-mirrors": [
        "https://dockerhub.woa.com",
        "http://docker.oa.com:8080"
    ],
    "insecure-registries" : [ 
        "hub.oa.com",
        "docker.oa.com:8080",
        "bk.artifactory.oa.com:8080"
    ],
    "exec-opts":["native.cgroupdriver=systemd"]
}
EOF

常见参数

资源限制

--cpus 0.8
-m 800m

文件夹映射

-v /root/.m2:/root/.m2

K8S

初始化K8S集群

kubeadm init --pod-network-cidr=10.244.0.0/16
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
kubectl taint nodes --all node-role.kubernetes.io/master-
vim /etc/kubernetes/manifests/kube-apiserver.yaml
#- --service-node-port-range=1000-32000

K8S客户端

export KUBECONFIG=/etc/kubernetes/admin.conf
sudo kubectl get pods

K8S资源负载情况

curl -L https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.3.6/components.yaml \
| sed -s 's/k8s.gcr.io/registry.cn-hangzhou.aliyuncs.com\/google_containers/g' \
| kubectl apply -f -

参考

# echo "serverTLSBootstrap: true" >> /var/lib/kubelet/config.yaml

systemctl daemon-reload
systemctl restart kubelet.service
kubectl get csr
kubectl certificate approve xxx ???

K8S 资源限制

echo "===prepare workspace==="
if [ ! -d "workspace" ]; then
echo "create new workspace"
mkdir workspace
fi
cd workspace

echo "===goto current space==="
version=$[$(ls | sort -n | tail -n 1)+1]
mkdir $version
cd $version
echo "Version: $version"
echo "Space: $(pwd)"


echo "===deploy to k8s==="
mkdir deploy
cd deploy
cat>limitRange.yaml<<EOF
apiVersion: v1
kind: LimitRange
metadata:
  name: cpu-min-max-demo-lr
spec:
  limits:
  - max:
      cpu: "800m"
    min:
      cpu: "200m"
    type: Container
EOF
kubectl apply -f limitRange.yaml
cd ..

K8S重启失败

systemctl status kubelet -n 1000

free -m # 看看swap分区是否被打开
swapoff -a

systemctl daemon-reload
systemctl restart kubelet

hostname -f
hostname xxxxxxx

重装

kubeadm reset
rm -rf /etc/kubernetes
rm -rf /var/lib/etcd/

simple Java Project

echo -e "===prepare workspace==="
if [ ! -d "workspace" ]; then
echo "create new workspace"
mkdir workspace
fi
cd workspace

echo -e "===goto current space==="
version=$[$(ls | sort -n | tail -n 1)+1]
mkdir $version
cd $version
echo "Version: $version"
echo "Space: $(pwd)"

echo -e "===set parmas==="
gitPath=xxxx
girBranch=xxxx
# mavenMirror=https://maven.aliyun.com/repository/public
mavenMirror=xxxx
mavenCacheVolume=maven-repo
# mavenImage=maven:3.6.3-openjdk-16
mavenImage=maven:3.6.3-jdk-8
mavenPackageTarget=xxx-start/target/*.jar
# jdkImage=openjdk:16-jdk
jdkImage=openjdk:8-jdk
javaApp=xxxx

echo -e "===get code==="
docker run -i --rm \
	-v ${HOME}:/root \
	-v $(pwd)/src:/git \
	alpine/git \
	clone $gitPath .
pwd
echo $girBranch
docker run -i --rm \
	-v ${HOME}:/root \
	-v $(pwd)/src:/git \
	alpine/git \
	checkout $girBranch 
	

echo -e "===build target==="
mkdir .m2
cat>.m2/settings.xml<<EOF
<settings>
    <mirrors>
        <mirror>
            <id>proxy</id>
            <mirrorOf>central</mirrorOf>
            <name>proxy maven</name>
            <url>$mavenMirror</url>
        </mirror>
    </mirrors>
</settings>
EOF
docker volume create --name $mavenCacheVolume
docker run -i --rm \
	-v $(pwd)/src:/usr/src/mymaven \
	-v $mavenCacheVolume:/root/.m2/repository \
	-v $(pwd)/.m2/settings.xml:/root/.m2/settings.xml \
	-w /usr/src/mymaven \
	$mavenImage \
	mvn package -Dmaven.test.skip=true

echo -e "===move jar==="
mkdir image
mv src/$mavenPackageTarget image/main.jar

echo -e "===build image==="
cd image
cat>Dockerfile<<EOF
FROM $jdkImage
COPY main.jar /main.jar
COPY entrypoint.sh /entrypoint.sh
CMD ["sh","entrypoint.sh"]
EOF
cat>entrypoint.sh<<EOF
java -jar -Xmx250m -Xms200m -Dserver.port=80 /main.jar --logger.print-parmas.enable=true
EOF
docker build -t $javaApp:$version .
cd ..

echo -e "===deploy to k8s==="
mkdir deploy
cd deploy
cat>${javaApp}-deployment.yaml<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${javaApp}-deployment
  labels:
    app: $javaApp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: $javaApp
  template:
    metadata:
      labels:
        app: $javaApp
    spec:
      containers:
      - name: $javaApp
        image: $javaApp:$version
        imagePullPolicy: IfNotPresent
        env: 
        - name: ENV
          value: "env"
        ports:
        - containerPort: 80
        resources:
          limits: 
            cpu: 0.3
            memory: 400Mi
          requests:
            cpu: 0.3
            memory: 300Mi
        livenessProbe:
          httpGet:
            path: /swagger-ui/
            port: 80
          initialDelaySeconds: 100
          periodSeconds: 3
  strategy: # 策略
    type: RollingUpdate # 也可以是Recreate
    rollingUpdate: 
      maxUnavailable: 50% # 滚动更新的时候的最大不可用pod数量, 可以是绝对数字或者比例10%
      maxSurge: 50% # 动更新的时候的溢出的pod数量,也可以是绝对数字
  progressDeadlineSeconds: 150 # 进度期限秒数,不懂是什么
  minReadySeconds: 100 # 最短就绪时间, 容器创建多久以后被视为就绪
  revisionHistoryLimit: 3 # 历史修订限制, 保留的rs的数量,这个数量会消耗etcd资源,rs删除了就不能回滚刀那个版本的Deployment了
EOF

cat>${javaApp}-service.yaml<<EOF
apiVersion: v1
kind: Service
metadata:
  name: ${javaApp}-service
spec:
  type: NodePort
  selector:
    app: $javaApp
  ports:
    - port: 80
      targetPort: 80
      nodePort: 10010
EOF

kubectl apply -f ${javaApp}-deployment.yaml
kubectl apply -f ${javaApp}-service.yaml
cd ..

JAVA

IDEA

Spring Boot 启动命令行太长

修改文件.idea/workspace.xml

  <component name="PropertiesComponent">
    <property name="dynamic.classpath" value="true" />

反编译

 # https://varaneckas.com/jad/
 wget https://varaneckas.com/jad/jad158e.linux.static.zip; \
 unzip jad158e.linux.static.zip
 jad xxx.class
 cat xxx.jad

Java启动参数

JVM参数

-ea
-Dhttp.proxyPort=12639
-Dhttp.proxyHost=127.0.0.1
-Dhttps.proxyPort=12639
-Dhttps.proxyHost=127.0.0.1
-Xmx400m # JVM最大内存
-Xms300m # JVM初始内存
-Xmn200m # 年轻代内存
-Xss128k # 线程堆栈

Java参数

--server.port=80
--jasypt.encryptor.password=xxx
--spring.profiles.active=development

Maven

docker run -it --rm \
	-v "$(pwd)":/usr/src/mymaven \
	-w /usr/src/mymaven \
	maven:3.3-jdk-8 \
	mvn clean install
# 发布
mvn clean javadoc:jar source:jar deploy

单元测试

@Import(XxxServiceImpl.class)
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TestRunner.class)
public class ScriptBizServiceTest {
    @Autowired
    XxxService xxxService;

    @MockBean
    XxxService xxxService;
}
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

源码上传插件

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-source-plugin</artifactId>
    <version>3.0.1</version>
    <configuration>
        <attach>true</attach>
    </configuration>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals>
                <goal>jar</goal>
            </goals>
        </execution>
    </executions>
</plugin>

规范

多仓库规范

https://maven.apache.org/guides/mini/guide-multiple-repositories.html

Remote repository URLs are queried in the following order for artifacts until one returns a valid result:

  1. Global settings.xml
  2. User settings.xml
  3. Local POM
  4. Parent POMs, recursively
  5. Super POM

For each of these locations, the repositories within the profiles are queried first in the order outlined at Introduction to build profiles.

Guava

RateLimiter

RateLimiter rateLimiter = RateLimiter.create(10);
for (int i = 0; i < 20; i++) {
    int finalI = i;
    new Thread(new Runnable() {
        @Override
        public void run() {
            int cnt = 0;
            while (true) {
                if (rateLimiter.tryAcquire()) {
                    cnt++;
                    System.out.println("thread: " + finalI + " cnt: " + cnt);
                }
            }
        }
    }).start();
}
Thread.sleep(1000 * 100 * 1000);

源码流程图:

image-20201230145440469

image-20201230145348241

image-20201230145401518

Spring

Spring Webflux 5.3.2

ResponseBodyResultHandler

这个ResultHandler是最常用的一个,我们可以看到他在containingClass被ResponseBody标记或者方法被ResponseBody标记时生效

	@Override
	public boolean supports(HandlerResult result) {
		MethodParameter returnType = result.getReturnTypeSource();
		Class<?> containingClass = returnType.getContainingClass();
		return (AnnotatedElementUtils.hasAnnotation(containingClass, ResponseBody.class) ||
				returnType.hasMethodAnnotation(ResponseBody.class));
	}

	@Override
	public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
		Object body = result.getReturnValue();
		MethodParameter bodyTypeParameter = result.getReturnTypeSource();
		return writeBody(body, bodyTypeParameter, exchange);
	}

ViewResolutionResultHandler

ViewResolutionResultHandler支持的attributes比较多,它可以解析CharSequence,Rendering,Model,Map,View等

	@Override
	public boolean supports(HandlerResult result) {
		if (hasModelAnnotation(result.getReturnTypeSource())) {
			return true;
		}

		Class<?> type = result.getReturnType().toClass();
		ReactiveAdapter adapter = getAdapter(result);
		if (adapter != null) {
			if (adapter.isNoValue()) {
				return true;
			}
			type = result.getReturnType().getGeneric().toClass();
		}

		return (CharSequence.class.isAssignableFrom(type) ||
				Rendering.class.isAssignableFrom(type) ||
				Model.class.isAssignableFrom(type) ||
				Map.class.isAssignableFrom(type) ||
				View.class.isAssignableFrom(type) ||
				!BeanUtils.isSimpleProperty(type));
	}

ServerResponseResultHandler

ServerResponseResultHandler只处理返回值为ServerResponse类型的Response,并使用内置的messageWriters和视图解析器来处理他们

	@Override
	public void afterPropertiesSet() throws Exception {
		if (CollectionUtils.isEmpty(this.messageWriters)) {
			throw new IllegalArgumentException("Property 'messageWriters' is required");
		}
	}

	@Override
	public boolean supports(HandlerResult result) {
		return (result.getReturnValue() instanceof ServerResponse);
	}

	@Override
	public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
		ServerResponse response = (ServerResponse) result.getReturnValue();
		Assert.state(response != null, "No ServerResponse");
		return response.writeTo(exchange, new ServerResponse.Context() {
			@Override
			public List<HttpMessageWriter<?>> messageWriters() {
				return messageWriters;
			}
			@Override
			public List<ViewResolver> viewResolvers() {
				return viewResolvers;
			}
		});
	}

Spring Boot

源码下載

git clone https://github.com/spring-projects/spring-boot.git

checkout failed原因

git config core.longPaths true

Aware

BeanNameAware

beanNameAware可以获得容器中Bean的名称,作用于每一个Bean。当bean被创建的时候设置他的名字,在基本properties填充完成以后,init调用前执行

摘自: spring-beans:5.3.4 org.springframework.beans.factory.BeanNameAware

Set the name of the bean in the bean factory that created this bean.

Invoked after population of normal bean properties but before an init callback such as {@link InitializingBean#afterPropertiesSet()} or a custom init-method.

package com.example.demo;

import org.springframework.beans.factory.BeanNameAware;
import org.springframework.stereotype.Component;

@Component
public class BeanNameAwareDemo implements BeanNameAware {
    @Override
    public void setBeanName(String name) {
        System.out.println(name);
    }
}

输出:

beanNameAwareDemo

BeanFactoryAware

注入beanFactory

package com.example.demo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.stereotype.Component;

@Component
public class BeanFactoryAwareDemo implements BeanFactoryAware {
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println(beanFactory);
    }
}

ApplicationContextAware

类比beanFactory

package com.example.demo;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextAwareDemo implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println(applicationContext);
    }
}

MessageSourceAware

这是使用国际化用到的

package com.example.demo;

import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MessageSourceAwareDemo implements MessageSourceAware {
    @Override
    public void setMessageSource(MessageSource messageSource) {
        String hello = messageSource.getMessage("hello", null, Locale.CHINA);
        log.info(hello);
    }
}
2021-03-05 13:36:38.263  INFO 17836 --- [           main] com.example.demo.MessageSourceAwareDemo  : 你好呀小老弟

ApplicationEventPublisherAware

用于发布事件

package com.example.demo;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationEventPublisherAwareDemo implements ApplicationEventPublisherAware {
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        applicationEventPublisher.publishEvent("hi");
    }
}

ResourceLoaderAware

用于获取静态文件内容

package com.example.demo;

import java.io.IOException;
import java.io.InputStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ResourceLoaderAwareDemo implements ResourceLoaderAware {
    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        try {
            InputStream inputStream =
                    resourceLoader.getResource("classpath:/messages_zh_CN.properties").getInputStream();
            IOUtils.readLines(inputStream).forEach(log::info);
        } catch (IOException ioException) {
            log.error("", ioException);
        }
    }
}
2021-03-05 13:56:08.067  INFO 17700 --- [           main] com.example.demo.MessageSourceAwareDemo  : 你好呀小老弟

自定义starter

@ConfigurationProperties 不能缺少下面这个依赖,否则不会自动处理配置的提示

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
    <scope>compile</scope>
</dependency>

Feign

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.codec.Decoder;
import feign.codec.Encoder;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

@Slf4j
@Configuration
public class CustomFeignConfig {
    @Bean
    public Decoder feignDecoder() {
        MappingJackson2HttpMessageConverter jacksonConverter =
                new MappingJackson2HttpMessageConverter(customObjectMapper());
        jacksonConverter.setSupportedMediaTypes(Arrays.asList(
                MediaType.ALL
        ));
        ObjectFactory<HttpMessageConverters> objectFactory =
                () -> new HttpMessageConverters(jacksonConverter);
        return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
    }
    @Bean
    public Encoder feignEncoder() {
        MappingJackson2HttpMessageConverter jacksonConverter =
                new MappingJackson2HttpMessageConverter(customObjectMapper());
        jacksonConverter.setSupportedMediaTypes(Arrays.asList(
                MediaType.ALL
        ));
        ObjectFactory<HttpMessageConverters> objectFactory =
                () -> new HttpMessageConverters(jacksonConverter);
        return new SpringEncoder(objectFactory);
    }
    public ObjectMapper customObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        //Customize as much as you want
        objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
        return objectMapper;
    }
    
    @Bean
    public Logger.Level logger() {
        return Logger.Level.FULL;
    }
}

Validaction

https://cloud.tencent.com/developer/article/1465749

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

使用方案

@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
数值检查
建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。 @Range(min=, max=) Checks whether the annotated value lies between (inclusive) the specified minimum and maximum. @Range(min=10000,max=50000,message=“range.bean.wage”) private BigDecimal wage;
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)

Spring Boot Starter Webflux

各个bean详细的功能详见[Spring Webflux 5.3.2](#Spring Webflux 5.3.2)

spring-webflux-starter-自动装配

Spring Cloud

Spring Cloud Cluster 1.0.1.RELEASE

参考

Spring Cloud Cluster提供了分布式系统中集群的特性,例如选主,集群持久化信息储存,全局锁和一次性token

以下是Spring Cloud Cluster 1.0.1的Spring Boot 自动装配流程,其中的zk模式主要用到了第三方框架CuratorFramework

image-20201221142540901

Spring Cloud Gateway

package com.example.demo;

import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

// 动态路由
// https://zhuanlan.zhihu.com/p/125018436
@RestController
@SpringBootApplication
public class DemoApplication implements RouteDefinitionRepository, ApplicationEventPublisherAware {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // event publisher
    ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }


    // router
    List<RouteDefinition> memery = new ArrayList<>();

    private void refreshRoute() {
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
    }

    @PutMapping
    Mono<Void> putRoute(@RequestBody Mono<RouteDefinition> o) {
        return o.flatMap(routeDefinition -> {
            memery.add(routeDefinition);
            refreshRoute();
            return Mono.empty();
        });
    }

    @PostMapping
    Mono<Void> postRoute(@RequestBody Mono<RouteDefinition> o) {
        return o.flatMap(routeDefinition -> {
            for (int i = 0; i < memery.size(); i++) {
                if (memery.get(i).getId().equals(routeDefinition.getId())) {
                    memery.set(i, routeDefinition);
                }
            }
            refreshRoute();
            return Mono.empty();
        });
    }

    @DeleteMapping
    Mono<Void> deleteRoute(@RequestBody Mono<String> o) {
        return o.flatMap(id -> {
            memery.removeIf(routeDefinition -> routeDefinition.getId().equals(id));
            refreshRoute();
            return Mono.empty();
        });
    }

    @GetMapping
    Mono<List<RouteDefinition>> getRoute(){
        return Mono.just(memery);
    }

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return Flux.fromIterable(memery);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return Mono.empty();
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return Mono.empty();
    }
}
GET http://localhost:52834/test

###

PUT http://localhost:52834
Content-Type: application/json

{
    "id": "test",
    "predicates": [
        {
            "name": "Path",
            "args": {
                "pattern": "/test"
            }
        }
    ],
    "filters": [
        {
            "name": "RewritePath",
            "args": {
                "regexp": "/test",
                "replacement": "/s"
            }
        }
    ],
    "uri": "http://www.baidu.com",
    "order": 0
}

###

GET http://localhost:52834

###

GET http://localhost:52834/test

微服务

Zookeeper

CuratorFramework

容器化开发

https://segmentfault.com/a/1190000023095631

注意事项

对于所有的容器化开发,我们的时区都需要设置

-v /etc/localtime:/etc/localtime

Nodejs开发

docker run -itd \
--restart=always \
--name xxx \
-v /src/xxx:/src/xxx \
-v /etc/localtime:/etc/localtime \
-v /root/.ssh:/root/.ssh \
-p 3000:3000 \
node:14.4.0

# 这个时区设置添加到启动程序中
# process.env.TZ = 'Asia/Shanghai';

Java开发

# docker 参数
-m 800m
--cpus 1
-v /root/.m2/:/root/.m2
-p 8080:8080 -p
--net docker-net
--ip 192.168.11.2
FROM maven:3.6.3-jdk-8
COPY . /src
WORKDIR /src
CMD ["sh", "dockerEntryPoint.sh"]
mvn -v
echo "package"
mvn clean package -Dmaven.test.skip=true
echo "start java application ... "
#java -jar -agentlib:jdwp=transport=dt_socket,server=n,address=10.40.28.63:5005,suspend=y main.jar
java -jar \
  -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 \
  *.jar

第一步,开发一个Spring程序

image-20201212153704735

第二步,连接远端Centos

image-20201212154138665

image-20201212154214889

第三步修改docker启动参数并重启docker

vim /lib/systemd/system/docker.service 

增加 -H tcp://0.0.0.0:2375

image-20201212154527215

systemctl daemon-reload && systemctl restart docker && systemctl status docker

第四步创建Dockerfile以及entrypoint.sh

注意Dockerfile中移动的jar包是编译产物

注意entrypoint.sh中的address后是自己本地机器的ip

image-20201212155043742

FROM openjdk:15
WORKDIR /
COPY entrypoint.sh /entrypoint.sh
COPY target/demo-0.0.1-SNAPSHOT.jar /main.jar
CMD ["sh", "/entrypoint.sh"]
java --version
echo "start java application ... "
java -jar -agentlib:jdwp=transport=dt_socket,server=n,address=192.168.0.109:5005,suspend=y -Duser.timezone=Asia/Shanghai /main.jar

第五步创建Docker启动配置和Debug启动配置

注意Dockerfile的Before lanch前加上 mvn package

注意Debug的Host为远程ip

image-20201212155416319

image-20201212155504553

第六步先启动远程调试,后启动docker build

image-20201212155611552

image-20201212155659369

第七步: enjoy it

image-20201212155744919

image-20201212155845815

hexo_post

Algorithm

势能分析

势能分析简介

势能分析是一种常用的数据结构时间复杂度分析的手段,我们常常会定义一个势能函数,用于评价数据结构某个状态的势能,把每个操作的时间复杂度加上操作导致的势能变化作为摊还复杂度,如果经过了一系列操作以后,势能不减少,这一系列操作的时间复杂度之和不大于这一系列操作的摊还复杂度之和。

势能分析更加严谨的简介

我们对一个初始数据结构$D_0$执行$n$个操作,对于每个$i=1,2,...,n$令$C_i$为第$i$个操作的实际代价,令$D_i$为在数据结构$D_{i-1}$上执行第$i$个操作后得到的结果数据结构,势能函数$\Phi$将每个数据结构$D_i$映射到一个实数$\Phi(D_i)$,此值即为关联到数据结构$D_i$的势,第$i$个操作的摊还代价$\hat{C_i}=C_i+\Phi(D_i)-\Phi(D_{i-1})$,则$n$个操作的总摊还代价为$\sum_{i=1}^n\hat{C_i}=\sum_{i=1}^n{C_i}+\Phi(D_n)-\Phi(D_0)$,如果势能函数满足$\Phi(D_n)\ge\Phi(D_0)$,则总摊还代价$\sum_{i=1}^n\hat{C_i}$是总实际代价$\sum_{i=1}^nC_i$的一个上界

后记

笔者在此不会做势能分析,能进行势能分析的东西太多了,例如splay、pairing heap、fibonacci heap、link cut tree等等,我们将其留在后边的博文中详细介绍。

BigData

hadoop

hadoop

hadoop = common+hdfs+mapreduce+yarn

common

工具、rpc通信

hdfs

分布式文件系统,一个文件分成多个128Mb的文件,存储在多个节点,为了保证分区容错性,存有备份,默认为3。主从架构。

namenode

用来记录各个文件的block的编号、各个block的位置、抽象目录树 处理读写请求 可以有多个namenode

secondarynamenode

用来备份namenode,当namenode宕机的时候,帮助namenode恢复

datanode

用来储存数据

副本机制

如果一个datanode挂了,就再开一个datanode,然后吧挂了的数据通过备份推出来存进去,如果之前那个挂了的又活了,则选择一个节点删掉。副本过多将导致维护成本提高

优点

  • 可构建在廉价机器上
  • 高容错性 : 自动恢复

缺点

  • 不支持数据修改(尽管支持改名和删除)
  • 延迟高
  • 不擅长存储小文件,寻址时间长,空间利用低

yarn

资源调度、管理框架

  • resourcemanager 统筹资源
  • nodemanager 资源调度

mapreduce

分布式计算框架

Zookeeper

Zookeeper介绍

Zookeeper是一个为分布式应用提供一致性服务的软件,是Hadoop项目的一个子项目,是分布式应用程序协调服务

Zookeeper安装

这里有一个下载地址, 也可以brew install zookeeper安装 还可以docker pull zookeeper安装

我们这里采取docker的方式

Zookeeper单机启动

docker run -d -p 2181:2181 --name zookeeper --restart always zookeeper
docker exec -it zookeeper bash
./bin/zkCli.sh

然后我们能看到下面的输出, 我只截取前几行

Connecting to localhost:2181
2020-04-17 07:54:30,252 [myid:] - INFO  [main:Environment@98] - Client environment:zookeeper.version=3.6.0--b4c89dc7f6083829e18fae6e446907ae0b1f22d7, built on 02/25/2020 14:38 GMT
...

输入quit可以退出

Zookeeper集群启动

嘿嘿嘿

Zookeeper配置

在conf目录下有配置文件zoo_sample.cfg和log4j.properties,他们分别是zoo的配置文件模版和日志配置,我们可以将zoo_sample.cfg改为zoo.cfg,这个才是Zookeeper默认的配置文件,其中有几个地方比较重要

配置 作用
tickTime=2000 这个是心跳时间间隔,单位是毫秒
dataDir= Zookeeper保存数据的目录,默认将日志也保存在其中
clientPort=2181 客户端连接Zookeeper服务器的端口
initLimit=5 当客户端超过5个心跳间隔仍然与服务器连接失败,则认为他宕机
synLimit=2 Leader和Follower之间发送消息的响应、请求时间长度不能超过的心跳间隔
server.1=192.168.211.1:2888:3888 server.A=B:C:D, A是数字表示服务器编号,B是这个服务器的ip,C是服务器于集群leader交流信息的端口,D是Leader宕机以后选举新Leader的端口

Zookeeper数据模型

Zookeeper会维护一个层次数据结构,他就像一个文件系统一样, 凑合着看吧

graph LR;
a((/)) --> b((/NameService));
a((/)) --> c((/Configuration));
a((/)) --> d((/GroupMembers));
a((/)) --> e((/Apps));
b((/NameService)) --> b1((/Server1))
b((/NameService)) --> b2((/Server2))
Loading

Zookeeper数据结构的特点

  • 所有的目录项都被叫做znode,这个zndoe是被他所在的路径唯一标识,
  • znode分4类,EPHEMERAL or PERSISTENT, SEQUENTIAL or N
  • 大部分znode都可以有子节点,都可以储存数据, 只有EPHEMERAL不可以有子节点
  • znode储存的数据可以拥有版本号
  • EPHEMERAL 是临时节点,服务器客户端用心跳机制来保证长连接,如果客户端宕机,这个节点会被删除
  • znode可以被监控,是一种观察者模式,客户端可以在目录上注册观察,当目录发生变化,客户端会得到通知

Zookeeper持久化

Zookeeper的数据分为两个部分,一部分是内存数据,另一部分是磁盘数据,内存数据提供对外服务,磁盘数据用来恢复内存数据,用来在集群汇总不同节点间数据的同步,磁盘数据包括快照和事务日志,快照是粗粒度,事务日志是细粒度。

Zookeeper数据加载

先加载快照然后加载日志

快照生成的时机

基于阈值,引入随机因素,我们要避免所有节点同时制造快照,这会导致消耗大量磁盘IO和CPU,降低对外服务能力,参见一个公式 $$countLog&gt;snapCount/2+randRoll$$ 这里的countLog是累计执行的事务个数,snapCount是一个预先设定的阈值,randRoll是一个随机数

事务日志的储存

事务日志是不断写入的,会触发底层磁盘IO,为了减少分配磁盘块对写入的影响,Zookeeper使用预分配的策略,每次分配64MB,当这64MB的空间被使用到只剩下4KB的时候,就开始再次分配空间了

Zookeeper架构

**过半:**当leader广播一个事务消息以后,收到了半数以上的ack,就认为集群中所有的节点都收到了消息,leader不会等待剩余节点的ack,直接广播commit消息,提交事务,选举投票中也是如此

Zookeeper集群中有3种角色

角色 任务
leader 一个集群只有一个leader,通过选举产生,负责所有事务的写操作,保证集群事务处理的顺序性
follower 处理非事务请求,转发事务给leader,参与leader选举投票,
observer 提供读取服务,不参与投票

Zookeeper一致性协议

  • 集群在半数以下节点宕机的情况下,能够正常对外提供服务
  • 客户端的写请求全部转交给leader处理,以确保变更能实时同步到所有的follower和observer
  • leader宕机或者整个集群重启的时候,要确保在leader上提交的事务最终被所有服务器提交,确保只在leader上提出单未被提交的事务被丢弃

Zookeeper选主

当集群中的服务器初始化启动或者运行期无法与leader保持连接的时候,会触发选主,投票者们混线传递投票的信息,包含了被推举的leader的服务id、事务zxid、逻辑时钟、选举状态,显然要选举事务zxid最大的那个,如果事务id相同,就选择服务id最大的那个

广播的时候每当外边传入的(id,zxid)比自己内存中的要优的时候,就更新自己的数据,然后向外广播[^理解zookeeper选举机制]

这里有一个有意思的东西,我们什么时候选举结束呢?

当一个(id,zxid)被超过半数的节点所选举的时候,它就有力担当leader,为什么是半数?因为Zookeeper集群的每一条事务,都是在超过半数ack的情况下才能被leader提交,所以如果一个节点在半数中为最优,那么它一定是最优者之一

这就好比一个数列,数列中的最大值的数量超过了半数,那么该序列的任何一个元素个数超过一半的子序列的最值,一定等于整个序列的最值。比方有一个序列[1,2,3,5,5,5,5], 你在其中选择至少4个数,那么他们中的最大值一定是5,其实就是鸽巢原理

另一方面选主的时候,每个节点都是三个线程,一个负责接收投票并放入队列,另一个用于发送投票信息,还有一个用于外部投票同自己的信息比较,选出新的结果

Zookeeper选主后的同步

这里的数据不一致可能有两种,要么比leader旧,要么比leader新,旧的话同步即可,新的话撤销这个未提交的事务即可, 两个不一致性的原因这里有谈到[^分析Zookeeper的一致性原理]

两阶段提交

事务由leader发起,follower执行,然后返回ack,最终由leader决定是否提交。

Zookeeper的应用

统一命名服务

路径就是名字

配置管理

我们的集群,每台机器都有自己的配置文件,这会非常难以维护,实际上我们会把配置文件储存在Zookeeper的某个目录节点中,让集群的每台机器都注册该节点的观察,当配置文件发生改变的时候,这些机器都会的得到通知,然后从Zookeeper更新自己的配置文件。

集群管理

我们的集群需要有一个总管知道集群中每台机器的状态,当一些机器发生故障或者新添加机器的时候,总管必须知道,这就可以用Zookeeper管理

甚至当总管宕机以后,Zookeeper能够重新选出总管,总管会在Zookeeper中创建一个EPHEMERAL类型的目录节点,每个Server会注册这个节点的watch,总管死去的时候,这个目录会被删除,所有的子节点都会收到通知,这时大家都知道总管宕机了,集群默认会选择接待你编号最小的Server作为新的Master。

分布式锁

同样,我们让第一个创建某目录成功的机器获得锁,其他机器在其子目录下创建新的目录节点,当它需要释放锁的时候,只需要删除目录,然后让子节点中编号最小的节点作文新的目录获得锁,其他节点继续跟随即可。

队列管理

同步队列,即当所有成员达到某个条件是,才能以前向后执行,我们创建一个同步目录,每当一个成员满足条件,就去Zookeeper上注册节点,如果当前节点的个数达到n,就创建start节点,否则注册watch,等待start节点被创建,当其被创建就会收到通知,然后执行自己的事情

FIFO队列, 如生产者消费者模型,创建子目录/queue,当生产者生产出东西的时候,在/queue上创建新节点,当消费者需要消费的时候,从当前目录去除编号最小的节点即可

参考资料

Docker下安装zookeeper(单机 & 集群) ZooKeeper学习 一:安装 zookeeper使用和原理探究 分布式服务框架 Zookeeper — 管理分布式环境中的数据 mac安装的docker替换镜像 Zookeeper持久化原理 ZooKeeper 技术内幕:数据的存储(持久化机制) Zookeeper-持久化 分析Zookeeper的一致性原理 理解zookeeper选举机制

Docker-Zookeeper集群部署

创建工作目录

mkdir ~/DockerDesktop
mkdir ~/DockerDesktop/Zookeeper
cd ~/DockerDesktop/Zookeeper

创建挂载目录

mkdir node1 
mkdir node1/data
mkdir node1/datalog
cp -r node1 node2
cp -r node1 node3
cp -r node1 node4
cp -r node1 node5

创建docker-compose.yml

vim docker-compose.yml
version: '3'

services:
  Zookeeper1:
    image: zookeeper
    hostname: Zookeeper1
    volumes: # 挂载数据
      - ~/DockerDesktop/Zookeeper/node1/data:/data
      - ~/DockerDesktop/Zookeeper/node1/datalog:/datalog
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=Zookeeper1:2888:3888;2181 server.2=Zookeeper2:2888:3888;2181 server.3=Zookeeper3:2888:3888;2181 server.4=Zookeeper4:2888:3888;2181 server.5=Zookeeper5:2888:3888;2181
    networks:
      default:
        ipv4_address: 172.17.1.1

  Zookeeper2:
    image: zookeeper
    hostname: Zookeeper2
    volumes: # 挂载数据
      - ~/DockerDesktop/Zookeeper/node2/data:/data
      - ~/DockerDesktop/Zookeeper/node2/datalog:/datalog
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=Zookeeper1:2888:3888;2181 server.2=Zookeeper2:2888:3888;2181 server.3=Zookeeper3:2888:3888;2181 server.4=Zookeeper4:2888:3888;2181 server.5=Zookeeper5:2888:3888;2181
    networks:
      default:
        ipv4_address: 172.17.1.2

  Zookeeper3:
    image: zookeeper
    hostname: Zookeeper3
    volumes: # 挂载数据
      - ~/DockerDesktop/Zookeeper/node3/data:/data
      - ~/DockerDesktop/Zookeeper/node3/datalog:/datalog
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=Zookeeper1:2888:3888;2181 server.2=Zookeeper2:2888:3888;2181 server.3=Zookeeper3:2888:3888;2181 server.4=Zookeeper4:2888:3888;2181 server.5=Zookeeper5:2888:3888;2181
    networks:
      default:
        ipv4_address: 172.17.1.3

  Zookeeper4:
    image: zookeeper
    hostname: Zookeeper4
    volumes: # 挂载数据
      - ~/DockerDesktop/Zookeeper/node4/data:/data
      - ~/DockerDesktop/Zookeeper/node4/datalog:/datalog
    environment:
      ZOO_MY_ID: 4
      ZOO_SERVERS: server.1=Zookeeper1:2888:3888;2181 server.2=Zookeeper2:2888:3888;2181 server.3=Zookeeper3:2888:3888;2181 server.4=Zookeeper4:2888:3888;2181 server.5=Zookeeper5:2888:3888;2181
    networks:
      default:
        ipv4_address: 172.17.1.4

  Zookeeper5:
    image: zookeeper
    hostname: Zookeeper5
    volumes: # 挂载数据
      - ~/DockerDesktop/Zookeeper/node5/data:/data
      - ~/DockerDesktop/Zookeeper/node5/datalog:/datalog
    environment:
      ZOO_MY_ID: 5
      ZOO_SERVERS: server.1=Zookeeper1:2888:3888;2181 server.2=Zookeeper2:2888:3888;2181 server.3=Zookeeper3:2888:3888;2181 server.4=Zookeeper4:2888:3888;2181 server.5=Zookeeper5:2888:3888;2181
    networks:
      default:
        ipv4_address: 172.17.1.5

networks: # 自定义网络
  default:
    external:
      name: net17

运行

docker-compose up -d

我们不难发现Zookeeper5成为了集群的leader,其他都都成为了follower

Zookeeper5_1  | 2020-04-18 09:53:01,840 [myid:5] - INFO  [QuorumPeer[myid=5](plain=0.0.0.0:2181)(secure=disabled):QuorumPeer@863] - Peer state changed: leading - broadcast
Zookeeper3_1  | 2020-04-18 09:53:01,871 [myid:3] - INFO  [QuorumPeer[myid=3](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@476] - Configuring CommitProcessor with readBatchSize -1 commitBatchSize 1
Zookeeper1_1  | 2020-04-18 09:53:01,874 [myid:1] - INFO  [QuorumPeer[myid=1](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@476] - Configuring CommitProcessor with readBatchSize -1 commitBatchSize 1
Zookeeper1_1  | 2020-04-18 09:53:01,876 [myid:1] - INFO  [QuorumPeer[myid=1](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@438] - Configuring CommitProcessor with 1 worker threads.
Zookeeper3_1  | 2020-04-18 09:53:01,875 [myid:3] - INFO  [QuorumPeer[myid=3](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@438] - Configuring CommitProcessor with 1 worker threads.
Zookeeper4_1  | 2020-04-18 09:53:01,874 [myid:4] - INFO  [QuorumPeer[myid=4](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@476] - Configuring CommitProcessor with readBatchSize -1 commitBatchSize 1
Zookeeper4_1  | 2020-04-18 09:53:01,882 [myid:4] - INFO  [QuorumPeer[myid=4](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@438] - Configuring CommitProcessor with 1 worker threads.
Zookeeper2_1  | 2020-04-18 09:53:01,890 [myid:2] - INFO  [QuorumPeer[myid=2](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@476] - Configuring CommitProcessor with readBatchSize -1 commitBatchSize 1
Zookeeper2_1  | 2020-04-18 09:53:01,897 [myid:2] - INFO  [QuorumPeer[myid=2](plain=0.0.0.0:2181)(secure=disabled):CommitProcessor@438] - Configuring CommitProcessor with 1 worker threads.
Zookeeper1_1  | 2020-04-18 09:53:01,909 [myid:1] - INFO  [QuorumPeer[myid=1](plain=0.0.0.0:2181)(secure=disabled):RequestThrottler@74] - zookeeper.request_throttler.shutdownTimeout = 10000
Zookeeper4_1  | 2020-04-18 09:53:01,915 [myid:4] - INFO  [QuorumPeer[myid=4](plain=0.0.0.0:2181)(secure=disabled):RequestThrottler@74] - zookeeper.request_throttler.shutdownTimeout = 10000
Zookeeper3_1  | 2020-04-18 09:53:01,921 [myid:3] - INFO  [QuorumPeer[myid=3](plain=0.0.0.0:2181)(secure=disabled):RequestThrottler@74] - zookeeper.request_throttler.shutdownTimeout = 10000
Zookeeper2_1  | 2020-04-18 09:53:01,928 [myid:2] - INFO  [QuorumPeer[myid=2](plain=0.0.0.0:2181)(secure=disabled):RequestThrottler@74] - zookeeper.request_throttler.shutdownTimeout = 10000
Zookeeper1_1  | 2020-04-18 09:53:02,053 [myid:1] - INFO  [QuorumPeer[myid=1](plain=0.0.0.0:2181)(secure=disabled):QuorumPeer@863] - Peer state changed: following - broadcast
Zookeeper4_1  | 2020-04-18 09:53:02,056 [myid:4] - INFO  [QuorumPeer[myid=4](plain=0.0.0.0:2181)(secure=disabled):QuorumPeer@863] - Peer state changed: following - broadcast
Zookeeper3_1  | 2020-04-18 09:53:02,063 [myid:3] - INFO  [QuorumPeer[myid=3](plain=0.0.0.0:2181)(secure=disabled):QuorumPeer@863] - Peer state changed: following - broadcast
Zookeeper2_1  | 2020-04-18 09:53:02,068 [myid:2] - INFO  [QuorumPeer[myid=2](plain=0.0.0.0:2181)(secure=disabled):QuorumPeer@863] - Peer state changed: following - broadcast

Kafka

Kafka概述

定义

Kafka是一个分布式的基于发布/订阅模式的消息队列,应用于大数据实时处理领域

消息队列的优点

主要是解耦和削峰

  • 解耦
  • 可恢复,如果系统中一部分组件失效,加入队列的消息仍然可以在系统恢复后被处理
  • 削峰
  • 灵活,可动态维护消息队列的集群
  • 异步

消息队列的两种模式

点对点

一对一,消费者主动拉取消息,收到后清除

发布/订阅模式

一对多,消费者消费后,消息不会清除,当然也不是永久保留,

分两种,一个是发布者主动推送,另一个是消费者主动拉取,Kafka就是消费者主动拉取,

推送 拉取
不好照顾多个消费者的接受速度 主动拉取,由消费者决定
消费者要每过一段时间就询问有没有新消息,长轮询

基础架构

Kafka Cluster 中有多个 Broker Broker中有多个Topic Partion 每个Topic的多个Parttition,放在多个Broker上,可以提高Producer的并发,每个Topic Partition在其他Cluster上存有副本,用于备份,他们存在leader和follower,我们只找leader,不找follower

Topic是分区的,每个分区都是有副本的,分为leader和follower

消费者存在消费者组,一个分区只能被同一个组的某一个消费者消费,我们主要是把一个组当作一个大消费者,消费者组可以提高消费能力,消费者多了整个组的消费能力就高了,消费组中消费者的个数不要比消息多,不然就是浪费资源

Kafka利用Zookeeper来管理配置 0.9前消费者把自己消费的位置信息储存在Zookeeper中 0.9后是Kafka自己储存在某个主题中(减少了消费者和zk的连接)

我偷了个图

Kafka入门

Kafka部署

官网下载Kafka brew install kafka docker pull wurstmeister/kafka 这里我依然选择docker安装,

mkdir ~/DockerDesktop
mkdir ~/DockerDesktop/Kafka
cd ~/DockerDesktop/Kafka
mkdir node1 node2 node3 node4 node5
vim docker-compose.yml
version: '3'

services:
  Kafka1:
    image: wurstmeister/kafka
    hostname: Kafka1
    environment:
      KAFKA_ADVERTISED_HOST_NAME: Kafka1
      KAFKA_ADVERTISED_PORT: 9092
      KAFKA_ZOOKEEPER_CONNECT: Zookeeper1:2181,Zookeeper2:2181,Zookeeper3:2181,Zookeeper4:2181,Zookeeper5:2181
    volumes:
    - ~/DockerDesktop/Kafka/node1:/kafka
    external_links:
    - Zookeeper1
    - Zookeeper2
    - Zookeeper3
    - Zookeeper4
    - Zookeeper5
    networks:
      default:
        ipv4_address: 172.17.2.1

  Kafka2:
    image: wurstmeister/kafka
    hostname: Kafka2
    environment:
      KAFKA_ADVERTISED_HOST_NAME: Kafka2
      KAFKA_ADVERTISED_PORT: 9092
      KAFKA_ZOOKEEPER_CONNECT: Zookeeper1:2181,Zookeeper2:2181,Zookeeper3:2181,Zookeeper4:2181,Zookeeper5:2181
    volumes:
    - ~/DockerDesktop/Kafka/node2:/kafka
    external_links:
    - Zookeeper1
    - Zookeeper2
    - Zookeeper3
    - Zookeeper4
    - Zookeeper5
    networks:
      default:
        ipv4_address: 172.17.2.2

  Kafka3:
    image: wurstmeister/kafka
    hostname: Kafka3
    environment:
      KAFKA_ADVERTISED_HOST_NAME: Kafka3
      KAFKA_ADVERTISED_PORT: 9092
      KAFKA_ZOOKEEPER_CONNECT: Zookeeper1:2181,Zookeeper2:2181,Zookeeper3:2181,Zookeeper4:2181,Zookeeper5:2181
    volumes:
    - ~/DockerDesktop/Kafka/node3:/kafka
    external_links:
    - Zookeeper1
    - Zookeeper2
    - Zookeeper3
    - Zookeeper4
    - Zookeeper5
    networks:
      default:
        ipv4_address: 172.17.2.3

  Kafka4:
    image: wurstmeister/kafka
    hostname: Kafka4
    environment:
      KAFKA_ADVERTISED_HOST_NAME: Kafka4
      KAFKA_ADVERTISED_PORT: 9092
      KAFKA_ZOOKEEPER_CONNECT: Zookeeper1:2181,Zookeeper2:2181,Zookeeper3:2181,Zookeeper4:2181,Zookeeper5:2181
    volumes:
    - ~/DockerDesktop/Kafka/node4:/kafka
    external_links:
    - Zookeeper1
    - Zookeeper2
    - Zookeeper3
    - Zookeeper4
    - Zookeeper5
    networks:
      default:
        ipv4_address: 172.17.2.4


  Kafka5:
    image: wurstmeister/kafka
    hostname: Kafka5
    environment:
      KAFKA_ADVERTISED_HOST_NAME: Kafka5
      KAFKA_ADVERTISED_PORT: 9092
      KAFKA_ZOOKEEPER_CONNECT: Zookeeper1:2181,Zookeeper2:2181,Zookeeper3:2181,Zookeeper4:2181,Zookeeper5:2181
    volumes:
    - ~/DockerDesktop/Kafka/node5:/kafka
    external_links:
    - Zookeeper1
    - Zookeeper2
    - Zookeeper3
    - Zookeeper4
    - Zookeeper5
    networks:
      default:
        ipv4_address: 172.17.2.5


networks:
  default:
    external:
      name: net17

执行下面的指令,Kafka集群开始运行

docker-compose up

看到了输出

Kafka3_1  | [2020-04-18 10:26:27,441] INFO [Transaction Marker Channel Manager 1002]: Starting (kafka.coordinator.transaction.TransactionMarkerChannelManager)
Kafka4_1  | [2020-04-18 10:26:27,451] INFO [ExpirationReaper-1005-AlterAcls]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
Kafka5_1  | [2020-04-18 10:26:27,473] INFO [TransactionCoordinator id=1001] Starting up. (kafka.coordinator.transaction.TransactionCoordinator)
Kafka5_1  | [2020-04-18 10:26:27,524] INFO [TransactionCoordinator id=1001] Startup complete. (kafka.coordinator.transaction.TransactionCoordinator)
Kafka5_1  | [2020-04-18 10:26:27,554] INFO [Transaction Marker Channel Manager 1001]: Starting (kafka.coordinator.transaction.TransactionMarkerChannelManager)
Kafka1_1  | [2020-04-18 10:26:27,635] INFO [TransactionCoordinator id=1003] Starting up. (kafka.coordinator.transaction.TransactionCoordinator)
Kafka1_1  | [2020-04-18 10:26:27,644] INFO [TransactionCoordinator id=1003] Startup complete. (kafka.coordinator.transaction.TransactionCoordinator)
Kafka1_1  | [2020-04-18 10:26:27,669] INFO [Transaction Marker Channel Manager 1003]: Starting (kafka.coordinator.transaction.TransactionMarkerChannelManager)
Kafka2_1  | [2020-04-18 10:26:27,748] INFO [ExpirationReaper-1004-AlterAcls]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
Kafka4_1  | [2020-04-18 10:26:27,753] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
Kafka3_1  | [2020-04-18 10:26:27,843] INFO [ExpirationReaper-1002-AlterAcls]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
Kafka4_1  | [2020-04-18 10:26:27,882] INFO [SocketServer brokerId=1005] Started data-plane processors for 1 acceptors (kafka.network.SocketServer)
Kafka4_1  | [2020-04-18 10:26:27,945] INFO Kafka version: 2.4.1 (org.apache.kafka.common.utils.AppInfoParser)
Kafka4_1  | [2020-04-18 10:26:27,950] INFO Kafka commitId: c57222ae8cd7866b (org.apache.kafka.common.utils.AppInfoParser)
Kafka4_1  | [2020-04-18 10:26:27,955] INFO Kafka startTimeMs: 1587205587891 (org.apache.kafka.common.utils.AppInfoParser)
Kafka4_1  | [2020-04-18 10:26:27,976] INFO [KafkaServer id=1005] started (kafka.server.KafkaServer)
Kafka2_1  | [2020-04-18 10:26:27,989] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
Kafka1_1  | [2020-04-18 10:26:28,076] INFO [ExpirationReaper-1003-AlterAcls]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
Kafka3_1  | [2020-04-18 10:26:28,095] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
Kafka2_1  | [2020-04-18 10:26:28,190] INFO [SocketServer brokerId=1004] Started data-plane processors for 1 acceptors (kafka.network.SocketServer)
Kafka2_1  | [2020-04-18 10:26:28,239] INFO Kafka version: 2.4.1 (org.apache.kafka.common.utils.AppInfoParser)
Kafka2_1  | [2020-04-18 10:26:28,241] INFO Kafka commitId: c57222ae8cd7866b (org.apache.kafka.common.utils.AppInfoParser)
Kafka2_1  | [2020-04-18 10:26:28,243] INFO Kafka startTimeMs: 1587205588196 (org.apache.kafka.common.utils.AppInfoParser)
Kafka2_1  | [2020-04-18 10:26:28,244] INFO [KafkaServer id=1004] started (kafka.server.KafkaServer)
Kafka3_1  | [2020-04-18 10:26:28,253] INFO [SocketServer brokerId=1002] Started data-plane processors for 1 acceptors (kafka.network.SocketServer)
Kafka3_1  | [2020-04-18 10:26:28,292] INFO Kafka version: 2.4.1 (org.apache.kafka.common.utils.AppInfoParser)
Kafka3_1  | [2020-04-18 10:26:28,295] INFO Kafka commitId: c57222ae8cd7866b (org.apache.kafka.common.utils.AppInfoParser)
Kafka3_1  | [2020-04-18 10:26:28,297] INFO Kafka startTimeMs: 1587205588257 (org.apache.kafka.common.utils.AppInfoParser)
Kafka3_1  | [2020-04-18 10:26:28,313] INFO [KafkaServer id=1002] started (kafka.server.KafkaServer)
Kafka1_1  | [2020-04-18 10:26:28,327] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
Kafka5_1  | [2020-04-18 10:26:28,365] INFO [ExpirationReaper-1001-AlterAcls]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
Kafka1_1  | [2020-04-18 10:26:28,533] INFO [SocketServer brokerId=1003] Started data-plane processors for 1 acceptors (kafka.network.SocketServer)
Kafka1_1  | [2020-04-18 10:26:28,582] INFO Kafka version: 2.4.1 (org.apache.kafka.common.utils.AppInfoParser)
Kafka1_1  | [2020-04-18 10:26:28,582] INFO Kafka commitId: c57222ae8cd7866b (org.apache.kafka.common.utils.AppInfoParser)
Kafka1_1  | [2020-04-18 10:26:28,584] INFO Kafka startTimeMs: 1587205588534 (org.apache.kafka.common.utils.AppInfoParser)
Kafka1_1  | [2020-04-18 10:26:28,607] INFO [KafkaServer id=1003] started (kafka.server.KafkaServer)
Kafka5_1  | [2020-04-18 10:26:28,931] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
Kafka5_1  | [2020-04-18 10:26:29,129] INFO [SocketServer brokerId=1001] Started data-plane processors for 1 acceptors (kafka.network.SocketServer)
Kafka5_1  | [2020-04-18 10:26:29,218] INFO Kafka version: 2.4.1 (org.apache.kafka.common.utils.AppInfoParser)
Kafka5_1  | [2020-04-18 10:26:29,218] INFO Kafka commitId: c57222ae8cd7866b (org.apache.kafka.common.utils.AppInfoParser)
Kafka5_1  | [2020-04-18 10:26:29,220] INFO Kafka startTimeMs: 1587205589130 (org.apache.kafka.common.utils.AppInfoParser)
Kafka5_1  | [2020-04-18 10:26:29,222] INFO [KafkaServer id=1001] started (kafka.server.KafkaServer)

同时我们在Zookeeper集群也看到了输出

Zookeeper1_1  | 2020-04-18 10:26:09,983 [myid:1] - WARN  [QuorumPeer[myid=1](plain=0.0.0.0:2181)(secure=disabled):Follower@170] - Got zxid 0x500000001 expected 0x1
Zookeeper1_1  | 2020-04-18 10:26:09,990 [myid:1] - INFO  [SyncThread:1:FileTxnLog@284] - Creating new log file: log.500000001
Zookeeper5_1  | 2020-04-18 10:26:09,988 [myid:5] - INFO  [SyncThread:5:FileTxnLog@284] - Creating new log file: log.500000001
Zookeeper2_1  | 2020-04-18 10:26:10,002 [myid:2] - WARN  [QuorumPeer[myid=2](plain=0.0.0.0:2181)(secure=disabled):Follower@170] - Got zxid 0x500000001 expected 0x1
Zookeeper2_1  | 2020-04-18 10:26:10,045 [myid:2] - INFO  [SyncThread:2:FileTxnLog@284] - Creating new log file: log.500000001
Zookeeper4_1  | 2020-04-18 10:26:10,059 [myid:4] - WARN  [QuorumPeer[myid=4](plain=0.0.0.0:2181)(secure=disabled):Follower@170] - Got zxid 0x500000001 expected 0x1
Zookeeper1_1  | 2020-04-18 10:26:10,087 [myid:1] - INFO  [CommitProcessor:1:LearnerSessionTracker@116] - Committing global session 0x500000589e20000
Zookeeper5_1  | 2020-04-18 10:26:10,092 [myid:5] - INFO  [CommitProcessor:5:LeaderSessionTracker@104] - Committing global session 0x500000589e20000
Zookeeper2_1  | 2020-04-18 10:26:10,093 [myid:2] - INFO  [CommitProcessor:2:LearnerSessionTracker@116] - Committing global session 0x500000589e20000
Zookeeper3_1  | 2020-04-18 10:26:10,071 [myid:3] - WARN  [QuorumPeer[myid=3](plain=0.0.0.0:2181)(secure=disabled):Follower@170] - Got zxid 0x500000001 expected 0x1
Zookeeper4_1  | 2020-04-18 10:26:10,098 [myid:4] - INFO  [SyncThread:4:FileTxnLog@284] - Creating new log file: log.500000001
Zookeeper3_1  | 2020-04-18 10:26:10,109 [myid:3] - INFO  [SyncThread:3:FileTxnLog@284] - Creating new log file: log.500000001
Zookeeper1_1  | 2020-04-18 10:26:10,113 [myid:1] - INFO  [CommitProcessor:1:LearnerSessionTracker@116] - Committing global session 0x100000589b30000
Zookeeper2_1  | 2020-04-18 10:26:10,126 [myid:2] - INFO  [CommitProcessor:2:LearnerSessionTracker@116] - Committing global session 0x100000589b30000
Zookeeper2_1  | 2020-04-18 10:26:10,141 [myid:2] - INFO  [CommitProcessor:2:LearnerSessionTracker@116] - Committing global session 0x200000589b20000
Zookeeper4_1  | 2020-04-18 10:26:10,144 [myid:4] - INFO  [CommitProcessor:4:LearnerSessionTracker@116] - Committing global session 0x500000589e20000
Zookeeper3_1  | 2020-04-18 10:26:10,137 [myid:3] - INFO  [CommitProcessor:3:LearnerSessionTracker@116] - Committing global session 0x500000589e20000
Zookeeper1_1  | 2020-04-18 10:26:10,171 [myid:1] - INFO  [CommitProcessor:1:LearnerSessionTracker@116] - Committing global session 0x200000589b20000
Zookeeper3_1  | 2020-04-18 10:26:10,199 [myid:3] - INFO  [CommitProcessor:3:LearnerSessionTracker@116] - Committing global session 0x100000589b30000
Zookeeper4_1  | 2020-04-18 10:26:10,176 [myid:4] - INFO  [CommitProcessor:4:LearnerSessionTracker@116] - Committing global session 0x100000589b30000
Zookeeper4_1  | 2020-04-18 10:26:10,202 [myid:4] - INFO  [CommitProcessor:4:LearnerSessionTracker@116] - Committing global session 0x200000589b20000
Zookeeper3_1  | 2020-04-18 10:26:10,203 [myid:3] - INFO  [CommitProcessor:3:LearnerSessionTracker@116] - Committing global session 0x200000589b20000
Zookeeper4_1  | 2020-04-18 10:26:10,204 [myid:4] - INFO  [CommitProcessor:4:LearnerSessionTracker@116] - Committing global session 0x200000589b20001
Zookeeper4_1  | 2020-04-18 10:26:10,209 [myid:4] - INFO  [CommitProcessor:4:LearnerSessionTracker@116] - Committing global session 0x200000589b20002
Zookeeper2_1  | 2020-04-18 10:26:10,224 [myid:2] - INFO  [CommitProcessor:2:LearnerSessionTracker@116] - Committing global session 0x200000589b20001
Zookeeper3_1  | 2020-04-18 10:26:10,227 [myid:3] - INFO  [CommitProcessor:3:LearnerSessionTracker@116] - Committing global session 0x200000589b20001
Zookeeper3_1  | 2020-04-18 10:26:10,241 [myid:3] - INFO  [CommitProcessor:3:LearnerSessionTracker@116] - Committing global session 0x200000589b20002
Zookeeper2_1  | 2020-04-18 10:26:10,243 [myid:2] - INFO  [CommitProcessor:2:LearnerSessionTracker@116] - Committing global session 0x200000589b20002
Zookeeper5_1  | 2020-04-18 10:26:10,245 [myid:5] - INFO  [CommitProcessor:5:LeaderSessionTracker@104] - Committing global session 0x100000589b30000
Zookeeper5_1  | 2020-04-18 10:26:10,260 [myid:5] - INFO  [CommitProcessor:5:LeaderSessionTracker@104] - Committing global session 0x200000589b20000
Zookeeper5_1  | 2020-04-18 10:26:10,270 [myid:5] - INFO  [CommitProcessor:5:LeaderSessionTracker@104] - Committing global session 0x200000589b20001
Zookeeper5_1  | 2020-04-18 10:26:10,307 [myid:5] - INFO  [CommitProcessor:5:LeaderSessionTracker@104] - Committing global session 0x200000589b20002
Zookeeper1_1  | 2020-04-18 10:26:10,403 [myid:1] - INFO  [CommitProcessor:1:LearnerSessionTracker@116] - Committing global session 0x200000589b20001
Zookeeper1_1  | 2020-04-18 10:26:10,407 [myid:1] - INFO  [CommitProcessor:1:LearnerSessionTracker@116] - Committing global session 0x200000589b20002

Kafka操作

开始操作

docker exec -it kafka_Kafka1_1 bash
cd /opt/kafka/bin
ls

我们可以看到一大堆东西

connect-distributed.sh               kafka-console-producer.sh            kafka-log-dirs.sh                    kafka-server-start.sh                windows
connect-mirror-maker.sh              kafka-consumer-groups.sh             kafka-mirror-maker.sh                kafka-server-stop.sh                 zookeeper-security-migration.sh
connect-standalone.sh                kafka-consumer-perf-test.sh          kafka-preferred-replica-election.sh  kafka-streams-application-reset.sh   zookeeper-server-start.sh
kafka-acls.sh                        kafka-delegation-tokens.sh           kafka-producer-perf-test.sh          kafka-topics.sh                      zookeeper-server-stop.sh
kafka-broker-api-versions.sh         kafka-delete-records.sh              kafka-reassign-partitions.sh         kafka-verifiable-consumer.sh         zookeeper-shell.sh
kafka-configs.sh                     kafka-dump-log.sh                    kafka-replica-verification.sh        kafka-verifiable-producer.sh
kafka-console-consumer.sh            kafka-leader-election.sh             kafka-run-class.sh                   trogdor.sh

指定Zookeeper1,看看消息,结果啥都没有,因为kafka中没有消息

kafka-topics.sh --zookeeper Zookeeper1:2181 --list

创建主题, --topic 定义topic名字,--replication-factor定义副本数量,--partitions定义分区数量, 我们创建3个副本一个分区的主题first

kafka-topics.sh --zookeeper Zookeeper1:2181 --create --replication-factor 3 --partitions 1 --topic first

看到输出

Created topic first.

然后使用kafka-topics.sh --zookeeper Zookeeper1:2181 --list就可以看到输出了一个first

first

现在我们回到docker外面的宿主机的终端

cd ~/DockerDesktop/Kafka
ls node1/kafka-logs-Kafka1/ node2/kafka-logs-Kafka2 node3/kafka-logs-Kafka3 node4/kafka-logs-Kafka4 node5/kafka-logs-Kafka5

得到了输出,由此可见,我们的node3,node4,node5上分别保留了first的副本,这里还有一个细节,我们现在是在kafka1上执行的命令,这也能说明我们的集群是搭建成功了的

node1/kafka-logs-Kafka1/:
cleaner-offset-checkpoint        log-start-offset-checkpoint      meta.properties                  recovery-point-offset-checkpoint replication-offset-checkpoint

node2/kafka-logs-Kafka2:
cleaner-offset-checkpoint        log-start-offset-checkpoint      meta.properties                  recovery-point-offset-checkpoint replication-offset-checkpoint

node3/kafka-logs-Kafka3:
cleaner-offset-checkpoint        first-0                          log-start-offset-checkpoint      meta.properties                  recovery-point-offset-checkpoint replication-offset-checkpoint

node4/kafka-logs-Kafka4:
cleaner-offset-checkpoint        first-0                          log-start-offset-checkpoint      meta.properties                  recovery-point-offset-checkpoint replication-offset-checkpoint

node5/kafka-logs-Kafka5:
cleaner-offset-checkpoint        first-0                          log-start-offset-checkpoint      meta.properties                  recovery-point-offset-checkpoint replication-offset-checkpoint

然后我们回到docker中,多来几次

kafka-topics.sh --zookeeper Zookeeper2:2181 --create --replication-factor 3 --partitions 1 --topic second
kafka-topics.sh --zookeeper Zookeeper3:2181 --create --replication-factor 3 --partitions 1 --topic third
kafka-topics.sh --zookeeper Zookeeper4:2181 --create --replication-factor 3 --partitions 1 --topic four
kafka-topics.sh --zookeeper Zookeeper5:2181 --create --replication-factor 3 --partitions 1 --topic five

最后再查看宿主机中的磁盘映射,这里一切正常,并且访问zookeeper集群中的任意一台机器都可行

node1/kafka-logs-Kafka1/:
cleaner-offset-checkpoint        log-start-offset-checkpoint      recovery-point-offset-checkpoint second-0
five-0                           meta.properties                  replication-offset-checkpoint    third-0

node2/kafka-logs-Kafka2:
cleaner-offset-checkpoint        log-start-offset-checkpoint      recovery-point-offset-checkpoint second-0
four-0                           meta.properties                  replication-offset-checkpoint

node3/kafka-logs-Kafka3:
cleaner-offset-checkpoint        five-0                           log-start-offset-checkpoint      recovery-point-offset-checkpoint
first-0                          four-0                           meta.properties                  replication-offset-checkpoint

node4/kafka-logs-Kafka4:
cleaner-offset-checkpoint        five-0                           meta.properties                  replication-offset-checkpoint    third-0
first-0                          log-start-offset-checkpoint      recovery-point-offset-checkpoint second-0

node5/kafka-logs-Kafka5:
cleaner-offset-checkpoint        four-0                           meta.properties                  replication-offset-checkpoint
first-0                          log-start-offset-checkpoint      recovery-point-offset-checkpoint third-0

全删掉

kafka-topics.sh --delete --zookeeper Zookeeper1:2181 --topic first
kafka-topics.sh --delete --zookeeper Zookeeper1:2181 --topic second
kafka-topics.sh --delete --zookeeper Zookeeper1:2181 --topic third
kafka-topics.sh --delete --zookeeper Zookeeper1:2181 --topic four
kafka-topics.sh --delete --zookeeper Zookeeper1:2181 --topic five

看到输出,在我的集群中,我发先几秒钟后,就被删干净了

Topic first is marked for deletion.
Note: This will have no impact if delete.topic.enable is not set to true.

为了后续的操作,我们重新创建一个新的主题

kafka-topics.sh --zookeeper Zookeeper5:2181 --create --replication-factor 3 --partitions 2 --topic first

随便起一台Kafka1, 作为生产者, 这里可以用localhost是因为他自己就是集群的一部分

kafka-console-producer.sh --topic first --broker-list localhost:9092

再起另外一台Kafka2作为消费者,这台就开始等待了

kafka-console-consumer.sh --topic first --bootstrap-server localhost:9092

在生成者中输出>hello I am producer, 我们就能在消费者中看到,那么过时的消费者怎么办呢?我们使用上面的指令再起一台消费者Kafka3, 发现他并不能收到hello那条消息了,在生成者中输入>this is the second msg,发现kafka2和kafka3都可以收到消息,然后我们使用下面的指令再其一台Kafka4,等待片刻,发现kafka4收到了所有的消息

kafka-console-consumer.sh --topic first --bootstrap-server localhost:9092 --from-beginning

在宿主机中输入

ls node1/kafka-logs-Kafka1/ node2/kafka-logs-Kafka2 node3/kafka-logs-Kafka3 node4/kafka-logs-Kafka4 node5/kafka-logs-Kafka5

得到输出,可以看到offsets是轮流保存的, 因为分区是为了负载均衡,而备份是为了容错

node1/kafka-logs-Kafka1/:
__consumer_offsets-14            __consumer_offsets-29            __consumer_offsets-4             __consumer_offsets-9             log-start-offset-checkpoint      replication-offset-checkpoint
__consumer_offsets-19            __consumer_offsets-34            __consumer_offsets-44            cleaner-offset-checkpoint        meta.properties
__consumer_offsets-24            __consumer_offsets-39            __consumer_offsets-49            first-1                          recovery-point-offset-checkpoint

node2/kafka-logs-Kafka2:
__consumer_offsets-0             __consumer_offsets-20            __consumer_offsets-35            __consumer_offsets-5             log-start-offset-checkpoint      replication-offset-checkpoint
__consumer_offsets-10            __consumer_offsets-25            __consumer_offsets-40            cleaner-offset-checkpoint        meta.properties
__consumer_offsets-15            __consumer_offsets-30            __consumer_offsets-45            first-0                          recovery-point-offset-checkpoint

node3/kafka-logs-Kafka3:
__consumer_offsets-13            __consumer_offsets-28            __consumer_offsets-38            __consumer_offsets-8             log-start-offset-checkpoint      replication-offset-checkpoint
__consumer_offsets-18            __consumer_offsets-3             __consumer_offsets-43            cleaner-offset-checkpoint        meta.properties
__consumer_offsets-23            __consumer_offsets-33            __consumer_offsets-48            first-0                          recovery-point-offset-checkpoint

node4/kafka-logs-Kafka4:
__consumer_offsets-1             __consumer_offsets-21            __consumer_offsets-36            __consumer_offsets-6             first-1                          recovery-point-offset-checkpoint
__consumer_offsets-11            __consumer_offsets-26            __consumer_offsets-41            cleaner-offset-checkpoint        log-start-offset-checkpoint      replication-offset-checkpoint
__consumer_offsets-16            __consumer_offsets-31            __consumer_offsets-46            first-0                          meta.properties

node5/kafka-logs-Kafka5:
__consumer_offsets-12            __consumer_offsets-22            __consumer_offsets-37            __consumer_offsets-7             log-start-offset-checkpoint      replication-offset-checkpoint
__consumer_offsets-17            __consumer_offsets-27            __consumer_offsets-42            cleaner-offset-checkpoint        meta.properties
__consumer_offsets-2             __consumer_offsets-32            __consumer_offsets-47            first-1                          recovery-point-offset-checkpoint

查看zk中的数据,起一台zk,执行zkCli.sh, 再执行ls /, 其中除了zookeeper文件以外,其他的数据都是Kafka的,部分终端显示如下

Welcome to ZooKeeper!
2020-04-19 07:03:58,554 [myid:localhost:2181] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1154] - Opening socket connection to server localhost/127.0.0.1:2181.
2020-04-19 07:03:58,557 [myid:localhost:2181] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1156] - SASL config status: Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled
2020-04-19 07:03:58,638 [myid:localhost:2181] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@986] - Socket connection established, initiating session, client: /127.0.0.1:41878, server: localhost/127.0.0.1:2181
2020-04-19 07:03:58,690 [myid:localhost:2181] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1420] - Session establishment complete on server localhost/127.0.0.1:2181, session id = 0x1000223de0e000b, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 0] ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]

Kafka架构深入

文件储存

面向主题,消息按照主题分类,生产者生产消息,消费者消费消息

topic是逻辑概念, partition是物理概念,因为文件夹是用topic+parttiton命名的 查看first-0的文件内容, 0000.log实际上存的是数据,不是日志

bash-4.4# ls
00000000000000000000.index      00000000000000000000.log        00000000000000000000.timeindex  leader-epoch-checkpoint

Kafka的配置文件中有谈到, 即上面的000000.log最多只能保存1G,当他超过1G以后,会创建新的.log

### The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
分片和索引
00000000000000000000.index 
00000000000000000000.log
00000000000000170410.index 
00000000000000170410.log
00000000000000239430.index 
00000000000000239430.log

文件名其实值得是当前片段(segment)中最小的消息的偏移量,log只存数据,index存消息在log中的偏移量

当我们要寻找某个消息的时候,先通过二分消息的编号,找到该消息再哪个index中,由于index中的数据都是等长的,所以可以直接用乘法定位index文件中的偏移量,然后根据这个偏移量定位到log文件中的位置

生产者

分区

方便扩展,提高并发,可以指定分区发送,可以指定key发送(key被hash成分区号), 可以不指定分区不指定key发送(会被随机数轮循)

数据可靠性保证

怎么保证可靠?Kafka需要给我们返回值,但是是leader写成功后返回还是follower成功后返回呢?哪个策略好呢?

副本数据同步策略
方案 优点 缺点
半数以上同步则ack 延迟低 选举新leader的时候,容忍n台节点故障,需要2n+1个副本
完全同步则ack 选举新leader的时候,容忍n台节点故障,需要n+1个副本 延迟高
Kafka选择了完全同步才发送ack,这有一个问题,如果同步的时候,有一台机器宕机了,那么永远都不会发送ack了
ISR

in-sync replica set leader 动态维护了一个动态的ISR,只要这个集合中的机器同步完成,就发送ack,选举ISR的时候,根据节点的同步速度和信息差异的条数来决定,在高版本中只保留了同步速度,为什么呢?延迟为什么比数据重要?

由于生产者是按照批次生产的,如果我们保留信息差异,当生产者发送大量信息的时候,直接就拉开了leader和follower的信息差异条数,同步快的follower首先拉小了自己和leader信息差异,这时候他被加入ISR,但最一段时间后他会被同步慢但是,最终信息差异小的follower赶出ISR,这就导致了ISR频繁发生变化,意味着ZK中的节点频繁变化,这个选择不可取

acks
ack级别 操作 数据问题
0 leader收到后就返回ack broker故障可能丢失数据
1 leader写入磁盘后ack 在follower同步前的leader故障可能导致丢失数据
-1 等待ISR的follower写入磁盘后返回ack 在follower同步后,broker发送ack前,leader故障则导致数据重复

acks=-1也会丢失数据,在ISR中只有leader一个的时候发生

数据一致性问题

HW(High Watermark) 高水位, 集群中所有节点都能提供的最新消息 LEO(Log End Offset) 节点各自能提供的最新消息 为了保证数据的一致性,我们只提供HW的消费,就算消息丢了后,消费者也不知道,他看起来就是一致性的

leader故障

当重新选择leader后,为了保证多个副本之间的数据一致性,会通知follower将各自的log文件高于HW的地方截断,重新同步,这里只能保证数据一致性,不能保证数据不丢失或者不重复

精准一致性(Exactly Once)

ACKS 为 -1 则不会丢失数据,即Least Once ACKS 为 1 则生产者的每条数据只发送一次, 即At Most Once 他们一个丢数据一个重复数据

幂等性

开启幂等性,将Producer参数中的enable.idompotence设置为true,Producer会被分配一个PID(Producer ID), 发往同一个Partition的消息会附带序列号,而Broker会对PID,Partition,SeqNumber做缓存,当具有相同的主键消息提交的时候,Broker只会持久化一条,但是要注意PID重启会变化,不同的Partition也有不同的主键,所以幂等性无法保证跨分区会话的Exactly Once。

消费者

分区分配策略

一个consumer group中有多个consumer,一个topic中有多个partition,那么怎么分配呢?

RoundRobin策略
Topic1: x0,x1,x2
Topic2: y0,y1,y2
-> [x0,y2,y1,y0,x1,x2] 
-> [x0,y1,x1],[y2,y0,x2]

把所有主题中的所有partition放到一起,按照hash值排序,然后轮循分配给消费者

这样太混乱了,不太好

Range策略
Topic1: x0,x1,x2
Topic2: y0,y1,y2
-> [x0,x1,y0,y1],[x2,y2]

对于每个主题分开考虑,各自截取一段,分给消费者,

负载不均衡了

重新分配

当消费者的个数发生变化的时候,就会触发重新分配

offset维护

按照消费者组、主题、分区来维护offset,不能按照消费者维护,要是这样就不能让消费者组具有动态性质了 进入zk中

ls /brokers # 查看kafka集群
ls /brokers/ids # 查看ids
ls /brokers/topics # 查看主题
ls /consumers # 查看消费者组

消费者会默认生成一个消费者组的编号,其中有offset/mytopic/0

单机高效读写

顺序写磁盘

写磁盘的时候一直使用追加,官方数据表明同样的磁盘,顺序写可以达到600M/s但是随机写只有100K/s,

零拷贝技术

一般情况下,用户读取文件需要操作系统来协助,先读到内核空间,然后读到用户空间,然后写入内核空间,最后写入磁盘,零拷贝技术允许直接将这个拷贝工作交给操作系统完成

Zookeeper

Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线、topic分区副本分配和leader选举等工作

Kafka事务

Producer事务

引入全局唯一的Transaction ID,替代PID,为了管理Transaction,Kafka引入了Transaction Producer和Transaction Coordinator交互获得Transaction ID。

Consumer事务

相对弱一些,用户可以自己修改offset或者跨segment的消费如果出错并且等满7天以后,segment被删除了,这些都导致问题

Kafka API

消息发送流程

Kafka的Producer发送消息是异步消息,两个线程main和sender,

发送消息的时候分三步,先经过拦截器,然后经过序列化器,最后经过分区器,最后才发出去

创建kafka项目

springinit 里面选择kafka

### 指定kafka集群
bootstrap.servers=172.17.1.1:9092
### ack应答级别
acks=all
### 重试次数
retries=3
### 批次大小 16K, 当超过16K就提交
batch.size=16384
### 等待时间 , 当超过1ms就提交
linger.ms=1
### RecordAccmulator缓冲区大小 32M
buffer.memory=33554432
### key value 的序列化类
key.serializer=org.apache.kafka.serialization.StringSerializer
value.serializer=org.apache.kafka.serialization.StringSerializer
package com.wsx.study.kafka.debug;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

public class Main {
    public static void main(String[] args) {
        // 创建Kafka生产者配置信息
        try {
            Properties properties = new Properties();
            FileInputStream in = new FileInputStream("KafkaProducer.properties");
            properties.load(in);
            in.close();
            KafkaProducer<String, String> stringStringKafkaProducer = new KafkaProducer<>(properties);
            for (int i = 0; i < 10; i++) {
                stringStringKafkaProducer.send(new ProducerRecord<>("first", "javarecord" + i));
            }
            stringStringKafkaProducer.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后创建消费者

kafka-console-consumer.sh --topic first --bootstrap-server localhost:9092
### 指定kafka集群
bootstrap.servers=172.17.2.1:9092 # 日了狗了,这些mac似乎不行了
### ack应答级别
acks=all
### 重试次数
retries=3
### 批次大小 16K, 当超过16K就提交
batch.size=16384
### 等待时间 , 当超过1ms就提交
linger.ms=1
### RecordAccmulator缓冲区大小 32M
buffer.memory=33554432
### key value 的序列化类
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer
package com.wsx.study.kafka.debug;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class Main {
    public void test() {
        // 创建Kafka生产者配置信息
        try {
            Properties properties = new Properties();
            InputStream in =  getClass().getClassLoader().getResourceAsStream("KafkaProducer.properties");
            properties.load(in);
            assert in != null;
            in.close();
            KafkaProducer<String, String> stringStringKafkaProducer = new KafkaProducer<>(properties);
            for (int i = 0; i < 1; i++) {
                stringStringKafkaProducer.send(new ProducerRecord<>("first", "javarecord" + i));
            }
            stringStringKafkaProducer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Main().test();
    }
}

消费者

            for (int i = 0; i < 1; i++) {
                stringStringKafkaProducer.send(new ProducerRecord<>("first", "javarecord" + i), (recordMetadata, e) -> {
                    if(e==null){
                        System.out.println(recordMetadata.offset()+recordMetadata.offset());
                    }
                });
            }

自己写分区器

配置文件配置一下就可以了

class MyPartitioner implements Partitioner {

    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        return 0;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

生产者

同理,

consumer.subscribe(Arrays.asList("first"));
while(true){
  ConsumerRecords<String,Strings> consumerRecods = consumer.poll(long timeout); // 延迟
}
如何--beginning

auto.offset.reset 当没有初始offset或者offset被删除了(数据过期)就会启动earliest,从最老的数据开始消费,这个东西不是0,他叫earlist,是最早不是开头

默认值是latest, 因为命令行的创建出来的是新的消费者组,所以启用了earliest

想要重新开始消费,要设earlist且换新的消费者组

offset加速

消费者只会在启动的时候拉取一次offset,如果没有自动提交offset,那么消费者就不会提交,这会导致数据不一致,如果这个时候消费者被强制终止,那么你下一次跑这个代码的时候,还是从之前的offset开始消费,除非你提交

enable.auto.commit

可以按时间提交

手动提交

同步: 当前线程会阻塞直到offset提交成功 异步: 加一个回调函数就可以

问题

自动提交速度快可能丢数据,比如我还没处理完,他就提交了,然后我挂了,数据就丢了 自动提交速度慢可能重复数据,我处理完了,他还没提交,然后我挂了,下次又来消费一次数据 手动提交也有这些问题

自定义offset

由于消息往往对消费者而言,可能存在本地的sql中,所以就可以和数据以前做成一个事务,

这可以解决问题,但是碰到了rebalace问题,即当一个消费者挂了以后消息资源要重新分配,借助ConsumerRebalanceListener,点这里, 自己维护一个消费者组数据、自己写代码,(可怕)

自定义拦截器

configure 读取配置信息 onSend(ProducerRecord) 拦截 onAcknowledgement(RecordMetadata,Exception), 这个和上面的回调函数一样,拦截器也会收到这个东西, close 拦截

例子

现在有个需求,消息发送前在消息上加上时间挫,消息发送后打印发送成功次数和失败次数 时间拦截器

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        //  取出数据
        String value = producerRecord.value();
        // 创建新的
        return new ProducerRecord<String, String>(producerRecord.topic(),
                producerRecord.partition(), producerRecord.timestamp(),
                producerRecord.key(), System.currentTimeMillis()+","+producerRecord.value(),
                producerRecord.headers());
    }

计数拦截器

class CountInterceptor implements ProducerInterceptor<String, String>{

    int success = 0;
    int error = 0;

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        return null;
    }

    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        if(e==null){
            success++;
        }else{
            error++;
        }
    }

    @Override
    public void close() {
        System.out.println("success:"+success);
        System.out.println("error:"+error);
    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

注意如果拦截器太多,考虑使用拦截器链

拦截器、序列化器、分区器都是卸载配置文件中的

Kafka监控

Kafka Eagle 修改Kafka的kafka-server-start.sh, 对其进行修改,

if ["x$KAFKA_HEAP_OPTS" = "x"] then
  export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128...."

然后分发这个文件,再上传kafka-eagle-bin-1.3.7.tar.gz到集群的/opt/software中,

配置文件

可以跟踪多个集群 kafka.eagle.zk.cluster.alisa = cluster1,cluster2 cluster1.zk.list=ip:port,ip:port,... 保存的位置 cluster1.kafka.eagle.offset.storage=kafka 监控图表 kafka.eagle.metrics.charts=true 启动 bin/ke.sh start http://192.168.9.102:8048/ke 有很多信息都能看到,

Kafka面试题

Kafka 的ISR OSR AR

ISR+OSR=AR

HW LEO

高水位,LEO

怎么体现消息的顺序

区内有序

分区器、序列化器、拦截器

生产者的整体结构,几个线程

消费者组中的消费者个数超过了topic就会有消费者收不到数据对吗

对的

提交的是offset还是offset+1

是offset+1

什么时候重复消费

先处理数据后提交

什么时候漏消费

先提交后处理数据

创建topuc背后的逻辑

zk中创建新的topic节点,触发controller的监听,controller创建topic然后更新metadata cache

topic分区可以增加吗?

可以,不可以减少

kafka内部有topic吗

有个offset

kafka分区分配的概念

Rodrobin和range

日志目录结构

二分->index->log

kafka controller的作用

相当于老大,他是干活的,他和zk通信,还通知其他人

kafka什么时候选举

选controller,leader,ISR

失效副本是什么

这个问题很奇怪,大概是想说重新选举leader的时候,那个HW变化

为什么kafka高效率

顺序写+0拷贝

架构

压测

有一个***perf-test.sh

消息积压,消费者消费能力不够怎么办

增加topic分区、提高消费者组的消费者数量、提高消费者每次拉取的数量(默认500)

参考资料

Kafka教程 docker安装kafka

分布式系统

集群的到来

你为什么要使用集群? 如果一个问题可以在单机上解决,那就使用单机的方法,在使用分布式解决问题前,要充分尝试别的思路,因为分布式系统会让问题解决变得复杂

并行、容错、通信、安全/孤立

难以解决的局部错误

人们设计分布式系统的根本原因是为了获得更高的性能,你的集群到底发挥了多大的能力?1000台发挥了1000倍的能力吗?

如果一个系统,只需要提高几倍的机器的数量,就可以提高几倍的性能或者吞吐量,这就是一个巨大的胜利,因为不需要花高价雇程序员

并行

比方有一个web服务器,一夜之间火了,上亿的人来访问,就需要更多的web服务器,可是当你的web服务器的数量增大以后,数据库又成为了瓶颈,限制了性能,你可以尝试更多的数据库,但是这又会问到其他问题。

容错

假设每台计算机一年只坏一次,那么1000台计算机的集群,每天坏3台,计算机崩溃成为了常见的错误,各种问题,网络,交换机,计算机过热,错误总会发生,

可用性

某个系统经过精心设计,在某些错误下,系统可以正常运行,提供完整的服务,就想没有发生错误一样,比如多个备份,即使一个备份出错,但是另一个备份是正常的

我们的可用性是在一定的条件下的,并非所有的错误都不怕

修复

系统在修复前无法继续工作,当恢复后会正常工作,这一点非常重要,这就是最重要的指标,

非易失性储存

他们更新起来昂贵,构建高性能容错系统非常繁琐,聪明的办法是避免写入非易失性储存

使用复制

管理复制来实现容错,这也很复杂,你需要保证一致性,

强一致性

get得到的值一定是最新的put的值

弱一致性

某人put,你可能看到的是旧值,但一段时间以后他会变成新值,我们不保证时间。我们要避免通信,所以我们更加倾向于弱一致性,强一致性太麻烦了,代价太高

把所有备份放到一个机房,放到一个机架上,这非常糟糕,要是有人不小心绊倒了电源线,就糟糕了,位了让副本更好的容错,人们希望将不同的副本尽可能的分开远放,例如放在不同的城市,

副本在几千英里以外,想抱着强一致性特别困难,你要去等待多个服务器来给你反馈,等个20-30毫秒,这难忍受,并浪费了资源。

MapReduce

要在TB数量的数据中分析,需要大量的并行计算,我们会把输入分成多份,对每一份进行map,他会生成一堆keyvalue,然后是数据移动,按照key合并,并交给reduce处理,不同的reduce输出不同的结果

MapReduce最大的瓶颈是网络,我们要尽量避免数据的传输,

shuffle

map之后的数据,传给reduce,往往意味着由行储存变为列储存,

链接

分布式系统

C++

Boost

Boost学习笔记1 - Boost入门

Boost 与c++

Boost是基于C++标准的现代库,他的源码按照Boost Software License 来发布,允许任何人自由使用、修改和分发。

Boost有哪些功能?

Boost强大到拥有超过90个库,但我们暂时只学习其中一部分

Any

boost::any是一个数据类型,他可以存放任意的类型,例如说一个类型为boost::any的变量可以先存放一个int类型的值,然后替换为一个std::string的类型字符串。

Array

好像是一个数组容器,和std::vector应该差别不大。

and more ...

这个系列的博客来干嘛?

这个系列的博客用来介绍Boost提供的接口,不对Boost进行源码分析,关于Boost的源码,预计会在Boost源码分析笔记系列的博客中。

Boost学习笔记2 - Boost.Any

Boost.Any

Any在C++17被编入STL C++是强类型语言,没有办法向Python那样把一个int赋值给一个double这种*操作,而Boost.Any库为我们模拟了这个过程,使得我们可以实现弱类型语言的东西。

在基本数据类型中玩弱类型

#### include <boost/any.hpp>
int main(){
  boost::any a = 1;
  a = 3.14;
  a = true;
}

这样的代码是允许编译的,大概是因为boost::any内部储存的是指针。

char数组不行了

#### include <boost/any.hpp>
int main(){
  boost::any a = 1;
  a = "hello world";
}

上诉代码可以编译和运行,但是一定会碰到问题的,当我们把char数组弄过去的时候,就不太行了,原因是char[]不支持拷贝构造,但是我们可以通过把std::string来解决这个问题。

用std::string代替char数组

#### include <boost/any.hpp>
int main(){
  boost::any a = 1;
  a = std::string("hello world");
}

可以见到我们用string完美地解决了这个问题。

写很容易,如何读呢?

我们已经学习了boost::any的写操作,但是应该如何读取呢?

#### include <boost/any.hpp>
#### include <iostream>
int main(){
  boost::any a = 1;
  std::cout << boost::any_cast<int>(a) << std::endl;
}

boost提供了一个模版函数any_cast<T>来对我们的any类进行读取

类型不对会抛出异常

有了any<T>的模版,看起来我们可以对boost进行任意读取,我们试试下这个

#### include <boost/any.hpp>
#### include <iostream>
int main() {
  boost::any a = 1;
  a = "hello world";
  std::cout << boost::any_cast<int>(a) << std::endl;
}

抛出了如下异常

libc++abi.dylib: terminating with uncaught exception of type boost::wrapexcept<boost::bad_any_cast>: boost::bad_any_cast: failed conversion using boost::any_cast

实际上上诉代码是永远无法成功的。因为你把一个char数组传了进去。

成员函数

boost的any是有很多成员函数的。比方说empty可以判断是否为空,type可以得到类型信息,

#### include <boost/any.hpp>
#### include <iostream>
#### include <typeinfo>

int main() {
  boost::any a = std::string("asdf");
  if (!a.empty()) {
    std::cout << a.type().name() << std::endl;
    a = 1;
    std::cout << a.type().name() << std::endl;
  }
}

代码运行结果如下,表示首先是字符串,然后是整形。

NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
i

拿到指针

当我们把一个any的地址传给any_cast的时候,我们会得到any内部数据的指针,

#### include <boost/any.hpp>
#### include <iostream>

int main()
{
  boost::any a = 1;
  int *i = boost::any_cast<int>(&a);
  std::cout << *i << std::endl;
}

Boost 源码分析笔记1 - remove_cv

先挑一个简单的来分析

remove_cv 这个模版类能够帮我们去掉类型的const,他的实现很简单,即使用模版元技术:

template <class T> struct remove_cv{ typedef T type; };
template <class T> struct remove_cv<T const>{ typedef T type;  };
template <class T> struct remove_cv<T volatile>{ typedef T type; };
template <class T> struct remove_cv<T const volatile>{ typedef T type; };

这个代码应该非常容易理解,remove_cv的模版是一个T,我们对他做模版偏特化,将const 和volatile分离,然后使用::value就可以得到没有const、volatile的类型了,所以这个类也叫remove_cv。

Boost 源码分析笔记2 - is_array

is array

要先看下面的笔记,才能看懂此篇。 {% post_link 'Boost-源码分析笔记3-integral-constant' 点我开始阅读 %}

实现

is array的实现非常简单,我们先假设所有的都不是array,即如第四行所示,然后利用偏特化,特判掉所有的array即可,让他们继承true_type,这样我们在使用的时候用::value即可判断。

#### if defined( __CODEGEARC__ )
   template <class T> struct is_array : public integral_constant<bool, __is_array(T)> {};
#### else
   template <class T> struct is_array : public false_type {};
#### if !defined(BOOST_NO_ARRAY_TYPE_SPECIALIZATIONS)
   template <class T, std::size_t N> struct is_array<T[N]> : public true_type {};
   template <class T, std::size_t N> struct is_array<T const[N]> : public true_type{};
   template <class T, std::size_t N> struct is_array<T volatile[N]> : public true_type{};
   template <class T, std::size_t N> struct is_array<T const volatile[N]> : public true_type{};
#### if !BOOST_WORKAROUND(__BORLANDC__, < 0x600) && !defined(__IBMCPP__) &&  !BOOST_WORKAROUND(__DMC__, BOOST_TESTED_AT(0x840))
   template <class T> struct is_array<T[]> : public true_type{};
   template <class T> struct is_array<T const[]> : public true_type{};
   template <class T> struct is_array<T const volatile[]> : public true_type{};
   template <class T> struct is_array<T volatile[]> : public true_type{};
#### endif
#### endif

Boost 源码分析笔记3 - integral_constant

integral_consant

这也是一个模版元技术,他储存了自己的类型,模版的类型,模版的值的类型,他的实现如下

 template <class T, T val>
   struct integral_constant
   {
      typedef mpl::integral_c_tag tag;
      typedef T value_type;
      typedef integral_constant<T, val> type;
      static const T value = val;

      operator const mpl::integral_c<T, val>& ()const
      {
         static const char data[sizeof(long)] = { 0 };
         static const void* pdata = data;
         return *(reinterpret_cast<const mpl::integral_c<T, val>*>(pdata));
      }
      BOOST_CONSTEXPR operator T()const { return val; }
   };

这里很明显了,value是值,value_type是value的类型,type是自己的类型。

true_type false_type

这里就很有意思了,看看就懂

typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;

可能有人会问这个有什么用,其实这样的,很多时候我们需要为我们的类添加一个value,表示true或者false,正常的实现方法是写两遍,一遍处理全部,另一遍特化false,这样写的话,代码复用就太low了,这时候,其实我们只需要实现一遍基类,派生的时候一个继承true,另一个继承false就OK了。

Boost 源码分析笔记4 - is_function

这个代码就nb了,我还没看懂,先留个坑,我猜了一下,大概是用来判断一个类型是否是函数指针的。

Boost 源码分析笔记5 - remove_bounds

remove_bounds

这个模版元我还真没猜出他的功能,话说怎么可能有人想得到这个bounds指的是数组的bounds呢?这个模版元的功能是传入一个数组,传出他的内容,即将T[]映射为T。注意: remove_bounds就是remove_extent。

template <class T> struct remove_extent{ typedef T type; };

#### if !defined(BOOST_NO_ARRAY_TYPE_SPECIALIZATIONS)
template <typename T, std::size_t N> struct remove_extent<T[N]> { typedef T type; };
template <typename T, std::size_t N> struct remove_extent<T const[N]> { typedef T const type; };
template <typename T, std::size_t N> struct remove_extent<T volatile [N]> { typedef T volatile type; };
template <typename T, std::size_t N> struct remove_extent<T const volatile [N]> { typedef T const volatile type; };
#### if !BOOST_WORKAROUND(__BORLANDC__, BOOST_TESTED_AT(0x610)) && !defined(__IBMCPP__) &&  !BOOST_WORKAROUND(__DMC__, BOOST_TESTED_AT(0x840))
template <typename T> struct remove_extent<T[]> { typedef T type; };
template <typename T> struct remove_extent<T const[]> { typedef T const type; };
template <typename T> struct remove_extent<T volatile[]> { typedef T volatile type; };
template <typename T> struct remove_extent<T const volatile[]> { typedef T const volatile type; };
#### endif
#### endif

还是老样子,数组就特判掉,然后返回其头,否则就返回他的本身。

Boost 源码分析笔记6 - remove_reference

remove_reference

这个名字就很棒,就是移除引用的意思。同样他也是模版元技术,他先将所有的类型映射为自己,然后通过模版偏特化的方式将那些引用映射为本身。这里有一个c++的特性即下面代码 这个代码看懂的人应该不多了。

#### include <iostream>

void f(int& x) { std::cout << "&" << std::endl; }
void f(int&& x) { std::cout << "&&" << std::endl; }

int main() {
  int a = 1, b = 2, c = 3, d = 4;
  f(a);
  f(b);
  f(c);
  f(d);
  f(1);
  f(2);
  f(3);
  f(4);
}

这里的&&就是右值引用的意思,所以输出是

&
&
&
&
&&
&&
&&
&&

然后我们来看源码

namespace detail{
//
// We can't filter out rvalue_references at the same level as
// references or we get ambiguities from msvc:
//
template <class T>
struct remove_rvalue_ref
{
   typedef T type;
};
#### ifndef BOOST_NO_CXX11_RVALUE_REFERENCES
template <class T>
struct remove_rvalue_ref<T&&>
{
   typedef T type;
};
#### endif

} // namespace detail

template <class T> struct remove_reference{ typedef typename boost::detail::remove_rvalue_ref<T>::type type; };
template <class T> struct remove_reference<T&>{ typedef T type; };

#### if defined(BOOST_ILLEGAL_CV_REFERENCES)
// these are illegal specialisations; cv-qualifies applied to
// references have no effect according to [8.3.2p1],
// C++ Builder requires them though as it treats cv-qualified
// references as distinct types...
template <class T> struct remove_reference<T&const>{ typedef T type; };
template <class T> struct remove_reference<T&volatile>{ typedef T type; };
template <class T> struct remove_reference<T&const volatile>{ typedef T type; };
#### endif

同样的我们使用模版元技术,将引用就消除了。

Boost 源码分析笔记7 - decay

这篇博客要求提前知道

{% post_link 'Boost-源码分析笔记2-is-array' is_array %} {% post_link 'Boost-源码分析笔记4-is-function' is_function%} {% post_link 'Boost-源码分析笔记5-remove-bounds' remove_bounds%} {% post_link 'Boost-源码分析笔记6-remove-reference' remove_reference%} {% post_link 'Boost-源码分析笔记1-remove-cv' remove_cv%}

decay

这个模版元的意思是移除引用、移除const、移除volatile、数组移除范围、函数变成指针。

   namespace detail
   {

      template <class T, bool Array, bool Function> struct decay_imp { typedef typename remove_cv<T>::type type; };
      template <class T> struct decay_imp<T, true, false> { typedef typename remove_bounds<T>::type* type; };
      template <class T> struct decay_imp<T, false, true> { typedef T* type; };

   }

    template< class T >
    struct decay
    {
    private:
        typedef typename remove_reference<T>::type Ty;
    public:
       typedef typename boost::detail::decay_imp<Ty, boost::is_array<Ty>::value, boost::is_function<Ty>::value>::type type;
    };

实际上做起来的时候是先移除引用,最后移除cv的。

Boost 源码分析笔记8 - any

这篇博客需要

{% post_link 'Boost-源码分析笔记7-decay' decay %}

我们来分析一个简单的any

如{% post_link 'Boost-学习笔记2-Boost-Any' Any接口学习 %}所示,any能够支持我们的c++向python一样,给一个变量瞎赋值,这也太爽了。

构造函数如下

        template<typename ValueType>
        any(const ValueType & value)
          : content(new holder<
                BOOST_DEDUCED_TYPENAME remove_cv<BOOST_DEDUCED_TYPENAME decay<const ValueType>::type>::type
            >(value))
        {
        }

这里是接受任意的类型,然后对这个类型使用decay得到他的基本类型,最后让holder来替我们管理。holder保持了一个输入参数的副本,我们发现这个holder类型的值放到了一个叫content的指针中。

holder

holder继承自placeholder,placeholder是一个接口,我们不去管他,holder内部的副本保存在held中。

        template<typename ValueType>
        class holder
#### ifndef BOOST_NO_CXX11_FINAL
          final
#### endif
          : public placeholder
        {
        public: // structors

            holder(const ValueType & value)
              : held(value)
            {
            }

#### ifndef BOOST_NO_CXX11_RVALUE_REFERENCES
            holder(ValueType&& value)
              : held(static_cast< ValueType&& >(value))
            {
            }
#### endif
        public: // queries

            virtual const boost::typeindex::type_info& type() const BOOST_NOEXCEPT
            {
                return boost::typeindex::type_id<ValueType>().type_info();
            }

            virtual placeholder * clone() const
            {
                return new holder(held);
            }

        public: // representation

            ValueType held;

        private: // intentionally left unimplemented
            holder & operator=(const holder &);
        };

any数据类型的读取

any数据有两种读取方式,一是指针,想要读取出里面的元素,显然元素是operand->content->held, 我们要得到他的指针的话,先构造出指针来: holder<remove_cv<ValueType>::type>*, 因为operand->content是placeholer,这也就是为什么下面的代码的括号在->held之前的原因。最后用boost::addressof取出地址就可以了。

    template<typename ValueType>
    ValueType * any_cast(any * operand) BOOST_NOEXCEPT
    {
        return operand && operand->type() == boost::typeindex::type_id<ValueType>()
            ? boost::addressof(
                static_cast<any::holder<BOOST_DEDUCED_TYPENAME remove_cv<ValueType>::type> *>(operand->content)->held
              )
            : 0;
    }

第二种方式是读取拷贝,先移除引用,调用上面的指针读取,最后指针取内容就可以返回了。

    template<typename ValueType>
    ValueType any_cast(any & operand)
    {
        typedef BOOST_DEDUCED_TYPENAME remove_reference<ValueType>::type nonref;


        nonref * result = any_cast<nonref>(boost::addressof(operand));
        if(!result)
            boost::throw_exception(bad_any_cast());

        // Attempt to avoid construction of a temporary object in cases when
        // `ValueType` is not a reference. Example:
        // `static_cast<std::string>(*result);`
        // which is equal to `std::string(*result);`
        typedef BOOST_DEDUCED_TYPENAME boost::conditional<
            boost::is_reference<ValueType>::value,
            ValueType,
            BOOST_DEDUCED_TYPENAME boost::add_reference<ValueType>::type
        >::type ref_type;
#### ifdef BOOST_MSVC
####   pragma warning(push)
####   pragma warning(disable: 4172) // "returning address of local variable or temporary" but *result is not local!
#### endif
        return static_cast<ref_type>(*result);
#### ifdef BOOST_MSVC
####   pragma warning(pop)
#### endif
    }

any的成员函数

前两个就不说了,直接说第三个,如果content存在,就调用content的type

        bool empty() const BOOST_NOEXCEPT
        {
            return !content;
        }

        void clear() BOOST_NOEXCEPT
        {
            any().swap(*this);
        }

        const boost::typeindex::type_info& type() const BOOST_NOEXCEPT
        {
            return content ? content->type() : boost::typeindex::type_id<void>().type_info();
        }

type是这样实现的

            virtual const boost::typeindex::type_info& type() const BOOST_NOEXCEPT
            {
                return boost::typeindex::type_id<ValueType>().type_info();
            }

Boost学习笔记3 - Boost

Boost::Tuple

boost::tuple是一个元组。在c++11被编入STL 第六行无法通过编译,这说明tuple的长度最长只能是10 第9-12行定义了3个元组 第13行演示了如何通过make_tuple构造元组 第14行演示了如何通过get来访问元组里面的元素 第16行演示了get的返回值是引用 第19-20行演示了tuple的等号操作 第23-27行演示了tuple中可以储存引用 第28行通过tie构造了一个全引用元组

#### include <boost/tuple/tuple.hpp>
#### include <boost/tuple/tuple_comparison.hpp>
#### include <boost/tuple/tuple_io.hpp>
#### include <bits/stdc++.h>

// boost::tuple<int, int, int, int, int, int, int, int, int, int, int>too_long;
int main() {
  // 基本操作
  boost::tuple<int, int, int, int, int, int, int, int, int, int> a(
      1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
  boost::tuple<std::string, std::string> b("hello", "world");
  boost::tuple<std::string, std::string> c("hello", "world2");
  std::cout << boost::make_tuple(1, 2, 3, 4) << std::endl;
  std::cout << "a.get<0>() is " << a.get<0>() << std::endl;
  std::cout << "a is " << a << std::endl;
  a.get<0>() = -1;
  std::cout << "a is " << a << std::endl;
  std::cout << "b is " << b << std::endl;
  std::cout << "b==c is " << (b == c) << std::endl;
  std::cout << "b==b is " << (b == b) << std::endl;

  // 进阶操作
  int x = 1, y = 2;
  boost::tuple<int&, int> reference(boost::ref(x), y);
  // boost::tuple<int&, int> reference(x, y); 也可以
  x = 5;
  std::cout << "reference is " << reference << std::endl;
  auto reference2 = boost::tie(x, y);
  x = 10, y = 11;
  std::cout << "reference2 is " << reference2 << std::endl;
}

输出

(1 2 3 4)
a.get<0>() is 1
a is (1 2 3 4 5 6 7 8 9 10)
a is (-1 2 3 4 5 6 7 8 9 10)
b is (hello world)
b==c is 0
b==b is 1
reference is (5 2)
reference2 is (10 11)

Boost学习笔记4 - Boost

Boost::Variant

boost::variant和any很像,variant和any一样在C++17中被编入STL variant可以指定一部分数据类型,你可以在这一部分中随便赋值,就像下面写到的一样,另外和any的any_cast不一样的是variant使用get<T>来获得内容。

#### include <boost/variant.hpp>
#### include <iostream>
#### include <string>

int main() {
  boost::variant<double, char, std::string> v;
  v = 3.14;
  std::cout << boost::get<double>(v) << std::endl;
  v = 'A';
  // std::cout << boost::get<double>(v) << std::endl; 这句现在会报错
  std::cout << boost::get<char>(v) << std::endl;
  v = "Hello, world!";
  std::cout << boost::get<std::string>(v) << std::endl;
}

访问者模式

variant允许我们使用访问者模式来访问其内部的成员,使用函数boost::apply_visitor来实现,访问者模式使用的时候重载仿函数。仿函数记得继承static_visitor即可。

#### include <boost/variant.hpp>
#### include <iostream>
#### include <string>

struct visit : public boost::static_visitor<> {
  void operator()(double &d) const { std::cout << "double" << std::endl; }
  void operator()(char &c) const { std::cout << "char" << std::endl; }
  void operator()(std::string &s) const { std::cout << "string" << std::endl; }
};

int main() {
  boost::variant<double, char, std::string> v;
  v = 3.14;
  boost::apply_visitor(visit(), v);
  v = 'A';
  boost::apply_visitor(visit(), v);
  v = "Hello, world!";
  boost::apply_visitor(visit(), v);
}

输出

double
char
string

Boost学习笔记5 - Boost

StringAlgorithms

我终于找到一个暂时没有被编入C++17的库了,听说在C++20中他也没进,哈哈哈。

大小写转化

首先上来的肯定就是大小写转化啦,使用函数to_upper_copy(string)就可以了。

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "abcdefgABCDEFG";
  std::cout << boost::algorithm::to_upper_copy(s) << std::endl;
  std::cout << boost::algorithm::to_lower_copy(s) << std::endl;
}

删除子串

erase_all_copy就是说先copy一份,然后再将子串全部删除,如果不带copy就是说直接操作穿进去的母串。下面的代码都可以去掉_copy,erase_first指的是删除第一次出现的,last指的是删除最后一次出现的,nth指的是删除第n次出现的,n从0开始,erase_head值的是删除前n个字符,erase_tail指的是删除后n个字符。

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "000111000111ababab000111000111";
  std::cout << s << std::endl;

  // boost::algorithm::erase_all(s,"ab");
  std::cout << boost::algorithm::erase_all_copy(s, "ab") << std::endl;
  std::cout << boost::algorithm::erase_first_copy(s, "111") << std::endl;
  std::cout << boost::algorithm::erase_last_copy(s, "111") << std::endl;
  std::cout << boost::algorithm::erase_nth_copy(s, "111",0) << std::endl;
  std::cout << boost::algorithm::erase_nth_copy(s, "111",100) << std::endl;
  std::cout << boost::algorithm::erase_head_copy(s, 4) << std::endl;
  std::cout << boost::algorithm::erase_tail_copy(s, 4) << std::endl;
}

查找子串

find一类的函数,同上,他将返回一个iterator_range的迭代器。这个迭代器可以操作子串。注意子串和母串共享空间。

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "000111000111ababab000111000111";
  std::cout << s << std::endl;
  auto x = boost::algorithm::find_first(s,"000");
  x[0] = '2';
  std::cout << s << std::endl;
  //std::cout << boost::algorithm::find_last(s, "111") << std::endl;
}

又是一套代码下来了

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "000111000111ababab000111000111";
  std::cout << s << std::endl;
  auto x = boost::algorithm::find_first(s,"000");
  x = boost::algorithm::find_last(s,"1");
  x = boost::algorithm::find_nth(s,"1",3);
  x = boost::algorithm::find_tail(s,3);
  x = boost::algorithm::find_head(s,3);
  std::cout << s << std::endl;
  //std::cout << boost::algorithm::find_last(s, "111") << std::endl;
}

替换子串

replace又是一套如下

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "000111000111ababab000111000111";
  std::cout << s << std::endl;

  // boost::algorithm::replace_all(s,"ab");
  std::cout << boost::algorithm::replace_all_copy(s, "ab","all") << std::endl;
  std::cout << boost::algorithm::replace_first_copy(s, "111","first") << std::endl;
  std::cout << boost::algorithm::replace_last_copy(s, "111","last") << std::endl;
  std::cout << boost::algorithm::replace_nth_copy(s, "111", 0,"nth") << std::endl;
  std::cout << boost::algorithm::replace_nth_copy(s, "111", 100,"nth") << std::endl;
  std::cout << boost::algorithm::replace_head_copy(s, 2,"Head") << std::endl;
  std::cout << boost::algorithm::replace_tail_copy(s, 2,"Tail") << std::endl;
}

修剪字符串

trim_left_copy 指的是从左边开始修建,删掉空字符等,trim_right_copy是从右边开始修建,trim_copy是两边一起修剪。

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "\t  ab  d d  d d d \t";
  std::cout << "|" << s << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_left_copy(s) << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_right_copy(s) << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_copy(s) << "|" << std::endl;
}

这个代码输出了

|	  ab  d d  d d d 	|
|ab  d d  d d d 	|
|	  ab  d d  d d d|
|ab  d d  d d d|

我们还可以通过指定谓词来修剪使用trim_left_copy_if

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = " 01 0 1 000ab  d d  d d d 11111111";
  std::cout << "|" << s << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_left_copy_if(s,boost::algorithm::is_any_of(" 01")) << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_right_copy_if(s,boost::algorithm::is_any_of(" 01")) << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_copy_if(s,boost::algorithm::is_any_of(" 01")) << "|" << std::endl;
}

更多的谓词,我们还有is_lower、is_upper、is_space等谓词。

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = " 01 0 1 000ab  d d  d d d 11111111";
  std::cout << "|" << s << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_copy_if(s,boost::algorithm::is_space()) << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_copy_if(s,boost::algorithm::is_digit()) << "|" << std::endl<<std::endl;
  s = "aaaBBBaBBaaa";
  std::cout << "|" << s << "|" << std::endl;
  std::cout << "|" << boost::algorithm::trim_copy_if(s,boost::algorithm::is_lower()) << "|" << std::endl;


}

字符串比较

starts_with(s,t)判断s是否以t开头,类似的有ends_with,contains,lexicographical_compare分别表示s是否以t结尾,s是否包含t,s与t的字典序比较。

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::cout << boost::algorithm::starts_with("abcde", "abc") << std::endl;
  std::cout << boost::algorithm::ends_with("abcde", "cde") << std::endl;
  std::cout << boost::algorithm::contains("abcde", "cde") << std::endl;
  std::cout << boost::algorithm::lexicographical_compare("abcde", "cde") << std::endl;
  std::cout << boost::algorithm::lexicographical_compare("abcde", "abcde") << std::endl;
  std::cout << boost::algorithm::lexicographical_compare("cde", "abcde") << std::endl;
}

字符串分割

这个就简单多了,直接split+谓词函数就行了

#### include <boost/algorithm/string.hpp>
#### include <iostream>

int main() {
  std::string s = "abc abc abc * abc ( abc )";
  std::vector<std::string> v;
  boost::algorithm::split(v, s, boost::algorithm::is_any_of(" *()"));
  for (auto x : v) std::cout << x << ".";
  std::cout << std::endl;
}

输出

abc.abc.abc...abc...abc...

我们注意看有些函数前面有个i,比如ierase_all, 这个说的是忽略大小写。

Boost学习笔记6 - Boost

boost::regex

C++11的时候,被编入STL 明天接着整。。。

C++基础

c++基础笔记1 - const与指针

前言

从大一上接触c++,到大一下接触ACM,到现在大三下,我自以为对c++有了很深的理解,其实不然,我不清楚的地方还特别多,准备趁此空闲时间重学c++。

const 与指针

这是这篇博文的重点,常常我们会碰到多种声明

const char* const a = new char[10];
const char* a = new char[10];
char* const a = new char[10];
char* a = new char[10];

他们有什么共性与不同呢?下面的程序演示了区别,注释的地方是非法操作会报错。

#### include <iostream>
using namespace std;
int main() {
  const char* const a = new char[10];
  const char* b = new char[10];
  char* const c = new char[10];
  char* d = new char[10];
  char* e = new char[10];

  // a[0]='e';
  // a=e;

  // b[0] = 'e';
  b = e;

  c[0] = 'e';
  // c = e;

  d[0] = 'e';
  d = e;

  delete[] a, b, c, d, e;

  return 0;
}

下面解释为啥会出现这种情况,我们注意到const关键字,指的是不可修改的意思,对于b而言,const 修饰char*,表面char*不可修改即指针指向的内容不可修改,对于c而言const修饰c,表示c这个指针本身不可修改。

c++基础笔记2 - enum back

enum back

这是这篇博文的重点,enum back 是一个很实用的编程技术,很多人都会用到它,更进一步,enum back技术是模版元编程的基本技术

#### include <iostream>
using namespace std;
class my_class {
  enum { size = 10 };
  int data[size];
};

int main() {}

这里其实我们也可以用static const size = 10;来实现,但是这不影响enum是一个好方法,enum不会导致额外的内存分配。

c++基础笔记3 - const修饰返回值

const 修饰返回值

如果有必要,尽量使用const修饰返回值

#### include <iostream>
using namespace std;

const int sum(int a, int b) { return a + b; }

int main() { return 0; }

有什么好处?

如果你不小心把==写成了=,下面的代码会报错。当然也有肯定是好处多余坏处

#### include <iostream>
using namespace std;

const int sum(int a, int b) { return a + b; }

int main() {
  if (sum(1, 2) = 3) {
    printf("hello world!");
  }
}

c++基础笔记4 - 用const重载成员函数

const 能够重载成员函数

为什么要重载一遍const? 目前笔者也不太懂,只知道const能够让c++代码更加高效。下面的代码解释了如何使用const重载成员函数,大概是这样的,const对象调用成员函数的时候会调用const版,普通的对象调用普通版。

#### include <iostream>
using namespace std;

class my_class {
  int x = 1, y = 2;

 public:
  const int& get() const {
    std::cout << "x" << std::endl;
    return x;
  }
  // int& get() const {return x; } 这句话不被允许编译,因为可能会改变x的值
  int& get() {
    std::cout << "y" << std::endl;
    return y;
  }
};

void f(my_class cls) { cls.get(); }
void f2(const my_class cls) { cls.get(); }

int main() {
  my_class cls;

  f(cls);
  f2(cls);
}

重载带来的代码翻倍该如何处理?

大多数情况下,我们不会写上面的代码,那个太蠢了,没人会这样做,通常const版与普通版函数得到的结果是相同的。仅仅多了一个const标记,如果我们对这样相同功能的函数写两份一样的代码,是很不值得的。我们可以这样处理。

#### include <iostream>
using namespace std;

class my_class {
  int x = 1, y = 2;

 public:
  const int& get() const {
    std::cout << "const" << std::endl;
    return x;
  }
  // int& get() const {return x; } 这句话不被允许编译,因为可能会改变x的值

  int& get() {
    std::cout << "normal" << std::endl;
    return const_cast<int&>(
        (static_cast<const my_class&>(*this)).get()
      );
  }
};

void f(my_class cls) { cls.get(); }
void f2(const my_class cls) { cls.get(); }

int main() {
  my_class cls;

  f(cls);
  f2(cls);
}

c++基础笔记5 - 对象初始化

对象在使用以前一定要初始化

基本数据类型这里就不说了,直接讲类 类的对象的初始化往往使用了构造函数,但是很多人不会写构造函数,他们这样实现

#### include <iostream>
using namespace std;

class node {
  int x;

 public:
  node() {}
  node(int x_) { x = x_; }
};

class my_class {
  node a, b, c, d;

 public:
  my_class(node a_, node b_, node c_, node d_) {
    a = a_;
    b = b_;
    c = c_;
    d = d_;
  }
};
int main() {}```

 这样实现没有问题,但是效率较低,c++标准保证类的构造函数调用之前初始化先调用成员的构造函数。这样以来,my_class里面的abcd都被先初始化再赋值了,通常我们使用冒号来构造他们。
​```cpp
#### include <iostream>
using namespace std;

class node {
  int x;

 public:
  node() {}
  node(int x_) : x(x_) {}
};

class my_class {
  node a, b, c, d;

 public:
  my_class(node a_, node b_, node c_, node d_) : a(a_), b(b_), c(c_), d(d_) {}
};

int main() {}```
##### 小细节
 c++标准规定了这里的构造顺序是与声明顺序为序的,而不是冒号后面的顺序。

#### 不同编译单元的非局部静态变量顺序问题
 先看代码,这是一个.h
​```cpp
#### include <iostream>
using namespace std;

class my_class {};
extern my_class mls;

注意到有一个extern my_class mls;如果我们有多个编译单元,每个都extern一些对象,这些对象初始化的顺序,c++没有规定,所以可能导致他们随机的初始化,但是如果这些对象之间有要求有顺序,怎么办?你乱序初始化可能会出错的。这时候我们可以使用单例模式来保证正确的顺序。

#### include <iostream>
using namespace std;

class my_class {
 public:
  my_class& singleton() {
    static my_class mls;
    return mls;
  }
};
// extern my_class mls;

结语

不要乱写类的构造函数,少写非局部静态变量。

c++基础笔记6 - 带引用成员变量的类

编译器默默作出的贡献

在我们写类的时候,我们可以不写构造函数、拷贝构造函数、赋值操作、析构函数,编译器就为我们作出这一切。

带引用成员变量的类

我们考虑这样一个类,他有一个成员变量是一个引用类型。

#### include <iostream>
using namespace std;

class my_class {
  int& a;
};

int main() { my_class m; }

这个类会报错。因为你缺少对a的初始化,现在有两种选择,第一种方案是用一个变量给他赋值

#### include <iostream>
using namespace std;

int hello = 0;
class my_class {
  int& a = hello;
};

int main() { my_class m; }

或者使用构造函数来给他赋值

#### include <iostream>
using namespace std;

class my_class {
  int& a;

 public:
  my_class(int& a) : a(a) {}
};

int main() {
  int x = 1, y = 2;
  my_class m1(x);
  my_class m2(y);
  // m1=m2;
}

另一方面,这里的m1=m2,这个赋值操作又不被允许了,原因是c++中没有让一个引用变成另一个引用这样的操作,所以我们必须自己实现赋值函数。

c++基础笔记7 - 制构造函数或者赋值函数

不让你拷贝

在应用中我们可能会碰到不允许使用拷贝这样的操作,我们实现这个约束有两种方案。第一是声明这个函数,然后不实现他。这样的话能够实现这功能,但是报错的时候编译器不会报错

#### include <iostream>
using namespace std;

class my_class {
 public:
  my_class() {}
  my_class(const my_class& rhs);
};

int main() { 
  my_class m;
  my_class m2(m);
}

然后链接器重锤出击。

Undefined symbols for architecture x86_64:
  "my_class::my_class(my_class const&)", referenced from:
      _main in cc9GRPax.o
ld: symbol(s) not found for architecture x86_64
collect2: error: ld returned 1 exit status

我也觉得这样有点坑爹。 正确的做法应该是将这些不希望被使用的函数显示定义为私有函数。这样的话在编译期就会被发现,然后报错。

#### include <iostream>
using namespace std;

class my_class {
  my_class(const my_class& rhs) {}

 public:
  my_class() {}
};

int main() {
  my_class m;
  my_class m2(m);
}

c++基础笔记8 - virtual函数

virtual函数

没有什么可说的,他就是为一个类添加了一个成员变量,每当你调用virtual函数的时候,会变成调用一个新的函数,在这个函数里面有一个局部的函数指针数组,根据编译器添加成员变量来决定接下来调用哪一个函数。于是就实现了多态。

无故添加virtual的后果

如果你对一个不需要virtual的类添加了virtual函数,那么这个类的大小将扩大32位,如果你这个类本身就只有64位大小,那么他将因为你无故添加的virtual增大50%的体积。

c++基础笔记9 - operator=()的陷阱

operator=

定义赋值函数难吗?难,真的特别难,如果你能看出下面的代码中赋值函数的问题,那你就懂为什么难了。

#### include <iostream>
using namespace std;

class my_class {
  int *p;

 public:
  my_class &operator=(const my_class &rhs) {
    delete p;
    p = new int(*rhs.p);
    return*this;
  }
};

int main() {}

这里的问题其实很明显,这个赋值不支持自我赋值。解决方案可以说在最前面特判掉自我赋值,或者是先拷贝最后再delete,又或者是用拷贝构造函数拷贝一份,然后swap来实现。

c++基础笔记10 - 智能指针与引用计数型智能指针

智能指针与引用计数型智能指针

这里指的分别是auto_ptr<T> 和shared_ptr<T>

智能指针

智能指针是一个模版类,他以一个类作为模版,当智能指针被析构的时候,他会去调用他保存的对象的析构函数。这样就达到了自动析构的效果,但是如果将一个智能指针赋值给另外一个智能指针的时候,如果不做处理就可能会导致智能指针指向的区域被多次析构函数,于是智能指针的解决方案是赋值对象会被设置为null。

引用计数型智能指针

引用计数型智能指针采取了引用计数的方案来解决上诉问题,当引用数为0的时候才对指向的空间进行析构。

c++基础笔记11 - 智能指针不经意间的内存泄漏

代码少压行,要考虑后果

#### include <iostream>
#### include <memory>
using namespace std;

int f() { return 1; }
int g(auto_ptr<int> p, int x) { return 1; }

int main() { g(auto_ptr<int>(new int(2)), f()); }

上诉代码不会发生内存泄漏,但是若f函数会抛出异常,则可能发生。 c++并没有规定上诉代码的执行顺序,我们不知道f函数什么时候被调用,若它发生在了new int(2)之后,auto_ptr构造前,那就凉凉了。new 了个int,没有传给auto_ptr,这里就泄漏了。

c++基础笔记12 - 不要返回引用

引用

为了防止拷贝构造函数导致的额外开销,我们往往把函数的参数设为const &,我也曾一直想如果返回值也是const &,会不会更快

#### include <iostream>
#### include <vector>
using namespace std;

vector<int>& f(int n) { 
  vector<int> res(100,0);
  res[0]=n;
  return res;
}

int main() {
  vector<int> a = f(10);
  a[0] = 1;
}

显然是错误的做法。你怎么可以想返回一个局部变量。 然后是一个看似正确的做法。我们返回一个static内部变量。

#### include <iostream>
#### include <vector>
using namespace std;

vector<int>& f(int n) { 
  static vector<int> res(100,0);
  res[0]=n;
  return res;
}

int main() {
  vector<int> a = f(10);
  a[0] = 1;
}

在大多数情况下这确实是正确的做法。然而下面这个操作,

int main() { cout << (f(0) == f(1)); }

我不想解释为什么输出是1 反正就是尽量少用这种引用就行了,单例模式除外。不用你去想着怎么优化这里,编译器会帮我们做。

c++基础笔记13 - 全特化与偏特化

全特化和偏特化

这两个东西是针对模版而言的,比方说你定义了一个模版类,但是想对其中的某一个特殊的类做一些优化,这时候就需要这两个东西了。 STL的vector<bool>就是这样一个东西,他重新为这个类写了一套代码。语法啥的不重要看看就行,我做了一些测试,记住优先级为 全特化>偏特化>普通模版

#### include <iostream>
#### include <vector>
using namespace std;

// 模版
template <class T, class S>
class node {
 public:
  void print() { puts("模版"); }
};
// 偏特化
template <class S>
class node<int, S> {
 public:
  void print() { puts("偏特化"); }
};
// 全特化
template <>
class node<int, int> {
 public:
  void print() { puts("全特化"); }
};

// 函数模版
template <class T, class S>
void f(T a, S b) {
  puts("函数模版");
};
// 偏特化
template <class S>
void f(int a, S b) {
  puts("函数偏特化");
};
// 全特化
template <>
void f(int a, int b) {
  puts("函数全特化");
};

int main() {
  node<double, double> n1;
  node<int, double> n2;
  node<int, int> n3;
  n1.print();
  n2.print();
  n3.print();
  f(1.0,1.0);
  f(1,1.0);
  f(1,1);
}

这个程序的输出是

模版
偏特化
全特化
函数模版
函数偏特化
函数全特化

c++基础笔记14 - 降低编译依存关系

降低编译依存关系的两种方法

很多大型c++项目如果编译的依存关系太复杂,则很有可能稍微修改一行代码就导致整个项目重新编译,这是很不友好的。

第一种方法是使用handle class
#### pragma once

namespace data_structure {

template <class T>
class handle {
 private:
  T* ptr;      // 句柄指向的指针
  int* count;  // 句柄引用计数器
 public:
  //构造函数
  handle(T* ptr) : ptr(ptr), count(new int(1)) {}
  // 拷贝构造函数
  handle(const handle<T>& rhs) : ptr(rhs.ptr), count(&++*rhs.count) {}
  //赋值函数
  const handle<T>& operator=(const handle<T>& rhs) {
    if (--*rhs.count == 0) delete ptr, count;
    ptr = rhs.ptr;
    count = &++*rhs.count;
    return *this;
  }

  ~handle() {
    if (--*count == 0) delete ptr, count;
  }

  T& operator*() { return *ptr; }
  T* operator->() { return ptr; }
  const T& operator*() const { return *ptr; }
  const T* operator->() const { return ptr; }
};

}  // namespace data_structure

这就是一个简单的handle类,当然这个类并不能降低依存关系,因为他是一个模版类,所有的模版类都不能够被分离编译。但我们可以对专用的类构造一个专用的handle,即可实现分离编译。

第二种方法是使用interface class

这里不提供代码了,简单说就是使用基类制造存虚函数作为接口,实现多态。

c++基础笔记15 - 分离模版类中的模版无关函数

让模版继承一个模版基类

如果你有一个矩阵模版,模版中包含了行数和列数,而里面有一个类似于矩阵求逆的操作,虽然他与行列有关,但是因为这个函数非常的长,另一方面又有客户定义了许多矩阵,11的、22的、23的、32的等等,然后你的代码就会开始膨胀,这非常不友好,我们最好的做法是,定义一个基类,让基类传入行列参数去实现这些代码。这样我们的矩阵模版就不必将求逆这种很长很长的代码放进去了,直接继承就可以。

c++基础笔记16 - 模版元编程入门

模版元编程

这种编程方式已经被证明具有图灵完备性了,即他能完成所有的计算工作。

模版元求阶乘
#### include <iostream>
using namespace std;


template <int n>
struct node {
  enum { value = n * node<n - 1>::value };
};
template <>
struct node<0> {
  enum { value = 1 };
};

int main(){
  cout<<node<10>::value<<endl;
}
模版元筛素数
#### include <iostream>
using namespace std;

// 使用dp
// dp[n][i] = 1 表示对于x in [2,i] , n%x!=0
// 否则dp[n][i] = 0
// 于是dp[n][n-1] = 1的时候,n为素数
template <int n, int i>
struct is_prime {
  enum { value = (n % i) && is_prime<n, i - 1>::value };
};

template <int n>
struct is_prime<n, 1> {
  enum { value = 1 };
};

int main() {
  printf("%d %d\n", 2, is_prime<2, 2 - 1>::value);
  printf("%d %d\n", 3, is_prime<3, 3 - 1>::value);
  printf("%d %d\n", 4, is_prime<4, 4 - 1>::value);
  printf("%d %d\n", 5, is_prime<5, 5 - 1>::value);
  printf("%d %d\n", 6, is_prime<6, 6 - 1>::value);
  printf("%d %d\n", 7, is_prime<7, 7 - 1>::value);
}
gcd和lcm

有兴趣的读者可以去实现这两个东西,这里我就不提供代码了。

c++基础笔记17 - policies设计

policies设计

这个设计目前对我而言,还有点深,先留个坑 假设某个对象有大量的功能需求,这时候大多数人选择的设计方案是:设计一个全功能型接口。这样做会导致接口过于庞大已经难以维护。 正确的做法是将功能正交分解,用多个类来维护这些接口,达到功能类高内聚,功能类间低耦合,然后使用多重继承来实现,并允许用户自己配置,这样的做法有一个很困难的地方,就是基类没有足够的信息知道派生类的类型。于是我们通过模版套娃,让派生类作为基类的模版参数。 &esp; 代码如下,笔者太菜,不敢自己写,不敢修改。

#### include <iostream>
#### include <tr1/memory>

using std::cin;
using std::cout;
using std::endl;
using std::tr1::shared_ptr;

template <class T>
class CreatorNew {
 public:
  CreatorNew() { cout << "Create CreatorNew Obj ! " << endl; }

  ~CreatorNew() { cout << "Destroy CreatorNew Obj ! " << endl; }

  shared_ptr<T> CreateObj() {
    cout << "Create with new operator !" << endl;
    return shared_ptr<T>(new T());
  }
};

template <class T>
class CreatorStatic {
 public:
  CreatorStatic() { cout << "Create CreatorStatic Obj ! " << endl; }

  ~CreatorStatic() { cout << "Destroy CreatorStatic Obj ! " << endl; }

  T& CreateObj() {
    cout << "Create with static obj !" << endl;

    static T _t;

    return _t;
  }
};

template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<WidgetManager<CreationPolicy> > {
 public:
  WidgetManager() { cout << "Create WidgetManager Obj !" << endl; }

  ~WidgetManager() { cout << "Destroy WidgetManager Obj !" << endl; }
};

int main(int argc, char** argv) {
  cout << "------------- Create WidgetManager Object ! ------------" << endl;

  WidgetManager<CreatorNew> a_wid;

  WidgetManager<CreatorStatic> b_wid;

  cout << endl
       << "-- Create WidgetManager Object With CreateObj Method (New) ! --"
       << endl;

  a_wid.CreateObj();

  cout << endl
       << "-- Create WidgetManager Object With CreateObj Method (Static) ! --"
       << endl;

  b_wid.CreateObj();

  cout << endl
       << "------------ Destroy WidgetManager Object ! ------------" << endl;

  return 0;
}

policies class 的析构函数

先说结论,不要使用public继承,上诉代码是错误的,第二policies类不要使用虚析构函数,并且为虚构函数设为protect。

policy 组合

当我们在设计一个智能指针的时候,我们能够想到有两个方向:是否支持多线程,是否进行指针检查,这两个功能是正交的,这就实现了policy的组装

定制指针

当我们设计智能指针的时候,我们不一定必须是传统指针,我们可以抽象指针为迭代器,缺省设置为一个既包含指针又包含引用的类。

留个坑

c++基础笔记18 - 静态断言检查器

我们来实现一个静态断言检查器

最前面给了一个基于构造长度为0的数组的断言检查,我的编译器似乎很强大,允许我这样操作了。。。。我们就忽略他吧 现在考虑到模版,我们定义一个bool型的模版,对其中的true型偏特化进行实现,false型不实现,当我们用这个类构造的时候,true会被编译通过,但是false就不行了, 第二种情况是,利用构造函数,似乎还是编译器原因,我的都能编译通过,我们也忽略吧。 第三种情况,我们考虑用宏把msg替换成一个字符串,这样就OK了,报错的时候还能看到是啥错,你只要输入msg就可以。

namespace program_check {

// 第一种静态检查方法
template <bool>
struct CompiledTimeError;

template <>
struct CompiledTimeError<true> {};

// 第二种静态检查的方法
template <bool>
struct CompiledTimeCheck {
CompiledTimeCheck(...){};
};

template <>
struct CompiledTimeCheck<false> {};

}  // namespace program_check

// 第一代静态检查器
#### define STATIC_CHECK_1(expr) program_check::CompiledTimeError<(expr) != 0>()
// 第二代静态检查器,还能输出错误类型
//#define STATIC_CHECK_2(expr, msg)                                        \
{                                                                    \
class ERROR_##msg {};                                              \
(void)sizeof(                                                      \
program_check::CompiledTimeCheck<(expr) != 0>(ERROR_##msg())); \
}

// 我觉得都不太好,不如试试这个
#### define STATIC_CHECK(expr,msg) \
(program_check::CompiledTimeError<(expr) != 0>(), "msg")

int main(int argc, char** argv) {
STATIC_CHECK(false,abssf );
}

c++基础笔记19 - int2type

int2type

int2type是一种技术,他把int映射为一个类型,从而能够让他对函数去实现重载,下面的程序就是一个很好的例子,注意我们的主函数里面用的是int2type<2>如果把2换成1,是无法编译的,因为int没有clone这个函数。 如果我们不使用这种技术,而是在运行期使用if else来判断,这不可能,你无法通过编译,这事只能在编译器做。

namespace trick {
template <int v>
struct int2type {
  enum { value = v };
};
}  // namespace trick
using namespace trick;

template <class T>
class node {
  T *p;

 public:
  void f(T x, int2type<1>) { p->clone(); }

  void f(T x, int2type<2>) {}

  void f(T x, int2type<3>) {}
};
int main() {
  node<int> a;
  a.f(1, int2type<2>());
}

c++基础笔记20 - type2type

type2type

这种技术类似与int2type,他用来解决函数不能偏特化的问题,当然现在的编译器似乎已经支持这个功能了。

template <class T>
struct type2type {
  typedef T orignal_type;
};

有了这个代码,我们能模拟出偏特化,甚至函数返回值的重载,而且这个类型不占任何空间。

c++基础笔记21 - 类型选择器

类型选择器

在泛型编程中,我们常常会碰到类型选择的问题,若一个类型配置有选择为是否多态,则我们可能需要通过这个bool的值来判断下一步是定义一个指针还是定义一个引用,这时候我们的类型选择器登场了

namespace trick {
template <bool c, class T, class S>
struct type_chose {
  typedef T type;
};
template <class T, class S>
struct type_chose<false, T, S> {
  typedef S type;
};
}  // namespace trick

type_choose<false,int*,int&>::type就是int&, type_choose<true,int*,int&>::type就是int*,

c++基础笔记22 - 锁

互斥锁与共享锁

#### include <bits/stdc++.h>

#### include <mutex>
#### include <shared_mutex>
#### include <thread>
using namespace std;

void f(int id, int* _x, shared_mutex* _m) {
  int& x = *_x;
  shared_mutex& m = *_m;
  if (id & 1) {
    for (int i = 0; i < 3000; i++) {
      unique_lock<shared_mutex> lock(m);
      x++;
    }
  } else {
    for (int i = 0; i < 3000; i++) {
      shared_lock<shared_mutex> lock(m);
      int read = x;
      assert(x == read);
    }
  }
}

int main() {
  int x;
  shared_mutex m;
  thread a[10];
  for (int i = 0; i < 10; i++) a[i] = thread(f, i, &x, &m);
  for (int i = 0; i < 10; i++) a[i].join();
  cout << x << endl;
}

递归锁

#### include <bits/stdc++.h>

#### include <mutex>
#### include <shared_mutex>
#### include <thread>
using namespace std;

mutex m1;
recursive_mutex m2;

void f(int i){
  //unique_lock<mutex> lock(m1);
  unique_lock<recursive_mutex> lock(m2);
  if(i==0) return;
  else f(i-1);
}

int main() {
  f(10);
}

超时锁,用于一定时间内获取锁,超时递归锁,同理

STL

STL源码分析1 - 空间适配器

从这开始我们进入《STL源码分析》的学习

STL分为6大组件: 空间配置器、容器、迭代器、算法、仿函数、配接器

空间配置器

STL的空间适配器事STL的基础,我们不能靠操作系统来为我们管理内存,那样的代价太大了,这不划算,作为一个c/c++开发人员,我们要完全控制我们程序的一切。

allocator

这是他的英文名字,我们的allocator定义了四个操作

  • alloc::allocate() 内存配置
  • alloc::dellocate() 内存释放
  • alloc::construct() 构造对象
  • alloc::destroy() 析构对象

type_traits

一个模版元技术,他是一个类,能够萃取类型的相关信息,模版元详见C++笔记中的Boost源码分析

destroy

对于基本数据类型,我们啥都不用干,对于用户定义的数据类型,我们显示调用析构函数即可,这个使用模版特化即可。

construct

就是new,但是不用申请空间了,因为allocate已经干了

一级配置器、二级配置器

一级配置大空间(>128bytes)就是malloc和free,二级配置小空间,利用内存池。

一级配置器

直接new的,new失败以后调用out of memery的处理方式调用处理例程,让其释放内存,不断尝试,释放的时候直接free

二级配置器

维护16个链表,每个链表维护一种类型的内存,分别为8bytes、16bytes、24bytes、一直到128bytes。更加巧妙的地方是将维护的内存和链表的指针使用联合体组装。这就不浪费空间了。当需要配置内存的时候,向8字节对齐,然后再分配,当释放内存的时候,丢到链表里面就行了 当链表空了的时候,从内存池中取出20个新的内存块填充链表。 内存池是一个大块大内存,当内存池空了以后,申请更多的内存,保证每次都比上一次申请的多就行了,要是让我实现,我才不这样做,我要用计算机网络中的自适应rtt原理来做。

STL源码分析2-迭代器

迭代器

说白了就是个指针,但是他比指针更强大,更灵活。

迭代器类型

  • input iterator 只读
  • output iterator 只写
  • forward iterator 单向迭代器
  • bidirectional iterator 双向移动一个单位
  • random access iterator 双向移动多个单位
graph TB
1((input)) --> 3((forward))
2((output)) --> 3((forward))
3((forward)) --> 4((bi))
4((bi)) --> 5((random))
Loading
类型

首先为了实现的容易,得设计iterator_category为迭代器自己的类型,value_type为迭代器维护的具体数据的类型,diference_type为两个迭代器之间的距离的类型,pointer为原生指针,reference为原生引用。

STL源码分析3-序列式容器

vector

不讲了,太简单了

vector 的迭代器

服了,居然就是指针,我一直以为他封装了一下,这也太懒了。

list

算了这都跳过算了,没啥意思,

deque

用分段连续来制造整体连续的假象。 两个迭代器维护首尾,一个二维指针维护一个二维数组,感觉很low,每一行被称为一个缓冲区,但是列的话,他前后都预留了一些指针位置。 当我们随机访问的时候,就可以根据每一行的长度来选择正确的缓冲区了。

deque的迭代器

这个就厉害一些了,他包含了4个地方,当前指针、当前缓冲区首尾指针,中控器上面当前缓冲区的指针。

代码我感觉也一般般,我写也这样

queue和stack

居然是deque实现的,明明有更好的实现方法,再见,看都不想看

heap

算法都是这样写的

priority heap

vector实现的,

slist

我还是不适合这个东西

STL源码分析4-关联式容器

关联式容器

这玩意就是红黑树啦,上一章的序列容器看的我难受死了,希望这个能爽一些

红黑树

翻了好几页,都是红黑树,并没有让我感到很吃惊的代码

set和map

set就是直接红黑树,map就把用pair分别存kv,然后自己定一个仿函数,难怪map找到的都是pair

multi

算了自己写过平衡树的都知道,和非multi没几行代码不一样。

hashtable

下一章下一章。。。

STL源码分析5-算法

算法

分为质变算法和非质变算法,一个会改变操作对象,另一个不会。

accumulate

这个强,accmulate(first,last,init),将[first,last)的值累加到init上 accmulate(first,last,init,binary op),将[first,last)从左到右二元操作(init,*)到init上

adjacent_difference

666666666,adjacent_difference(first,last,result)差分都来了[first,last)差分到[result,*) 6666666,自己定义的差分adjacent_difference(first,last,result,binary_op); 这个能自定定义减法, 注意可以result设为first

inner_product

内积,inner_product(first1,last1,first2,init),加到init上然后返回。 参数在加上一个binary_op1和binary_op2,init=binary_op1(init,binary_op2(eme1,eme2))

太强了,佩服的五体投地,明天继续学,看java去

STL源码分析6-算法2

partial_sum

和前面的差分一样,partial_sum 为前缀和,partial_sum(first,last,result)为前缀和输出到result中 当然你也可以定义binary_op操作,加在最后面

power

快速幂算法了解一下,power(x,n)x的n次方,n为整数,要求满足乘法结合律。 power(x,n,op),这个同理

itoa

&esmp; itoa(first,last,value); while(first!=last) *first++=value++;

equal

equal(first1,last1,first2) 判断[first1,last1) 和[first2,...)是否相同 同样支持二元仿函数。

fill

fill(first,last,value) 把区间的值设为value

fill_n

fill(first,n,value) 把first开始的n个元素设为value

iter_swap

iter_swap(a,b) 交换迭代器的内容,这里就有意思了,如何获取迭代器内容的类型呢?还记得之前讲迭代器的时候,在迭代器内部定义的value_type吗?对!就是那个。

lexicographical_compare

lexicographical_compare(first1,last1,first2,last2) 字典序比大小,需要支持小于号

max min

这俩也支持仿函数

mismatch

mismatch(first1,last1,first2) 用第一个去匹配第二个,你需要保证第二个不必第一个短,返回匹配尾部 支持仿函数==

swap

就是很普通的交换,

copy(first,last,result)

特化了char*和wchar_t*为memmove,特化了T*和const T*,通过萃取,若指向的数据为基本数据类型则调用memmove,否则再分为随机迭代器和非随机迭代器,随机迭代器使用last-first这个值来控制,非随机迭代器用if(last==frist)来控制。

copy_backward

和上面一样,但是为逆序拷贝

set_union

set_union(first1,last1,first2,last2,result) 就是遍历两个有序容器,然后赋值到result中,注意到它在碰到相同元素的时候两个迭代器都自增了,导致若第一个中有3个1,第二个中有5个1,则输出的时候只有5个1

set_intersection

同上 交集,得到3个1

set_difference

&esmp; 代码越来越平庸了,这个是S1-S2,出现在S1中不出现在S2中

set_symmetric_difference

对称差,平庸的代码

adjacent_find(first,last)

找到第一个相等的相邻元素,允许自定义仿函数

count(first,last,value)

对value出现对次数计数 count_if(first,last,op) 对op(*it)为真计数

越看越无聊了

find(first,last,value)

找value出现的位置,这个函数应该挺有用的 find_if(first,last,op) 同上啦

find_end 和find_first_of

这个函数没用,我为啥不用kmp算法

for_each(first,last,op)

op(*i)

geterate(first,last,op)

*i=op() generate_n 同上

transform(first,last,result,op)

*result=op(*first)

transform(first1,last1,first2,last2,result,op)

*result=op(*first1,*first2)

includes(first1,last1,first2,last2)

保证有序,然后判断2是不是1的子序列,显然On

max_element(first,last)

区间最大值

min_element(first,last)

同上

merge(first1,last1,first2,last2,result)

归并排序的时候应该用得到吧

partition(first,last,pred)

pred(*)为真在前,为假在后On

remove(first,last,value)

把value移到尾部 remove_copu(first,last,result,value),非质变算法

remove_if remove_copy_if

同上

replace(first,last,old_value,new_value)

看名字就知道怎么实现的 replace_copy,replace_if,replace_copy_if

revese

秀得我头皮发麻,这个。。。。。。。

while(true)
  if(first==last||first==--last) return;
  else iter_swap(first++,last);

随机迭代器的版本还好

while(first<last) iter_swap(first++,--last);

reverse_copy ,常见的代码

rotate

这个代码有点数学,大概率用不到,一般情况下我们写splay都用3次reverse来实现的,复杂度也不过On,他这个代码就一步到位了,使用了gcd,没啥意思,STL果然效率第一

search

子序列首次出现的位置,

search_n

好偏,算了,没啥用的代码

swap_ranges(first1,last1,first2)

区间交换,swap的增强版

unique

移除重复元素 unique_copy

STL源码分析7-算法3

这边的算法应该爽一些了

lower_bound upper_bound binary_search

不多说了,就是二分,

next_permutation

一直想不明白这个函数怎么实现的,今天来看看,既然是下一个排列,显然是需要找到一个刚好比现在这个大大排列,简单分析......6532,如果一个后缀都是递减的,显然这个后缀不能更大了,如果一个后缀都不能变得更大,就必须调整更前的,所以我们要找到这样的非降序....16532,把最后一个放到1的位置,其他的从小到大排列好就行了。也即swap(1,2),reverse(6531)

prev_permutation

同理

random_shuffle

洗牌算法,从first往last遍历,每次从最后面随机选一个放到当前位置即可。

partial_sort

partial_sort(first,middle,last) 保证[first,middle)有序且均小于[middle,last)直接对后面元素使用堆上浮,这样保证了小的元素均在[first,middle)中,然后使用sort_heap????? &ems; 为啥第一步不用线性时间选择,第二步不用快排?

sort

大一就听说了这个的大名,现在来学习学习

Median_of_three

__median(a,b,c) 返回中间那个值

Partitionining

这个大家都会写,就是按照枢轴,把小的放左边,大的放到右边

threshold

当只有很少很少的几个元素的时候,插入排序更快。

final insertion sort

我们不必在快速排序中进行插入排序,但是可以提前推出,保证序列基本有序,然后再对整体使用插入排序

SGI sort

先快速排序到基本有序,然后插入排序

快速排序

先排序右边,后左边,且将左边当作下一层,当迭代深度恶化的时候,即超过了lg(n)*2的时候,采取堆排序 枢轴的选择,首、尾、中间的中位数

RW sort

这个就少了堆排序了,其他的和SGI一样

equal_range

lower_bound和upper_bound的合体 比直接lower_bound+upper_bound应该快一些,大概这样,二分中值偏小,做缩左断点,偏大则缩右端点,若二分中值等于value,则对左边lower_bound,对右边upper_bound,然后就能直接返回了

inplace_merge

将两个相邻的有序序列合并为有序序列,他在分析卡空间的做法,再见。。。不缺空间,

nth_element

线性时间选择,三点中值,递归变迭代,长度很小以后直接插入排序,666666

mergesort

借助inplace_merge直接完成了,

总结

STL的算法还是很厉害的。

STL源码分析8-仿函数

仿函数

c++的一大特色,通过重载()来实现像函数一样的功能

一元仿函数

template<class Arg,class Result>
struct unary_function{
  typedef Arg argument_type;
  typedef Result result_type;
};

看到上面那玩意没,你得继承它。

  • negeta 取反,返回-x
  • logical_not !x
  • identity x
  • select1st a.first
  • select2nd a.second

二元仿函数

template<class Arg1,class Arg2,class Result>
struct unary_function{
  typedef Arg1 first_argument_type;
  typedef Arg2 second_argument_type;
  typedef Result result_type;
};
  • plus a+b
  • minus a-b
  • multiplies a*b
  • divides a/b
  • modulus a%b
  • equal_to a==b
  • not_equal_to a!=b
  • greater a>b
  • greater_equal a>=b
  • less a<b
  • less_equal a<=b
  • logical_and a&&b
  • logical_or a||b
  • project1st a
  • project2nd b

仿函数单位元

你要为你的仿函数设置一个identity_element单位元,用于快速幂

STL源码分析9-配接器

配接器

本质上,配接器是一种设计模式,改变仿函数的接口,成为仿函数配接器,改变容器接口,称为容器配接器,改变迭代器接口,称为迭代器配接器

容器配接器

queue和stack就是修饰了deque的配接器

迭代器配接器

迭代器的配接器有3个,insert itertors,reverse iterators,iostream iterators. 哇塞这东西有点深,明天再看。

DataStrcuture

线段树

### define ml ((l+r)>>1)
### define mr (ml+1)
const int maxn=3e5+5;
int a[maxn];
int ls[maxn*2],rs[maxn*2],tot;// 树结构
int cov[maxn*2];// 懒惰标记结构
ll sum[maxn*2];int mi[maxn*2],mx[maxn*2];// 区间结构

inline void modify(int&u,int l,int r,int cov_){// 这个函数要注意重写
    if(cov_!=-1){// 这行要注意重写
        cov[u]=cov_;// 这行要注意重写
        sum[u]=1ll*cov_*(r-l+1);// 这行要注意重写
        mi[u]=mx[u]=cov_;// 这行要注意重写
    }
}

inline void push_down(int u,int l,int r){
    modify(ls[u],l,ml,cov[u]);// 这行要注意重写
    modify(rs[u],mr,r,cov[u]);// 这行要注意重写
    cov[u]=-1;// 这行要注意重写
}

inline void pushup(int u,int l,int r){
    mi[u]=min(mi[ls[u]],mi[rs[u]]);// 这行要注意重写
    mx[u]=max(mx[ls[u]],mx[rs[u]]);// 这行要注意重写
    sum[u]=sum[ls[u]]+sum[rs[u]];// 这行要注意重写
}

void updatecov(int u,int l,int r,int ql,int qr,int d){//
    if(ql<=l&&r<=qr){// 不要改写为 if(mi[u]==mx[u]) 即使想写也要这样 if(ql<=l&&r<=qr&&mi[u]==mx[u])
        modify(u,l,r,d);// 这行要注意重写
        return;
    }
    push_down(u,l,r);
    if(ml>=ql) updatecov(ls[u],l,ml,ql,qr,d);
    if(mr<=qr) updatecov(rs[u],mr,r,ql,qr,d);
    pushup(u,l,r);
}

void updatephi(int u,int l,int r,int ql,int qr){
    if(ql<=l&&r<=qr&&mi[u]==mx[u]){// 这行要注意重写
        modify(u,l,r,math::phi[mi[u]]);// 这行要注意重写
        return;
    }
    push_down(u,l,r);
    if(ml>=ql) updatephi(ls[u],l,ml,ql,qr);
    if(mr<=qr) updatephi(rs[u],mr,r,ql,qr);
    pushup(u,l,r);
}

ll query(int u,int l,int r,int ql,int qr){
    if(ql<=l&&r<=qr) return sum[u];// 这行要注意重写
    push_down(u,l,r);
    ll ret=0;// 这行要注意重写
    if(ml>=ql) ret+=query(ls[u],l,ml,ql,qr);// 这行要注意重写
    if(mr<=qr) ret+=query(rs[u],mr,r,ql,qr);// 这行要注意重写
    return ret;
}

void build(int&u,int l,int r){
    u=++tot;
    cov[u]=-1;
    if(l==r) sum[u]=mi[u]=mx[u]=a[l];
    else{
        build(ls[u],l,ml);
        build(rs[u],mr,r);
        pushup(u,l,r);
    }
}

splay

//初始化时要初始化tot和stk[0]
const int N=3e5+3;
int c[N][2],f[N],stk[N],nie=N-1,tot;//树结构,几乎不用初始化
int nu[N],w[N],cov[N];//值和懒惰标记结构,一定要赋初值,
int sz[N],mx[N],mi[N]; long long s[N];//区间结构,不用赋予初值,

inline void pushfrom(int u,int son){// assert(son!=nie)
    sz[u]+=sz[son],mx[u]=max(mx[u],mx[son]),mi[u]=min(mi[u],mi[son]),s[u]+=s[son];
}
inline void pushup(int u){// assert(u!=nie)
    sz[u]=nu[u],mi[u]=mx[u]=w[u],s[u]=1ll*w[u]*nu[u];
    if(c[u][0]!=nie) pushfrom(u,c[u][0]);
    if(c[u][1]!=nie) pushfrom(u,c[u][1]);
}
inline void modify(int u,int _cov){// assert(u!=nie)
    if(_cov!=-1) {
        w[u]=mx[u]=mi[u]=_cov;
        s[u]=1ll*sz[u]*_cov;
    }
}
inline void pushdown(int u){
    if(u==nie||cov[u]==-1) return;
    if(c[u][0]!=nie) modify(c[u][0],cov[u]);
    if(c[u][1]!=nie) modify(c[u][1],cov[u]);
    cov[u]=-1;
}
inline void rotate(int x){// rotate后x的区间值是错误的,需要pushup(x)
    int y=f[x],z=f[y],xis=c[y][1]==x,yis=c[z][1]==y;
    f[x]=z,f[y]=x,f[c[x][xis^1]]=y;//father
    c[z][yis]=x,c[y][xis]=c[x][xis^1],c[x][xis^1]=y;//son
    pushup(y);
}
inline void splay(int x,int aim){//由于rotate后x的区间值不对,所以splay后x的区间值依旧不对,需要pushup(x)
    while(f[x]!=aim){
        int y=f[x],z=f[y];
        if(z!=aim) (c[y][0]==x)^(c[z][0]==y)?rotate(x):rotate(y);// 同一个儿子先旋转y
        rotate(x);
    }
}
void del(int u){// del compress newnode decompress 是一套独立的函数,可以直接删除,也可以与上面的代码共存
    stk[++stk[0]]=u;
    if(c[u][0]!=nie) del(c[u][0]);
    if(c[u][1]!=nie) del(c[u][1]);
}
inline void compress(int u){ // 压缩区间,将节点丢进栈 assert(u!=nie)
    if(c[u][0]!=nie) del(c[u][0]);
    if(c[u][1]!=nie) del(c[u][1]);
    c[u][0]=c[u][1]=nie,nu[u]=sz[u];
}
inline int newnode(int father,int val,int siz){//
    int u=stk[0]==0?(++tot):stk[stk[0]--];
    f[u]=father,c[u][0]=c[u][1]=nie; //树结构
    w[u]=val,nu[u]=siz,cov[u]=-1; //值和懒惰标记结构,
    sz[u]=siz,mi[u]=mx[u]=val,s[u]=1ll*val*siz;//区间结构
    return u;
}
inline void decompress(int x,int u){// 解压区间并提取第x个值 assert(u!=nie)
    int ls=c[u][0],rs=c[u][1];
    if(x>1) c[u][0]=newnode(u,w[u],x-1),c[c[u][0]][0]=ls;
    if(x<nu[u]) c[u][1]=newnode(u,w[u],nu[u]-x),c[c[u][1]][1]=rs;
    nu[u]=1;
}
inline int id(int x,int u=c[nie][0]){ // 查询排名为x的数的节点下标 n个数 [1,n]
    while(true){
        pushdown(u);
        if(sz[c[u][0]]>=x) u=c[u][0];
        else if(sz[c[u][0]]+nu[u]<x) x-=sz[c[u][0]]+nu[u],u=c[u][1];
        else{
            if(nu[u]!=1) decompress(x,u);
            return u;
        }
    }
}
int build(int father,int l,int r){// 把区间l,r建树,返回根(l+r)>>1
    int u=(l+r)>>1;
    f[u]=father;
    c[u][0]=l<=u-1?build(u,l,u-1):nie;
    c[u][1]=r>=u+1?build(u,u+1,r):nie;
    pushup(u);
    return u;
}

void updatephi(int u){
    pushdown(u);
    if(c[u][0]!=nie) updatephi(c[u][0]);
    if(c[u][1]!=nie) updatephi(c[u][1]);
    w[u]=math::phi[w[u]];
    pushup(u);
    if(nu[u]!=1&&mi[u]==mx[u]) compress(u);
}

树链剖分

const int maxn=1e5+5;
int to[maxn<<1],nex[maxn<<1],head[maxn],w[maxn],cnt;
void ini(){cnt=-1;for(int i=0;i<=n;i++) head[i]=-1;}
void add_edge(int u,int v){to[++cnt]=v;nex[cnt]=head[u];head[u]=cnt;}

int dep[maxn],dad[maxn],siz[maxn],son[maxn],chain[maxn],dfn[maxn];//
void dfs1(int u,int father){//dfs1(1,0)
    dep[u]=dep[father]+1;//ini  because dep[0]=1
    dad[u]=father, siz[u]=1, son[u]=-1;
    for(int i=head[u];~i;i=nex[i]){
        int v=to[i];
        if(v==father)continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        if(son[u]==-1||siz[son[u]]<siz[v]) son[u]=v;
    }
}
void dfs2(int u,int s,int&step){
    dfn[u]=++step;
    chain[u]=s;
    if(son[u]!=-1) dfs2(son[u],s,step);
    for(int i=head[u];~i;i=nex[i]){
        int v=to[i];
        if(v!=son[u]&&v!=dad[u]) dfs2(v,v,step);
    }
}
int query(int x,int y,int k){
    int res=0;
    while(chain[x]!=chain[y]){
        if(dep[chain[x]]<dep[chain[y]]) swap(x,y); //dep[chain[x]]>dep[chain[y]]
        res+=segtree::query(dfn[chain[x]],dfn[x],k);// [左,右,值]
        x=dad[chain[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);// dep[x]<dep[y]
    return res+segtree::query(dfn[x],dfn[y],k);// [左,右,值]
}

lct

int top,c[N][2],f[N],tim[N],sta[N],rev[N],val[N];
void ini(){
    for(int i=0;i<=n;i++)c[i][0]=c[i][1]=f[i]=rev[i]=0,tim[i]=i,val[i]=2e9;
    for(int i=n+1;i<=n+m;i++)c[i][0]=c[i][1]=f[i]=rev[i]=0,tim[i]=i,val[i]=R[i-n];
}
inline void pushup(int x){
    tim[x]=x;
    if(val[tim[c[x][0]]]<val[tim[x]]) tim[x]=tim[c[x][0]];
    if(val[tim[c[x][1]]]<val[tim[x]]) tim[x]=tim[c[x][1]];
}
inline void pushdown(int x){
    int l=c[x][0],r=c[x][1];
    if(rev[x]){
        rev[l]^=1;rev[r]^=1;rev[x]^=1;
        swap(c[x][0],c[x][1]);
    }
}
inline bool isroot(int x){return c[f[x]][0]!=x&&c[f[x]][1]!=x;}
inline void rotate(int x){
    int y=f[x],z=f[y],xis=c[y][1]==x,yis=c[z][1]==y;//
    if(!isroot(y)) c[z][yis]=x;//son
    f[x]=z;f[y]=x;f[c[x][xis^1]]=y;//father
    c[y][xis]=c[x][xis^1];c[x][xis^1]=y;//son
    pushup(y);
}
inline void splay(int x){
    top=1;sta[top]=x;//init stack
    for(int i=x;!isroot(i);i=f[i])sta[++top]=f[i];//update stack
    for(int i=top;i;i--)pushdown(sta[i]);//pushroad
    while(!isroot(x)){
        int y=f[x],z=f[y];
        if(!isroot(y)) (c[y][0]==x)^(c[z][0]==y)?rotate(y):rotate(x);
        rotate(x);
    }pushup(x);
}
inline void access(int x){for(int t=0;x;t=x,x=f[x])splay(x),c[x][1]=t,pushup(x);}
inline int treeroot(int x){access(x);splay(x);while(c[x][0])x=c[x][0];return x;}
inline void makeroot(int x){access(x);splay(x);rev[x]^=1;}// 让x变成根
inline void cut(int x,int y){makeroot(x);access(y);splay(y);f[x]=c[y][0]=0;pushup(y);}
inline void link(int x,int y){makeroot(x);f[x]=y;}

珂朵莉树

珂朵莉树

珂朵莉树是一颗树,我们用集合来维护,c++中集合是红黑树,所以我们借助此集合来完成珂朵莉树。
我们将区间分段,那么各段有唯一的左端点,我们将左端点放入集合,当我们遍历集合的时候,我们就得到了我们要的序列,此时我们维护了结构,但未维护值,进一步发现我们可以使用map,用键值对来维护更多信息,键用来维护树的结构,值来维护序列的值。

split

因为我们要维护区间信息,所以我们需要操作split来提取区间,本质上提取区间为提取单点,这一点在splay中表现的很出色,当我们提取出左端点和右端点的时候,区间也就被提取出来了,如果提取位置x,在红黑树中我们二分到x的位置,若此处是一个区间[l,r],我们将此区间拆分为[l,x-1][x,r]即可。

assign

我们提取出区间,删掉这些节点然后,插入一个新的节点即可

add

我们提取出区间,暴力更新所有节点即可

sum

我们提取出区间,暴力计算所有节点,使用快速幂

kth

我们提取出区间,还是暴力

什么时候选择此数据结构

数据随机且含有区间赋值操作,此数据结构的操作可以在splay上实现,并维护更多信息,map法仅仅只是编码简单了很多。

例题

C. Willem, Chtholly and Seniorious

odt代码 {% include_code cf896c lang:cpp cpp/cf896c-珂朵莉树.cpp %}

Euler Tour Tree

Euler Tour Tree

任何一颗树都能够用欧拉旅行路径来表示,欧拉旅行路径是一个序列,他记录了一颗树的dfs的时候的顺序,记录了进入每个节点的时间和退出该节点的时间,这样以后,子树就成了ETT上连续的区间 ,当我们对子树进行交换的时候,我们就可以将这个区间平移。这里我们用splay维护即可

heap

总览

这篇博客将用于整理各种堆的数据结构代码以及复杂度证明: 二叉堆、二项堆、斐波拉契堆、配对堆、左偏树、斜堆、bordal队列、B堆

注意

全部代码单对象测试通过,部分代码未实现拷贝构造函数达到深拷贝。

heap

堆是一种非常重要的数据结构,在计算机科学中,堆一般指堆是根节点比子孙后代都要大(小)的一种数据结构。

项目地址

链接

前置条件

基本数据结构:变长数组、栈、队列、字符串的实现(此时暂未实现,使用STL代替,后面有时间会自己实现) 内存池机制 {% post_link 势能分析%}

基类设计

在这里我们暂且只设置三个接口,如果不够,我们再补充。

heap代码 {% include_code heap lang:cpp cpp/perfect/data_structure/heap.h %}

binary heap

二叉堆,就是我们常见的堆,也是大多数人常用的堆,二叉堆是一个完全二叉树,满足根节点的值大于(小于)子孙的值。我们设计他的时候,采取下标从1开始的数组来直接模拟,使用i,2i,2i+1之间的关系来完成边的构建。

push

我们将数放入数组尾部,并不断上浮即可。细节稍微推一下就出来了。每个元素最多上浮堆的高度次,复杂度$O(lgn)$

pop

我们直接删掉第一个元素,并让最后一个元素顶替他,然后下沉即可。这里个细节也是稍微推一下就出来了。每个元素最多下沉堆的高度次,复杂度$O(lgn)$

top

就是第一个元素,复杂度$O(1)$

代码如下:

binary heap代码 {% include_code binary heap lang:cpp cpp/perfect/data_structure/binary_heap.h %}

binomial heap

二项堆,是一个堆森林,其中每个堆以及各自的子堆的元素个数都是2的幂,并且森林中没有两个堆元素个数相同的堆。举个简单的例子,一个包含了6个元素的二项堆,这个堆森林中一定是两个堆组成,因为6=2+4,他不能是6=2+2+2由三个堆组成,因为森林中不允许出现元素相同的堆。

堆的具体形状

*** 图片源于wiki*** ![binomial heap](/images/binomial heap.png)

merge

二项堆天然支持合并,即可并堆,当合并的时候,我们很容易发现,只要将森林合并即可,而对于哪些出现的元素个数相同的堆,我们可以两两合并,让其中一个作为另一个的根的直接儿子即可。每次合并的时候,两两合并的复杂度是$O(1)$,最多合并的次数等于森林中元素的个数减1,而森林中堆的个数等于二进制中1的个数,这个是O(lgn)级别的,所以总体复杂度O(lgn)

push

可以看作与一个只包含了一个元素的堆合并,每次push,最坏的情况下时间复杂度为$O(lgn)$,但是多次连续的push,均摊时间复杂度就不一样了,我们来分析一下n次连续push的情况,森林中的堆两两合并的次数等于时间复杂度,定义函数$f(x)$,表示在森林中所有堆中的元素个数的总和为$x$的情况下,push一个值以后,堆中合并发生的次数,显然$f(x)=$x的二进制表示中末尾连续的1的个数,不难发现 $f(x)&gt;=1$的时候$x%2=1$, $f(x)&gt;=2$的时候$x%4=3$, $f(x)&gt;=3$的时候$x%8=7$ 这里我们通过计数原理推算出 $$ \begin{aligned} \sum_{i=1}^n{f(i)}=\lfloor\frac{x+1}{2}\rfloor+\lfloor\frac{x+1}{4}\rfloor+\lfloor\frac{x+1}{8}\rfloor+...+\lt x+1 \end{aligned} $$ 所以在大量连续的push过程中,均摊时间复杂度为O(1)

pop

先遍历各个根,找出最值,不难发现,森林中,任意一个堆去掉根以后,恰好也是一个满足条件森林,这里也可以用合并处理,时间复杂度$O(lgn)$

top

遍历所有堆即可,时间复杂度O(lgn)

程序设计

很多人说用链表实现链接,这确实是一个好方法,但是如果用单链表或循环链表或双向链表实现,则有很多局限性,下面代码中也提及了。我这里采取的是使用数组存森林,使用左儿子右兄弟的手段,将多叉树用二叉树来表示。这个方法非常棒。

代码

binary heap代码 {% include_code binary heap lang:cpp cpp/perfect/data_structure/binomial_heap.h %}

fibonacci heap

斐波拉契堆,是目前理论上最强大的堆,他和二项堆很长得很相似。和二项堆一样,斐波拉契堆也是一个堆森林,斐波拉契堆简化了几乎所有的堆操作为懒惰性操作,这极大的提升了很多操作的时间复杂度。

potential method

对于一个斐波拉契堆$H$,我们定义势能函数为$\Phi(H) = t(H) + 2m(H)$, 其中$t(H)$是斐波拉契堆$H$的森林中堆的个数,$m(H)$是斐波拉契堆中被标记的点的数量。

push

当我们向一个斐波拉契堆中添加元素的时候,我们会选择将这个元素做成一个堆,然后链入森林的根集和,常常选择链表维护根集合,同时更新斐波拉契堆中最小值的指针,实际时间复杂度显然是$O(1)$,势能变化为1,因为堆的个数变大了1,均摊复杂度为$O(1)+1=O(1)$

merge

当我们合并两个斐波拉契堆的时候,我们是懒惰操作,直接将这两个堆森林的根集合并为一个根集,常常选择链表来维护根集合,同时更新新的最小值指针,实际实际复杂度为$O(1)$,势能无变化,均摊复杂度为$O(1)$

top

$O(1)$

decrease

当我们想要减小一个节点的值堆时候,我们直接将他和父亲断开,然后将他链入森林并减小值,然后标记父亲,如果父亲被标记过一次,则将父亲和爷爷也断开并链入森林,并清除标记,一直递归下去,这里我们不要太认真,加上这条路上一个有$c$个,则我们一共断开了c次,实际复杂度为$O(c)$,势能的第一项变为了$t(H)+c$,第二项变为了$2(m(H)-c)$,于是势能的变化为$c-2c=-c$,于是均摊复杂度为$O(c)-c$,这里本来并不等于$O(1)$,但是我们可以增大势的单位到和这里的$O(c)$同级,这样就得到了$O(1)$

erase

当我们想要删除一个节点的时候,先将其设为无穷小,然后在调用pop

pop

前面偷了很多懒,导致除了erase以外,其他操作的均摊复杂度均为$O(1)$,这里就要好好地操作了,我们是这样来操作的,删掉最小值以后,将他的儿子都链入森林,这里花费了$O(D(H))$的实际代价,这里的$D(H)$指的是斐波拉契堆$H$中堆的最大度数。然后我们更新top的时候,不得不遍历所有的根,这时候我们就顺便调整一下堆。我们遍历所有的根,依次对森林中所有的堆进行合并,直到没有任意两个堆的度数相同,假设最后我们得到了数据结构$H'$,那么这个过程是$O(t(H)-t(H'))$的,于是时间复杂度为$O(t(H)-t(H'))+O(D(H))$,然后我们观察堆的势能变化,显然第一项的变化量为$t(H')-t(H)$,第二项无变化,即势能总变化为$t(H')-t(H)$,则均摊复杂度为$O(t(H)-t(H'))+O(D(H))+(t(H')-t(H))$,这里依然不一定等于$O(D(H))$,但是我们依然可以增大势的单位到能够和$O(t(H)-t(H'))$抵消,最终,均摊复杂度成了$O(D(H))$

D(H)

现在我们进入最高潮的地方。我们考虑斐波拉契堆中一个度数为k的堆,若不考虑丢失儿子这种情况发生,我们对他的儿子按照儿子的度数进行排序,显然第i个儿子的度数为i-1,$i=1,2,3...k$,此时考虑儿子们会丢掉自己的儿子,则有第i个儿子的度数$\ge i-2$,在考虑他自己也会丢失儿子,但这不会影响到第i个儿子的度数$\ge i-2$这个结论。

斐波拉契数列

$$ \begin{aligned} F_i = \left{ \begin{aligned} &0&i=0\\ &1&i=1\\ &F_{i-2}+F_{i-1]}&i\ge 2\\ \end{aligned} \right. \end{aligned} $$

斐波拉契数列的两个结论

$F_{n+2}=1+\sum_{i=1}^nF_i$ $F_{n+2}\ge \phi^n,\phi^2=\phi+1,\phi大约取1.618$

比斐波拉契数更大

容易用数学归纳法证明对于一个度数为k的堆,他里面的节点的个数$\ge F_{k+2}$,这里$F_{i}$是斐波拉契数列的第i项。 当k=0的时候,节点数数目为1,大于等于1 当k=1的时候,节点数数目至少为2,大于等于2 若$k\le k_0$的时候成立, 则当$k=k_0+1$的时候,节点数目至少为$1+F_1+F_2+...+F_{k_0+1}=F_{k_0+3}=F_{k+2}$

比黄金分割值的幂更大

现在我们就能够得到一个结果了,一个度数为k的堆,他的节点个数至少为$\Phi^k$,这里我们很容易就能得到这样一个结果,$D(H)\le \log_\Phi 最大的堆内元素的个数$

结尾

至此我们已经全部证明完成,读者也应该知道为什么斐波拉契堆要叫这个名字。

fibonacci heap 代码

fibonacci heap代码 {% include_code fibonacci heap lang:cpp cpp/perfect/data_structure/fibonacci_heap.h %}

pairing heap

配对堆,名字来源于其中的一个匹配操作。很有趣,他的定义就是一个普通多叉堆,但使用特殊的删除方式来避免复杂度退化。是继Michael L. Fredman和Robert E.Tarjan发明斐波拉契堆以后,由于该数据结构实现难度高以及不如理论上那么有效率,Fredma、Sedgewick、Sleator和Tarjan一起发明的。

potential method

我们有一个配对堆$H$,其中有n节点$node$,$node_i$有$d_i$个儿子, 则有 $F(H) = \sum F(node)$, $F(node_i)=1-min(d_i,\sqrt{n})$ 复杂度证明方面,等我把论文看完再来整这个,感觉证明比斐波拉契堆更复杂。

merge

合并的时候,让次大堆做最大堆的儿子,显然时间复杂度$O(1)$

push

插入的时候,看作与只包含了一个元素的堆合并,所以$O(1)$

top

就是根 $O(1)$

pop

当我们删除根以后,会形成一个堆森林,这时我们从左到右,每两个连续的堆配对合并一次,然后从右到左依次合并。比方说有这样一个情况AABBCCDDEEFFGGH,我们先将其从左到右合并AA->A,BB->B...得到ABCDEFG,->ABCDEH -> ABCDI -> ABCJ -> ABK -> AL -> M

程序设计

同样的左儿子右兄弟

代码

pairing heap代码 {% include_code pairing heap lang:cpp cpp/perfect/data_structure/pairing_heap.h %}

leftist heap

左偏树、左式堆、左翼堆是一个堆,除此以外,他定义了距离,没有右子节点的节点的距离为0,其他节点的距离为右子节点的距离加1,在这个定义下,左偏树的左偏体现着每个节点的左子节点的距离不小于右子节点的距离。

push

为新节点建立堆,然后与堆合并 $O(lgn)$

pop

删除根节点,合并左右子树 $O(lgn)$

top

根节点 $O(1)$

merge

$O(lgn)$, 当我们合并两个堆$H1,H2$的时候,我们只需要比较这两个堆顶的大小,不妨设H1小,并设H3、H4为H1的左右儿子,则我们可以这样来看待,我们将H3,H4从H1中断开,递归合并H4和H2位H5,这时候我们还剩下H3、H5以及H1的堆顶,我们根据左偏树的定义,选择H3、H5分别作为左右或右左儿子即可,

复杂度证明

算法中每次均选择右儿子递归向下,这导致时间复杂度与右儿子的右儿子的右儿子的...有关,这里不难发现递归的次数就是节点的距离。根据左距离不小于右距离,我们很容易就能得到这个距离是$O(lgn)$级别的。

leftist heap 代码

leftist heap代码 {% include_code leftist heap lang:cpp cpp/perfect/data_structure/leftist_heap.h %}

skew heap

我们的左偏树不记录距离,并且每次递归的时候无条件交换左右儿子,则成了斜堆。

复杂度证明

potential method

定义斜堆中的右子树距离比左子树大的节点为重节点,否则为轻节点。 定义势能函数为重节点的个数。

merge

当我们合并两个斜堆$H_1,H_2$的时候,不妨设他们的右子节点链中的轻重儿子为$l_1,h_1,l_2,h_2$,则时间时间复杂度为$O(l_1+h_1+l_2+h_2)$,经过交换以后,链上的重节点一定会变成轻节点,轻节点可能会变为重节点,我们取最坏的情况,即轻节点全部变为重节点,这时势能的变化量为$l_1+l_2-h_1-h_2$,最后我们的均摊复杂度为$O(l_1+h_1+l_2+h_2)+l_1+l_2-h_1-h_2$,我们依然可以增大势的单位,直到足以抵消所有的h,最终均摊复杂度为$O(l_1+l_2)$,这里不难证明,一条右儿子构成的链上,轻节点的个数是对数级别。

skew heap代码

skew heap代码 {% include_code skew heap lang:cpp cpp/perfect/data_structure/skew_heap.h %}

bordal heap

这里已经涉及到一些冷门的东西了。暂时先放一下

B heap

是一种和B树一样利用内存页的东西。冷门,先放一下

wiki上还有数不清的堆,学到这里暂停一下

search_tree

总览

这篇博客将用于整理各种搜索树的数据结构,目前已经整理了BST、AVL、BTree、B+Tree、B*Tree、23Tree、234Tree、TTree、RBTree、LLRBTree、AATree、SplayTree、Treap、无旋Treap、scapegoatTree,VPTree、cartesianTree,

项目地址

链接

前置条件

基本数据结构:变长数组、栈、队列、字符串的实现(此时暂未实现,使用STL代替,后面有时间会自己实现) 内存池机制

树的设计

我们设计一个基类让所有的树来继承此基类,然后在看后面会有什么改变,以后再来更新

基类

我们的基类只提供接口,不提供数据类型

tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/tree.h %}

更新: 搜索树的设计

由于笔者能力有限,设计欠佳,导致后面的空间树、字典树等数据结构无法加入tree中,所以我们在tree的后面加一层search_tree来表示搜索树。

搜索树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/search_tree.h %}

B+ Tree

和B树一样,B+树也具有相同的性质。

不同点

B+树的内部节点、根节点只保存了键的索引,一般情况下保存的是一个键指向的子树的所有键的集合中最大的那个,即所有左边子树的max,唯一的键保存在叶子节点上, 叶子节点按照链表有序连接,这导致了B+树能够用链表来遍历整棵树。

23tree

参见3阶Btree

234tree

参见4阶Btree

T tree

T tree 是一颗二叉树,他和avl tree有着一定的联系,总所周知,avl树为一颗二叉树,利用其中序维护信息,利用子树高度维护平衡。我们借此修改一下,我们尝试让avl树的每个节点维护多个信息[信息序列],于是T tree就出现了。T tree是一颗二叉树,每个节点维护一个有序序列,用T 树的中序遍历方式,将其节点维护的序列依次相连即成为了我们维护的信息。

T tree 解释

为了便于编码,我们不考虑序列中会出现相同的元素,可以证明,对于泛型编程方式而言,这并不影响该数据结构的功能,该数据结构依旧具备维护相同元素的能力

T tree结论

非叶节点维护的序列都充满了各自的容器

T tree树上信息

每一颗子树都要维护一个序列,对于每个节点,我们都维护一个稍微小一点的序列,比该序列中元素更小的元素放入左子树,否则放入右子树。

T tree搜索

搜索的话,就是普通二叉树的搜索,比当前节点维护的最小值小,就在左子树找,比当前节点维护的最大值大,就在右子树找,否则就在当前节点找

T tree插入

当我们插入一个数的时候,我们首先递归向下,找到插入的节点位置,若该节点中储存的序列未满,则置入该节点,否则,有两种处理方式,第一种是从该节点中取出最小值,放入左子树,然后把带插入的树放入该节点,第二种是放入右子树,这里不多说明。插入可能会导致树失去平衡,我们用avl树单旋的方式来让树重新平衡

T tree删除

当我们删除一个数的时候,像avl树一样处理,若该数在叶子上,简单删掉并维护树的平衡即可,让该数在非叶节点时,我们取出前驱或后继来顶替即可。

T tree一个容易出错的地方

笔者在编码的时候,遇到了一个问题,就是有时候会出现非叶节点维护的数据并未充满容器,这种情况发生的原因是单旋造成的。在单旋的时候,将叶子结点旋转成非叶节点后,我们应该调整数据,让非叶节点重新维护的数据充满容器

T treecode

TT代码 {% include_code tree lang:cpp cpp/perfect/data_structure/T_tree.h %}

red black tree

red black tree定义

红黑树是一种平衡树,他满足下面的性质

1.节点是红色或黑色。 2.根是黑色。 3.所有叶子都是黑色(叶子是NIL节点)。 4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。) 5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

red black tree解读性质

红黑树的性质难以理解,这是因为他太过于抽象了, 如果你了解B Tree, 我们现在考虑节点中最多包含3个键的B Tree,他又叫2-3-4tree,意思是任何一个节点都有2,3或4个直接子孙,直接子孙指的是和当前节点相邻的子孙,相邻指的是恰好有一条边连接。 2-3-4树的编码是比较复杂的,原因在于节点种类过多。我们现在考虑这样一种情况,RB tree中的红色节点代表他和他父亲在一起,即他+他的父亲构成了2key3son-node,若他的兄弟也是红色,则他+兄弟+父亲构成了3key4son-node 性质1显然 性质2的原因是根没有父亲,所以他不能为红 性质3的原因是为了保证更具有一般性 性质4的原因是保证最多只有3key4son-node,不能出现4key5son-node 性质5的原因是B树的完全平衡性质

red black tree编码

由此可见,我们仿照234Tree即BTree即可完成编码

为什么红黑树跑得快

我们发现234树的所有操作都能在红黑树上表现,但是234树有一个很大的缺陷,即分裂合并的速度太慢了,要重构很多东西,细心的读者自己模拟会发现,这个过程在RBTree上对应的仅仅是染色问题,这极大的加速了数据结构,这是优势。

red black tree erase

删除是比较复杂的,你怎样操作都可以,只要旋转次数少,你可以分很多类来讨论,显然分类越多,平均旋转次数是最少的。正常情况下,erase会引进一个重黑色的概念,这个概念的实际意义指的是该节点有一个0key1son的黑色父亲被隐藏了。

red black tree code

red black tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/red_black_tree.h %}

left leaning red black tree

left leaning red black tree定义

在红黑树的基础上,左倾红黑树保证了3节点(2key-3son-node)的红色节点为向左倾斜,这导致了红黑树更加严格的定义,

left leaning red black tree实现

在红黑树代码的基础上,我们定义一个left leaning函数,用来调整右倾斜为左倾斜,这个函数需要适当的加入到红黑树代码当中,笔者调试了很久,找到了很多思维漏洞,把这些漏洞全部用数学的方式严格证明以后,调用left leaning函数即可。

left leaning red black tree优点

相比红黑树而言,笔者认为提升不大,真的,但是有人使用了很少的代码就实现了LLRBT,这也算一个吧,笔者是修改的红黑树,所以很难受,代码更长了。

left leaning red black tree code

left leaning red black tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/left_leaning_red_black_tree.h %}

AA Tree

AA树真的很棒,虽然他没有普通红黑树那么厉害,但是AA树挺容易实现的,AA树是一棵右倾红黑树23树,注意! 这里是23树,不是234树。

AA树的由来

Arne Andersson教授在论文Balanced search trees made simple中提到,红黑树有7种特殊情况(图片源于wiki) 为了改进,他提出了使用23树并强行要求3节点(2key-3son-node)向右倾斜,于是,我们只剩下两种情况(图片源于wiki) 为了更加容易编码,他提出不再使用红黑来标识节点,而是选择高度,这里的高度指的是黑高度,即黑色节点的高度,学习过左偏树(左翼堆)或斜堆的读者应该对这里不太陌生,这里的高度其实和左偏树或斜堆中的右距离是同一个东西。

AA树的特性

所有叶节点的level都是1 每个左孩子的level恰好为其父亲的level减一 每个右孩子的level等于其父亲的level或为其父亲的level减一 每个右孙子的level严格小于其祖父节点的level 每一个level大于1的节点有两个子节点

AA树的skew

skew 是一个辅助函数,他的本质是zig,即如果发现一个节点的左儿子与自己黑高相同,则将左儿子选择至根。这将保证右倾。

AA树中的split

split同样是一个辅助函数,他的本质是zag,即如果发现一个节点的右孙子与自己黑高相同,则将右儿子选择至根,并将黑高+1,这将保证不会出现4节点(3key-4son-node)

AA树中的insert

递归向下,找到插入位置,然后插入,最后调整,调整的时候,树会变高,对每一层递归而言,左儿子变高我们就先让其skew,这可能导致出现4节点,我们再split,对于右儿子变高的情况,这时候可能右儿子本身是一个3节点,当他变高,导致根成为了4节点,我们调用skew即可,全部统一一下,就是先skew后split

AA树中的erase

很多时候删除都是一件困难的事情,但是我们可以通过寻找前驱后继,可以保证删除的节点一定是叶子,对于删除叶子,可能树高下降,同样的,先删除后对每一层进行调整。我们前面说过,AA树只有两种结构。我们来分析一下树高下降产生的影响。

情况1

右儿子与自己同黑高

情况1.1

右儿子下降 这种情况是合法的,不需要调整

情况1.2

左儿子下降 我们观察到这里是一种较为复杂的情况,可以这样处理,让节点a和c同时黑下降,得到了 然后我们考虑到c节点的左右儿子,注意到c和a以前黑同高,所以c的右儿子cr,一定比c矮,当c下降以后,cl、c、cr同高 根据定义,这里最多还能拖出两个同黑高的,cl的右儿子clr,cr的右儿子crr 这时候我们对c执行skew,然后clr成了c的左儿子,我们再次对c执行skew,最终a-cl-clr-c-cr-crr同黑高, 接下来的一步是让我最吃惊的,非常漂亮,我们先对a进行split,然后对根的右儿子再次split,就结束了。对a进行split后我们得到,注意到这里根的高度提高了 对根对右儿子split,就结束了

情况2

右儿子与自己不同黑高

情况2.1

右儿子下降 让a节点高度降低 让a进行skew,最后因为b的右儿子高度,分两种情况 对于b的右儿子太高的时候,对a进行skew 然后对b进行split即可

情况2.2

左儿子下降 让a下降 这里可能发生c的右儿子与c同高,split(a)即可

AA树erase总结

至此我们的删除已经讨论完了,实际细分只有4种情况,这要比普通红黑树简单多了,

AA树缺点

多次旋转导致性能不及红黑树,旋转次数较多

AA树代码

AA树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/aa_tree.h %}

splay tree

伸展树,以其操作splay出名。 伸展树的本质就是bst,

splay操作

伸展树对splay操作的参数是一个节点,他的结果是将这个节点通过双旋变成根。

splay insert

伸展树insert的时候,先按照bst的操作insert,然后将insert的点进行splay操作即可

splay search

伸展树search的时候,先按照bst的操作search,对找到的节点进行splay即可

splay erase

伸展树erase的时候,先search,这样我们要删除的节点就成为了根,然后按照bst的操作删除即可

splay操作详解

重新定义旋转rotate

rotate(x)即交换x和x的父亲的位置,即如果x是父亲y的左儿子,则rotate(x)等价与zig(y),反之则等价于zag(y)

定义splay

如果祖父-父亲-自己构成一条直链,则选rotate父亲再rotate自己,若不是直链则rotate自己两次。知道自己成为根。

splay复杂度分析

splay势能函数

对于一个伸展树T,他的一个节点x的子树大小为$s(x)$,定义一个节点x的势能为$X=log_2(s(x))$

对数函数是一个凸函数

已知a,b>0,则$lg(a)+lg(b)\lt 2lg(\frac{a+b}{2}) = 2lg(a+b)-2$

对于一条直链,我们要先rotate父亲,再rotate自己

设自己为x,父亲为y,祖父为z, 则势能变化为 $$ \begin{aligned} &X'+Y'+Z'-X-Y-Z \\&=Y'+Z'-X-Y\lt X'+Z'-2X \\&=(3X'-3X)+(X+Z'-2X') \end{aligned} $$ 这里的x和z‘的子树大小加起来刚好等于x'的子树大小-1。所以势能变化小于$3(X'-X)-2$ ##### 对于一条非直链,我们要rotate自己两次,才能上去,rotate父亲不行的

同理,势能变化为 $$ \begin{aligned} &X'+Y'+Z'-X-Y-Z \\&=Y'+Z'-X-Y\lt Y'+Z'-2X \\&=(2X'-2X)+(Y'+Z'-2X') \end{aligned} $$ 这里的y'和z'的子树大小加起来刚好等于x‘的子树大小-1,所以势能变化小于$2(X'-X)-2$ ##### 单旋 易证势能变化小于$X'-X$ ##### 整理合并 三种操作的均摊复杂度分别为$O(1)+X'-X$,$O(1)+2(X'-X)-2$,$O(1)+3(X'-X)-2$,对于后面的两种情况,我们增大势的单位来支配隐藏在O(1)中的常数,最终分别为$O(1)+X'-X$,$2(X'-X)$,$3(X'-X)$,再次放缩: $O(1)+3(X'-X)$,$3(X'-X)$,$3(X'-X)$,最后对于所有的旋转求和,因为只有一次单旋所以最终我们得到了均摊复杂度为$O(1)+X'-X\lt O(1)+X'$,显然X'是一个很小的数,他恰好等于伸展树中的元素的个数取对数后的结果。至此所有的操作均取决于splay的复杂度,均为$lg$级别。 #### splay代码
splay树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/splay_tree.h %}

Treap

树堆Treap来源于Tree+Heap的组合, 其实就是一棵树,他的节点储存了两个键,一个是我们维护的信息,另外一个是随机数,我们不妨设前者叫key,后者叫rand_key,Treap的key满足搜索树的性质,Treap的rand_key满足堆的性质。(从某种意义上而言,笛卡尔树是key=rand_key的Treap) 特点: 若key与rand_key确定后,Treap的形态唯一, Treap在大多数情况下显然是平衡的,但我不会证明,也没找到证明,暂时先放一下。

Treap insert

我们向一棵Treap中按照搜索树的性质插入值以后,不会破坏搜索树的特点,但是大概率导致Heap的性质被违反。考虑到单旋不会导致搜索树的性质被破坏,我们通过单旋来从新让Treap满足Heap的性质。考虑回溯,假设我们对某个子树插入了一个值,若最终插入到左子树,则可能导致左子树树根的rand_key比当前节点的rand_key大,同时因为我们只插入了一个节点,所以最多也只有一个节点的rand_key比当前节点的rand_key大,这时候如果使用zig,则树恢复平衡。

Treap erase

还是使用平衡树的操作来对Treap进行删除。如果过程中用到了前驱后继替换的技巧,这将导致替换节点的rand_key和他所处在为位置不匹配,我们就只考虑这颗子树,因为只有这颗子树的树根出现了问题,我们尝试递归向下,将位置不匹配这个现象下移,因为不匹配,必然是这个节点的rand_key比儿子们小,这时候如果左儿子的rand_key大就zig,否则zag,最后能发现这问题在向叶子结点转移,我们能够递归向下,直到最后转移到叶子上,树就恢复平衡了。

Treap 代码

Treap代码 {% include_code tree lang:cpp cpp/perfect/data_structure/treap.h %}

无旋Treap

无旋treap,指的是不使用zig和zag来重新恢复平衡的Treap 我们使用merge和split

无旋Treap merge

merge的参数是两个treap,他返回treap合并后的结果,不妨设其中一个为T1,另一个为T2,这里还要求T1的最大key小于等于T2的最小key。merge其实很简单,如果你学过左偏树的话,会很容易理解。我们不妨设T1的根的rand_key比T2的小。那么很显然,最终结果的根为T2的根,这里我们就可以递归了,我们将T2的左子树与T1合并出T3,最后让T3最为T2新的左子树,我们得到的T2就是merge的结果。

无旋Treap split

split的参数是一个Treap和一个值W,他返回两颗Treap,其中一个的最大key小于W,另一个大于W(不需要考虑等于的情况),这个过程依然很简单,我们考虑根就可以了,如果根的key大于w,则根和右子树分到一遍,然后递归左儿子,将得到的两个Treap中key大的那个作为之前分到一边的根的左儿子即可。

无旋Treap insert

先split,然后merge两次

无旋Treap erase

很多人这里使用了split两次然后merge三次,我认为这个不太好,常数过大,我们可以这样做,先search找到要删的点,然后merge其左右子树顶替自己即可。

无旋Treap代码

无旋Treap代码 {% include_code tree lang:cpp cpp/perfect/data_structure/no_rotate_treap.h %}

scapegoat Tree

替罪羊树,他是一个暴力的bst,与普通bst相比,他记录了子树的大小,用参数alpha来定义平衡,即左右子树的大小都不允许超过根的alpha倍,所以往往aplha是一个0.5到1的数字,当违反了这个性质,就暴力重构,将树构造为完全平衡树。

替罪羊树erase

为节点打上标记scapegoat,代表这个节点已经被删除了,回溯子树大小信息。

替罪羊树insert

使用bst插入的方式来插入,注意特判掉那些被打删除标记的点,就可以了

替罪羊树重构

当我们erase或者insert以后,受影响的节点应该恰好构成了一条从根到目标的链,我们使用maintain来重新调整子树大小的时候,注意标记那些非法(不平衡)的节点,然后当我们maintain到根的时候,我们重构离根最近的不平衡的子树。

替罪羊树代码

替罪羊树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/scapegoat_tree.h %}

vantate point tree

vp tree 是一颗二叉树,他和kd tree有着一定的相似度,

树上信息

每一颗子树都要维护一个点集,对于每个节点,我们都维护一个距离d,然后将到该节点的距离小于d的点放到左儿子,其他的放到右儿子中。

vantate point

vantate point的选取是一个比较麻烦的事情,我们仔细想想都知道,这个点的选取肯定会影响算法,有一种处理办法是随机选取,这显然不是我们想要的。我们其实可以这样来处理,

Our algorithm constructs a set of vantage point candidates by random sampling,and then evaluates each of them.Evaluation is accomplished by extracting another sample,from which the median of $\prod_p(S)$,and a corresponding moment are estimated.Finally,based on these statistical images,the candidate with the largest moment is chosen.

这里的$\prod_p(S)$指的就是在该度量空间中点p和点s的距离,作者选取的statistical images是方差,我们可以从伪码中看出。

建树

和kd树一样,建树的过程是一致的,我们选出vantate point,然后递归左右建树

搜索

搜索的话,也是一样的,用结果剪枝即可

修改

这样的树不存在单旋这种方式,我们只能用替罪羊树套vantate point tree来实现

参考资料

Data Structures and Algorithms for Nearest Neighbor Search in General Metric Spaces Peter N.Yianilos*

cartesian tree

笛卡尔树是一颗二叉树,他满足中序遍历为维护的序列,且满足堆的性质

build

我们使用单调栈来维护树根到叶子的链,在单调栈的构建中完成树的构建

ct代码 {% include_code tree lang:cpp cpp/perfect/data_structure/cartesian_tree.h %}

BST

binary search tree

BST是二叉搜索树,满足中序遍历是一个有序的序列,他是最最基础的二叉树,他不一定平衡,

BST insert

插入的时候,在树上递归插入,比当前节点大就向右边走,否则向左走

BST search

查找的时候,同上

BST erase

删除的时候,相对复杂,如果只有一个儿子,很简单,但是当他有两个儿子的时候,我们可以选择将一个儿子顶替自己,另外一个儿子去找前驱或后继即可。

BST code

我们使用内存池来维护整个数据结构

BST代码 {% include_code tree lang:cpp cpp/perfect/data_structure/binary_search_tree.h %}

AVL

AVL Tree

AVL Tree使用高度差作为平衡因子,他要求兄弟的高度差的绝对值不超过1

code

avl Tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/avl_tree.h %}

BTree

B Tree

B树是一颗多叉树,和二叉树比较类似,但是每个节点有多个子节点和多个键,通常我们称最多拥有N个子节点的B树为N阶B树,B树为了保证树的有序,树上节点的子节点数量恰好比键的数量多1,这就保证了存在一种方式将子节点和键排在一起,且每个键的左边、右边都是子节点,这样形成的序列即为维护的序列。

B Tree 约束

B树的节点分三类,根节点、叶子节点、内部节点(除了根节点和叶子节点以外的节点) 所有的叶子节点的深度相同,这一点保证树的平衡 根节点的键的的数量在区间[2,N-1]上 , N>=3 每个内部节点、叶子节点的键的数量在区间$[\lceil\frac{N}{2}\rceil-1,N-1]$上 每个节点的键的个数恰好比子节点个数多1

B Tree insert

B树的插入很简单,找到应该插入的叶子节点,然后插入。这会可能导致树不符合约束->叶子节点上键的数量过多,此时叶子结点上的键的数量为N,这时候我们分裂叶子节点为两个叶节点,从中取出中位数置入父节点作为划分这两个叶子节点的键。我们很容易证明$\lfloor\frac{N-1}{2}\rfloor\ge\lceil\frac{N}{2}\rceil-1$,若父节点依旧超出约束范围,同理向上继续对内部节点分裂,直道碰到根节点,若根节点依旧键的个数过多,则继续分裂,然后创建新的根节点将分裂出的节点连接。

B Tree erase

B树的删除同普通平衡树一样,若删除点出现在内部节点或根节点中,我们取出他的前驱或后继将他替换。然后再删除。我们将所有情况合并到了删除叶子节点上。若删除后树依旧满足约束,则不需要调整。若不满足约束,根据N>=3我们得出每个节点最少两个子节点,若删除位置的兄弟节点有较多键,我们只需要从兄弟节点移动一个键过来即可。若兄弟节点同样处于最少键时,我们可以合并这两个节点$2*(\lceil\frac{N}{2}\rceil-1)\le N-1$

B Tree search

直接二分向下即可。

注意

注意vector的 insert、erase后会导致的引用失效,

code

B Tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/B_tree.h %}

B+Tree

B+树优点

因为内部节点、根节点保存的是索引(指针),这导致单位储存空间储存的指针要比单位空间储存的键多得多,这同时导致了B+树能够比B树更加扁平。

代码

这东西实现难度有点高,太码农了,我随缘实现吧。哈哈哈哈哈哈。

BstartTree

B* Tree

B*树在B+树的基础上再把内部节点也整上链表,同时要求空间使用率为$\frac{2}{3}$而不是$\frac{1}{2}$

代码

随缘

23Tree

23tree

参见3阶Btree

234Tree

234tree

参见4阶Btree

TTree

T tree

T tree 是一颗二叉树,他和avl tree有着一定的联系,总所周知,avl树为一颗二叉树,利用其中序维护信息,利用子树高度维护平衡。我们借此修改一下,我们尝试让avl树的每个节点维护多个信息[信息序列],于是T tree就出现了。T tree是一颗二叉树,每个节点维护一个有序序列,用T 树的中序遍历方式,将其节点维护的序列依次相连即成为了我们维护的信息。

T tree 解释

为了便于编码,我们不考虑序列中会出现相同的元素,可以证明,对于泛型编程方式而言,这并不影响该数据结构的功能,该数据结构依旧具备维护相同元素的能力

T tree结论

非叶节点维护的序列都充满了各自的容器

T tree树上信息

每一颗子树都要维护一个序列,对于每个节点,我们都维护一个稍微小一点的序列,比该序列中元素更小的元素放入左子树,否则放入右子树。

T tree搜索

搜索的话,就是普通二叉树的搜索,比当前节点维护的最小值小,就在左子树找,比当前节点维护的最大值大,就在右子树找,否则就在当前节点找

T tree插入

当我们插入一个数的时候,我们首先递归向下,找到插入的节点位置,若该节点中储存的序列未满,则置入该节点,否则,有两种处理方式,第一种是从该节点中取出最小值,放入左子树,然后把带插入的树放入该节点,第二种是放入右子树,这里不多说明。插入可能会导致树失去平衡,我们用avl树单旋的方式来让树重新平衡

T tree删除

当我们删除一个数的时候,像avl树一样处理,若该数在叶子上,简单删掉并维护树的平衡即可,让该数在非叶节点时,我们取出前驱或后继来顶替即可。

T tree一个容易出错的地方

笔者在编码的时候,遇到了一个问题,就是有时候会出现非叶节点维护的数据并未充满容器,这种情况发生的原因是单旋造成的。在单旋的时候,将叶子结点旋转成非叶节点后,我们应该调整数据,让非叶节点重新维护的数据充满容器

T treecode

TT代码 {% include_code tree lang:cpp cpp/perfect/data_structure/T_tree.h %}

RBTree

red black tree

red black tree定义

红黑树是一种平衡树,他满足下面的性质

1.节点是红色或黑色。 2.根是黑色。 3.所有叶子都是黑色(叶子是NIL节点)。 4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。) 5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

red black tree解读性质

红黑树的性质难以理解,这是因为他太过于抽象了, 如果你了解B Tree, 我们现在考虑节点中最多包含3个键的B Tree,他又叫2-3-4tree,意思是任何一个节点都有2,3或4个直接子孙,直接子孙指的是和当前节点相邻的子孙,相邻指的是恰好有一条边连接。 2-3-4树的编码是比较复杂的,原因在于节点种类过多。我们现在考虑这样一种情况,RB tree中的红色节点代表他和他父亲在一起,即他+他的父亲构成了2key3son-node,若他的兄弟也是红色,则他+兄弟+父亲构成了3key4son-node 性质1显然 性质2的原因是根没有父亲,所以他不能为红 性质3的原因是为了保证更具有一般性 性质4的原因是保证最多只有3key4son-node,不能出现4key5son-node 性质5的原因是B树的完全平衡性质

red black tree编码

由此可见,我们仿照234Tree即BTree即可完成编码

为什么红黑树跑得快

我们发现234树的所有操作都能在红黑树上表现,但是234树有一个很大的缺陷,即分裂合并的速度太慢了,要重构很多东西,细心的读者自己模拟会发现,这个过程在RBTree上对应的仅仅是染色问题,这极大的加速了数据结构,这是优势。

red black tree erase

删除是比较复杂的,你怎样操作都可以,只要旋转次数少,你可以分很多类来讨论,显然分类越多,平均旋转次数是最少的。正常情况下,erase会引进一个重黑色的概念,这个概念的实际意义指的是该节点有一个0key1son的黑色父亲被隐藏了。

red black tree code

red black tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/red_black_tree.h %}

LLRBTree

left leaning red black tree

left leaning red black tree定义

在红黑树的基础上,左倾红黑树保证了3节点(2key-3son-node)的红色节点为向左倾斜,这导致了红黑树更加严格的定义,

left leaning red black tree实现

在红黑树代码的基础上,我们定义一个left leaning函数,用来调整右倾斜为左倾斜,这个函数需要适当的加入到红黑树代码当中,笔者调试了很久,找到了很多思维漏洞,把这些漏洞全部用数学的方式严格证明以后,调用left leaning函数即可。

left leaning red black tree优点

相比红黑树而言,笔者认为提升不大,真的,但是有人使用了很少的代码就实现了LLRBT,这也算一个吧,笔者是修改的红黑树,所以很难受,代码更长了。

left leaning red black tree code

left leaning red black tree代码 {% include_code tree lang:cpp cpp/perfect/data_structure/left_leaning_red_black_tree.h %}

AATree

AA Tree

AA树真的很棒,虽然他没有普通红黑树那么厉害,但是AA树挺容易实现的,AA树是一棵右倾红黑树23树,注意! 这里是23树,不是234树。

AA树的由来

Arne Andersson教授在论文Balanced search trees made simple中提到,红黑树有7种特殊情况(图片源于wiki) 为了改进,他提出了使用23树并强行要求3节点(2key-3son-node)向右倾斜,于是,我们只剩下两种情况(图片源于wiki) 为了更加容易编码,他提出不再使用红黑来标识节点,而是选择高度,这里的高度指的是黑高度,即黑色节点的高度,学习过左偏树(左翼堆)或斜堆的读者应该对这里不太陌生,这里的高度其实和左偏树或斜堆中的右距离是同一个东西。

AA树的特性

所有叶节点的level都是1 每个左孩子的level恰好为其父亲的level减一 每个右孩子的level等于其父亲的level或为其父亲的level减一 每个右孙子的level严格小于其祖父节点的level 每一个level大于1的节点有两个子节点

AA树的skew

skew 是一个辅助函数,他的本质是zig,即如果发现一个节点的左儿子与自己黑高相同,则将左儿子选择至根。这将保证右倾。

AA树中的split

split同样是一个辅助函数,他的本质是zag,即如果发现一个节点的右孙子与自己黑高相同,则将右儿子选择至根,并将黑高+1,这将保证不会出现4节点(3key-4son-node)

AA树中的insert

递归向下,找到插入位置,然后插入,最后调整,调整的时候,树会变高,对每一层递归而言,左儿子变高我们就先让其skew,这可能导致出现4节点,我们再split,对于右儿子变高的情况,这时候可能右儿子本身是一个3节点,当他变高,导致根成为了4节点,我们调用skew即可,全部统一一下,就是先skew后split

AA树中的erase

很多时候删除都是一件困难的事情,但是我们可以通过寻找前驱后继,可以保证删除的节点一定是叶子,对于删除叶子,可能树高下降,同样的,先删除后对每一层进行调整。我们前面说过,AA树只有两种结构。我们来分析一下树高下降产生的影响。

情况1

右儿子与自己同黑高

情况1.1

右儿子下降 这种情况是合法的,不需要调整

情况1.2

左儿子下降 我们观察到这里是一种较为复杂的情况,可以这样处理,让节点a和c同时黑下降,得到了 然后我们考虑到c节点的左右儿子,注意到c和a以前黑同高,所以c的右儿子cr,一定比c矮,当c下降以后,cl、c、cr同高 根据定义,这里最多还能拖出两个同黑高的,cl的右儿子clr,cr的右儿子crr 这时候我们对c执行skew,然后clr成了c的左儿子,我们再次对c执行skew,最终a-cl-clr-c-cr-crr同黑高, 接下来的一步是让我最吃惊的,非常漂亮,我们先对a进行split,然后对根的右儿子再次split,就结束了。对a进行split后我们得到,注意到这里根的高度提高了 对根对右儿子split,就结束了

情况2

右儿子与自己不同黑高

情况2.1

右儿子下降 让a节点高度降低 让a进行skew,最后因为b的右儿子高度,分两种情况 对于b的右儿子太高的时候,对a进行skew 然后对b进行split即可

情况2.2

左儿子下降 让a下降 这里可能发生c的右儿子与c同高,split(a)即可

AA树erase总结

至此我们的删除已经讨论完了,实际细分只有4种情况,这要比普通红黑树简单多了,

AA树缺点

多次旋转导致性能不及红黑树,旋转次数较多

AA树代码

AA树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/aa_tree.h %}

SplayTree

splay tree

伸展树,以其操作splay出名。 伸展树的本质就是bst,

splay操作

伸展树对splay操作的参数是一个节点,他的结果是将这个节点通过双旋变成根。

splay insert

伸展树insert的时候,先按照bst的操作insert,然后将insert的点进行splay操作即可

splay search

伸展树search的时候,先按照bst的操作search,对找到的节点进行splay即可

splay erase

伸展树erase的时候,先search,这样我们要删除的节点就成为了根,然后按照bst的操作删除即可

splay操作详解

重新定义旋转rotate

rotate(x)即交换x和x的父亲的位置,即如果x是父亲y的左儿子,则rotate(x)等价与zig(y),反之则等价于zag(y)

定义splay

如果祖父-父亲-自己构成一条直链,则选rotate父亲再rotate自己,若不是直链则rotate自己两次。知道自己成为根。

splay复杂度分析

splay势能函数

对于一个伸展树T,他的一个节点x的子树大小为$s(x)$,定义一个节点x的势能为$X=log_2(s(x))$

对数函数是一个凸函数

已知a,b>0,则$lg(a)+lg(b)\lt 2lg(\frac{a+b}{2}) = 2lg(a+b)-2$

对于一条直链,我们要先rotate父亲,再rotate自己

设自己为x,父亲为y,祖父为z, 则势能变化为 $$ \begin{aligned} &X'+Y'+Z'-X-Y-Z \\&=Y'+Z'-X-Y\lt X'+Z'-2X \\&=(3X'-3X)+(X+Z'-2X') \end{aligned} $$ 这里的x和z‘的子树大小加起来刚好等于x'的子树大小-1。所以势能变化小于$3(X'-X)-2$ ##### 对于一条非直链,我们要rotate自己两次,才能上去,rotate父亲不行的

同理,势能变化为 $$ \begin{aligned} &X'+Y'+Z'-X-Y-Z \\&=Y'+Z'-X-Y\lt Y'+Z'-2X \\&=(2X'-2X)+(Y'+Z'-2X') \end{aligned} $$ 这里的y'和z'的子树大小加起来刚好等于x‘的子树大小-1,所以势能变化小于$2(X'-X)-2$ ##### 单旋 易证势能变化小于$X'-X$ ##### 整理合并 三种操作的均摊复杂度分别为$O(1)+X'-X$,$O(1)+2(X'-X)-2$,$O(1)+3(X'-X)-2$,对于后面的两种情况,我们增大势的单位来支配隐藏在O(1)中的常数,最终分别为$O(1)+X'-X$,$2(X'-X)$,$3(X'-X)$,再次放缩: $O(1)+3(X'-X)$,$3(X'-X)$,$3(X'-X)$,最后对于所有的旋转求和,因为只有一次单旋所以最终我们得到了均摊复杂度为$O(1)+X'-X\lt O(1)+X'$,显然X'是一个很小的数,他恰好等于伸展树中的元素的个数取对数后的结果。至此所有的操作均取决于splay的复杂度,均为$lg$级别。 #### splay代码
splay树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/splay_tree.h %}

Treap

Treap

树堆Treap来源于Tree+Heap的组合, 其实就是一棵树,他的节点储存了两个键,一个是我们维护的信息,另外一个是随机数,我们不妨设前者叫key,后者叫rand_key,Treap的key满足搜索树的性质,Treap的rand_key满足堆的性质。(从某种意义上而言,笛卡尔树是key=rand_key的Treap) 特点: 若key与rand_key确定后,Treap的形态唯一, Treap在大多数情况下显然是平衡的,但我不会证明,也没找到证明,暂时先放一下。

Treap insert

我们向一棵Treap中按照搜索树的性质插入值以后,不会破坏搜索树的特点,但是大概率导致Heap的性质被违反。考虑到单旋不会导致搜索树的性质被破坏,我们通过单旋来从新让Treap满足Heap的性质。考虑回溯,假设我们对某个子树插入了一个值,若最终插入到左子树,则可能导致左子树树根的rand_key比当前节点的rand_key大,同时因为我们只插入了一个节点,所以最多也只有一个节点的rand_key比当前节点的rand_key大,这时候如果使用zig,则树恢复平衡。

Treap erase

还是使用平衡树的操作来对Treap进行删除。如果过程中用到了前驱后继替换的技巧,这将导致替换节点的rand_key和他所处在为位置不匹配,我们就只考虑这颗子树,因为只有这颗子树的树根出现了问题,我们尝试递归向下,将位置不匹配这个现象下移,因为不匹配,必然是这个节点的rand_key比儿子们小,这时候如果左儿子的rand_key大就zig,否则zag,最后能发现这问题在向叶子结点转移,我们能够递归向下,直到最后转移到叶子上,树就恢复平衡了。

Treap 代码

Treap代码 {% include_code tree lang:cpp cpp/perfect/data_structure/treap.h %}

无旋Treap

无旋treap,指的是不使用zig和zag来重新恢复平衡的Treap 我们使用merge和split

无旋Treap merge

merge的参数是两个treap,他返回treap合并后的结果,不妨设其中一个为T1,另一个为T2,这里还要求T1的最大key小于等于T2的最小key。merge其实很简单,如果你学过左偏树的话,会很容易理解。我们不妨设T1的根的rand_key比T2的小。那么很显然,最终结果的根为T2的根,这里我们就可以递归了,我们将T2的左子树与T1合并出T3,最后让T3最为T2新的左子树,我们得到的T2就是merge的结果。

无旋Treap split

split的参数是一个Treap和一个值W,他返回两颗Treap,其中一个的最大key小于W,另一个大于W(不需要考虑等于的情况),这个过程依然很简单,我们考虑根就可以了,如果根的key大于w,则根和右子树分到一遍,然后递归左儿子,将得到的两个Treap中key大的那个作为之前分到一边的根的左儿子即可。

无旋Treap insert

先split,然后merge两次

无旋Treap erase

很多人这里使用了split两次然后merge三次,我认为这个不太好,常数过大,我们可以这样做,先search找到要删的点,然后merge其左右子树顶替自己即可。

无旋Treap代码

无旋Treap代码 {% include_code tree lang:cpp cpp/perfect/data_structure/no_rotate_treap.h %}

无旋Treap

name
descirption
input
output
sample input
sample output
toturial
code

scapegoateTree

scapegoat Tree

替罪羊树,他是一个暴力的bst,与普通bst相比,他记录了子树的大小,用参数alpha来定义平衡,即左右子树的大小都不允许超过根的alpha倍,所以往往aplha是一个0.5到1的数字,当违反了这个性质,就暴力重构,将树构造为完全平衡树。

替罪羊树erase

为节点打上标记scapegoat,代表这个节点已经被删除了,回溯子树大小信息。

替罪羊树insert

使用bst插入的方式来插入,注意特判掉那些被打删除标记的点,就可以了

替罪羊树重构

当我们erase或者insert以后,受影响的节点应该恰好构成了一条从根到目标的链,我们使用maintain来重新调整子树大小的时候,注意标记那些非法(不平衡)的节点,然后当我们maintain到根的时候,我们重构离根最近的不平衡的子树。

替罪羊树代码

替罪羊树代码 {% include_code tree lang:cpp cpp/perfect/data_structure/scapegoat_tree.h %}

VPTree

vantate point tree

vp tree 是一颗二叉树,他和kd tree有着一定的相似度,

树上信息

每一颗子树都要维护一个点集,对于每个节点,我们都维护一个距离d,然后将到该节点的距离小于d的点放到左儿子,其他的放到右儿子中。

vantate point

vantate point的选取是一个比较麻烦的事情,我们仔细想想都知道,这个点的选取肯定会影响算法,有一种处理办法是随机选取,这显然不是我们想要的。我们其实可以这样来处理,

Our algorithm constructs a set of vantage point candidates by random sampling,and then evaluates each of them.Evaluation is accomplished by extracting another sample,from which the median of $\prod_p(S)$,and a corresponding moment are estimated.Finally,based on these statistical images,the candidate with the largest moment is chosen.

这里的$\prod_p(S)$指的就是在该度量空间中点p和点s的距离,作者选取的statistical images是方差,我们可以从伪码中看出。

建树

和kd树一样,建树的过程是一致的,我们选出vantate point,然后递归左右建树

搜索

搜索的话,也是一样的,用结果剪枝即可

修改

这样的树不存在单旋这种方式,我们只能用替罪羊树套vantate point tree来实现

参考资料

Data Structures and Algorithms for Nearest Neighbor Search in General Metric Spaces Peter N.Yianilos*

cartesianTree

cartesian tree

笛卡尔树是一颗二叉树,他满足中序遍历为维护的序列,且满足堆的性质

build

我们使用单调栈来维护树根到叶子的链,在单调栈的构建中完成树的构建

ct代码 {% include_code tree lang:cpp cpp/perfect/data_structure/cartesian_tree.h %}

trie

字典树

字典树是我接触自动机的开端,我们先讲自动机,

自动机

自动机有五个要素,开始状态,转移函数,字符集,状态集,结束状态。

自动机识别字符串

假设我们有一个自动机,他长这个样子,他能识别字符串abc. 稍等片刻!下图正在转码中

graph LR
start((start))--a--> 1((1))
1((1))--b-->2((2))
2((2))--c-->3((end))
Loading

最开始我们在位置start,也就是初始状态,当我们读入字符a的时候,经过转移函数我们到达了1号状态,如果我们在初始状态读到的是字符b,则因为初始状态没有字符b的转移函数。会导致自动机在非终结状态停机,这就意味着无法识别字符b,同理也无法识别c-z,于是在初始状态只能识别a,
然后分析状态1,只能识别b,到达状态2,只能识别c到达终态。最后就识别了字符串abc。
然后我们来考虑一个复杂一点的自动机,他能识别字符串abc、abd、bc、ac

稍等片刻!下图正在转码中

graph TB
start((start))--a--> 1((1))
1((1))--c-->10((end))
start((start))--b--> 3((3))
3((3))--c--> 10((end))
1((1))--b-->2((2))
2((2))--c-->10((end))
2((2))--d-->10((end))
Loading

如果我们不去分析其中的end节点,他的本质就是一颗树,他同时又叫做字典树,特别的,如果这个字典树的字符集为01,则他又叫做01字典树。

字典树的插入

字典树的插入应该是一个字符串,这个过程我们可以用递归实现,

字典树的删除

特别注意,为了能够支持多重集合,我们常常用一个数字来代表有多少个字符串在某个状态结束,这样删除的时候只要减少那个值就可以了

字典树的查找

递归。

递归转非递归

因为字典树的代码特别简单,我们常常直接用递归转非递归来实现。

代码

先欠着,暂时拖一个不太友好的代码在这里,这里面有一部分代码就是字典树啦。 代码链接

三叉搜索树

ternary search tree

字典树的缺点

不难想到,对于那些字符集特别大的字典树来说,他们的空间消耗特别大,因为每个节点都要储存大量的指针,而这些指针往往又是空的。

将BST与trie结合起来

考虑这样一种树,每个节点有三个儿子,左右儿子表示自己的左右兄弟,向下的儿子表示真正的儿子。这样的树,将极大的提高了空间利用率。

偷个图来放着

这里插入了as,at,be,by,he....

三叉搜索树的插入

考虑递归,假设我们插入到了某个节点,若下儿子与当前字符相等,则递归到下儿子并使用下一个字符来递归,如果当前字符小于下儿子,则递归到左儿子,保持当前字符不变,如果当前节点不存在了,则创建新节点,直接向下儿子走。

三叉搜索树的删除

我们还是用数字来记录终结节点的终结字符串有多少个,若找到待删除的节点以后,终止与此的只有一个字符串,则直接删掉,否则让终极节点的计数器减1,注意在回溯的时候,如果三个儿子都没有终结字符了,就删掉自己。

三叉搜索树的查找

递归递归。

三叉搜索树的缺点

树的平衡是一个很大的问题,这个我也没办法

三叉搜索树的本质

很多时候,很多数据结构像变戏法一样,我们从本质看带三叉搜索树,我们发现下儿子其实是字典树的边,在考虑左右儿子,其实这个就是bst,哈哈哈发现了没有, 我们考虑删掉所有下儿子,你会发现,剩下的是一个bst森林,就像lct删掉指向父亲没有指向自己的边以后,就是splay森林一样,很神奇。我们将这些bst森林转化为一个一个普通的数组,让这些数组从新充当节点,然后将下儿子连回去, 一切都清晰了,又变成普通字典树了。 所以三叉搜索树的本质是优化后的字典树,每个节点都是一个bst。即trie套bst,外层树为trie,内层树为bst。

三叉搜索树的优化?

我们让这里的bst用avl代替?用rbt代替?用sbt代替?都可以,但是我觉得这样编码太难了吧,若实在是真的差这点效率,可以去这样做,但我认为,把普通字典树的节点用avl-map、rbt-map、sbt-map直接范型编程或设为类中类不香吗。在这玩树套树确实高大上,效率也高,但编码难度也太高了。

代码

先欠着,以后再还。

X快速前缀树

X快速前缀树

我以前就说过,当你的数据结构达到了一定的基础,就可以学习那些更加高级的数据结构了,往往那些更加高级的数据结构由基本数据结构组合而成。

先提出一个问题

现在要你维护一个多重集合,支持3种操作,1询问一个值(这个值不一定在集合中)的前驱和后继,2向集合中插入一个元素,3从集合中删掉一个元素,1操作$10^6$次,2操作$10^5$次,3操作$10^5$,要求你在1S内完成回答。保证集合元素都小于M=$10^6$

普通平衡树?

我们考虑到上面三种操作都是普通平衡树常见的操作,很多人可能就直接拿起他自己的平衡树上了。很遗憾,大概率是无法通过的。因为操作次数太多了。

观察,思考

我们的操作需要的是大量的查询,大量、大量,你个普通平衡树怎么操作的过来?

新的平衡树

现在我们提出一个新的平衡树,这个平衡树非常厉害,他支持$O(lgM)$的时间来删除和插入,支持$O(lglgM)$的时间来查询前驱后继。

X快速前缀树

字典树+哈希表+维护叶子节点的双向链表+二分 首先,我们先建立一颗普通的01字典树,这个树我们对他稍作修改,考虑到字典树的节点分3类: 叶子节点、根节点、内部节点,我们让叶子节点间构造双向环状链表,其次,对于仅有左儿子的内部节点,让其右儿子指向子树的最小叶子节点,对于仅有右儿子的内部节点,让其左儿子指向子树的最大叶子节点。另一方面,我们对字典树的每一层都建立一个hash表,hash掉他们节点所代表的值是有还是没有,这样我们就构造出了一个X快速前缀树的模型了。

X快速前缀树的查找

假设我们要找一个数k,我们在树上寻找树和k的lca[最低公共祖先],这个过程可以只要在hash表中二分即可知道lca在哪一个节点,可以证明,这个节点要么没有左儿子,要么没有右儿子。如果有的话,他的儿子和k的lcp[最长公共前缀]一定更长,但是这与他自己是lca的事实相悖。另一方面,由于我们的单儿子节点储存了最值信息,如果这个节点没有右儿子,则他的右儿子指向的值是k的前驱,至于后继,在叶子节点的链表上后移一个单位即可。这个过程总复杂度在二分lca上,树的高度为lgn,二分高度,所以总体复杂度为$O(lglgn)$

X快速前缀树的插入

找出lca,若lca没有右儿子,则说明当前节点要插入到右儿子里面。照着做就行了,同时注意向下递归的时候把值也插入到hash表里面,递归到叶子的时候重新连接双向环状链表(前驱和后继),最后回溯的时候维护单儿子节点的信息,以及树根方面的值就行了。

X快速前缀树的删除

找到待删除节点,然后删掉,重新维护叶子链表,回溯的时候从hash表里面删掉自己,对于单儿子的节点也要根据子树回溯即可。

Y快速前缀树

Y快速前缀树

继X快速前缀树以后,Dan Willard又提出了X快速前缀树的改进版本

改进X快速前缀树

我们还是继续考虑n个小于M的整数(n<M),我们按照大小,从小到大分组,每组的元素的个数在区间$[\frac{lgM}{4},2lgM]$上,对每组构建一颗普通平衡树,这样我们一共会得到$\frac{n}{2lgM}$到$\frac{4n}{lgM}$颗树,我们在这些树中取一个随便的值r,r要在平衡树的最大和最小值之间,这样我们每棵树对应了一个r,把所有的r作为键,其对应的平衡树作为值放到X快速平衡树中,这就是Y快速平衡树。

Y快速前缀树的查找前驱后继

首先在上层X前缀树上查找前驱和后继,最终我们会定位到两个叶子节点,也就对应了两颗普通平衡树,我们在这两颗普通平衡树里面直接找前驱后继然后合并就结束了。总复杂度$lglg(\frac{n}{lgM})+2*lg(lgM)=lglgM$

Y快速前缀树的插入

先在X前缀树上查询前驱后继,然后在其对应的平衡树上插入真正要插入的值,总复杂度$lglg(\frac{n}{lgM})+lglgM=lglgM$,这里有可能导致插入的值太多,进行分裂,我们来看一下这次分裂前插入了多少元素,我们考虑最坏的情况,不妨设之前也是分裂的,那么他的大小为$lgM$,到这次分裂一共插入了lgM+1个元素,导致现在的大小超过了2lgM,于是这lgM次插入的均摊分裂复杂度为$\frac{lg(\frac{n}{lgM})}{lgM}\lt 1$,于是总复杂度为$lglgM$

Y快速前缀树的删除

同样的,我们还是先查询前驱后街,然后在对应的平衡树上删除真正要删除的值,总复杂度为$lglg(\frac{n}{lgM})+lglgM=lglgM$,这里有可能导致平衡树上剩下的值太少,我们考虑合并,合并后如果依然太大,比方说大于lgM,我们就再次分裂为两个平衡树即可。这里可以证明,均摊复杂度依然是O1,因为从$\frac{lgM}{2}$到$\frac{lgM}{4}$也要$\frac{lgM}{4}$次删除,均摊下来,依然是O1,为了懒惰删除,我们甚至可以不必要求合并后超过lgM猜分裂,到2lgM也行。懒惰总是优秀的。于是总体复杂度为$lglgM$

总结

至此,Y快速前缀树的所有操作均为lglgM了.

代码

欠着欠着,以后再补。

笛卡尔树

笛卡尔树

这个笛卡尔树没写出来,气死了 他是二叉树,他是堆,二叉树中序遍历的结果就是数组a

笛卡尔树的构造

先看一个简单的笛卡尔树 我们使用增量构建笛卡尔树来完成,就和增量构建后缀自动机一样容易。我们来对x分类讨论如果x比5小怎么样,如下 如果x在5和7之间 如果x在7和9之间 如果x比9大呢 我们不难发现,每当增加一个新的值的时候,笛卡尔树变化的一定只有从根一直向右走的路径,我们可以想出一个很简单的方法,每次新增加值a[i+1]的时候,让他不断和右链比较,找到lower_bound的地方,然后插入到那去就可以了。 进一步发现,上诉代码需要维护指向父亲的指针,我们考虑到用一个栈来维护右链,栈低为根,栈顶为叶子,在弹栈的时候维护右儿子指针,在压栈的时候维护左儿子指针即可。代码如下

int n;
int a[N]; // a[1], a[2], a[3], a[4] ... a[n]
int l[N],r[N]; // 0为空指针
int s[N],top; //
void build(){
  s[++top]=1;// 把起点压栈
  for(int i=2;i<=n;i++){
    //r[i]=l[i]=0;
    while(top&&a[s[top]]<=a[i]) l[i]=s[top--]; //弹栈的时候维护左儿子指针
    if(top) r[s[top]]=i; // 压栈的时候维护右儿子指针
    s[++top]=i;
  } // 返回的时候栈顶为根
}

DB

mysql刷题1

查找最晚入职员工的所有信息

查找最晚入职员工的所有信息 CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

我们排序以后选出最大的

select * from employees
    order by hire_date desc
    limit 0,1

找到最大值以后使用where

select * from employees
    where hire_date = (select max(hire_date) from employees);

查找入职员工时间排名倒数第三的员工所有信息

查找入职员工时间排名倒数第三的员工所有信息 CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

select * from employees
    order by hire_date desc
    limit 2,1

使用distinct去重

select * from employees
    where hire_date = (
        select distinct hire_date
            from employees
            order by hire_date desc
            limit 2,1
    )

查找各个部门当前(to_date='9999-01-01')领导当前薪水详情以及其对应部门编号dept_no

CREATE TABLE dept_manager ( dept_no char(4) NOT NULL, emp_no int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select s.*, d.dept_no
    from
       salaries as s  inner join  dept_manager as d
            on d.emp_no = s.emp_no
    where
         d.to_date = '9999-01-01'
         and S.to_date = '9999-01-01'

查找所有已经分配部门的员工的last_name和first_name以及dept_no

CREATE TABLE dept_emp ( emp_no int(11) NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

select e.last_name,e.first_name,d.dept_no
    from
        dept_emp as d inner join employees as e
            on d.emp_no = e.emp_no

查找所有员工的last_name和first_name以及对应部门编号dept_no,也包括展示没有分配具体部门的员工

CREATE TABLE dept_emp ( emp_no int(11) NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

select e.last_name, e.first_name, d.dept_no
    from
        employees as e left join dept_emp as d
            on d.emp_no = e.emp_no

查找所有员工入职时候的薪水情况,给出emp_no以及salary, 并按照emp_no进行逆序

CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

找出最早的那个

select distinct s.emp_no,s.salary
from salaries as s
group by s.emp_no
having min(s.from_date)
order by s.emp_no desc
select s.emp_no , s.salary
from
    employees as e
    left join salaries as s
    on e.emp_no = s.emp_no and e.hire_date = s.from_date
order by s.emp_no desc

查找薪水涨幅超过15次的员工号emp_no以及其对应的涨幅次数t

CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select emp_no, count(emp_no) as t
from salaries
group by emp_no
having t > 15

找出所有员工当前(to_date='9999-01-01')具体的薪水salary情况,对于相同的薪水只显示一次,并按照逆序显示

CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

** 记录我第一次没看题解作出的题目 **

select distinct salary
from (
    select salary
    from salaries
    where to_date = '9999-01-01'
)
order by salary desc

有个更好的写法

select salary
from salaries
where to_date='9999-01-01'
group by salary
order by salary desc

获取所有部门当前manager的当前薪水情况,给出dept_no, emp_no以及salary,当前表示to_date='9999-01-01'

CREATE TABLE dept_manager ( dept_no char(4) NOT NULL, emp_no int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select d.dept_no,s.emp_no,s.salary
from (
    dept_manager as d
    inner join salaries as s
    on d.emp_no = s.emp_no
    and d.to_date = s.to_date
    and d.to_date = '9999-01-01'
)

获取所有非manager的员工emp_no

CREATE TABLE dept_manager ( dept_no char(4) NOT NULL, emp_no int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

select emp_no
from employees
where emp_no not in(
    select emp_no from dept_manager
)

获取所有员工当前的manager,如果当前的manager是自己的话结果不显示,当前表示to_date='9999-01-01'。

结果第一列给出当前员工的emp_no,第二列给出其manager对应的manager_no。 CREATE TABLE dept_emp ( emp_no int(11) NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE dept_manager ( dept_no char(4) NOT NULL, emp_no int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no));

select de.emp_no,dm.emp_no as manager_no
from (
    dept_emp as de inner join dept_manager as dm
    on de.dept_no = dm.dept_no
)
where de.emp_no != dm.emp_no
and de.to_date = '9999-01-01'
and dm.to_date = '9999-01-01'

获取所有部门中当前员工薪水最高的相关信息,给出dept_no, emp_no以及其对应的salary

CREATE TABLE dept_emp ( emp_no int(11) NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select d.dept_no,d.emp_no,max(s.salary) as salary
from (
    dept_emp as d inner join salaries as s
    on d.emp_no = s.emp_no
)
where d.to_date = '9999-01-01' AND s.to_date = '9999-01-01'
group by d.dept_no

从titles表获取按照title进行分组,每组个数大于等于2,给出title以及对应的数目t。

CREATE TABLE IF NOT EXISTS "titles" ( emp_no int(11) NOT NULL, title varchar(50) NOT NULL, from_date date NOT NULL, to_date date DEFAULT NULL);

select title,count(title) as t
from titles
group by title
having t>=2

从titles表获取按照title进行分组,每组个数大于等于2,给出title以及对应的数目t。

注意对于重复的emp_no进行忽略。 CREATE TABLE IF NOT EXISTS titles ( emp_no int(11) NOT NULL, title varchar(50) NOT NULL, from_date date NOT NULL, to_date date DEFAULT NULL);

select title,count(title) as t
from (
    select title from titles group by title,emp_no
)
group by title
having t>=2
select title,count(distinct emp_no) as t
from titles
group by title
having t>=2

查找employees表所有emp_no为奇数,且last_name不为Mary的员工信息,并按照hire_date逆序排列

CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

select *
from employees
where emp_no%2 == 1
and last_name != "Mary"
order by hire_date  desc

统计出当前各个title类型对应的员工当前(to_date='9999-01-01')薪水对应的平均工资。结果给出title以及平均工资avg。

CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date)); CREATE TABLE IF NOT EXISTS "titles" ( emp_no int(11) NOT NULL, title varchar(50) NOT NULL, from_date date NOT NULL, to_date date DEFAULT NULL);

select t.title,avg(s.salary)
from (
    salaries as s inner join titles as t
    on s.emp_no = t.emp_no
)
where s.to_date='9999-01-01'
and t.to_date='9999-01-01'
group by title

获取当前(to_date='9999-01-01')薪水第二多的员工的emp_no以及其对应的薪水salary

CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select emp_no,salary
from salaries
order by salary desc
limit 1,1

查找当前薪水(to_date='9999-01-01')排名第二多的员工编号emp_no、薪水salary、last_name以及first_name,不准使用order by

CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select e.emp_no,s.salary,e.last_name,e.first_name
from (
    employees as e inner join salaries as s
    on e.emp_no = s.emp_no
    and s.to_date= '9999-01-01'
    and s.salary = (
        /*select max(salary) from salaries*/
        select max(salary) from salaries where salary<(
            select max(salary) from salaries
        )
    )
)

查找所有员工的last_name和first_name以及对应的dept_name,也包括暂时没有分配部门的员工

CREATE TABLE departments ( dept_no char(4) NOT NULL, dept_name varchar(40) NOT NULL, PRIMARY KEY (dept_no)); CREATE TABLE dept_emp ( emp_no int(11) NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no));

select e.last_name,e.first_name,dm.dept_name
from (
    (
        employees as e left join dept_emp as de
        on e.emp_no = de.emp_no
    ) left join departments as dm
    on de.dept_no=dm.dept_no
)

查找员工编号emp_no为10001其自入职以来的薪水salary涨幅值growth

CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select
(

(select salary
from salaries
where emp_no= 10001
order by to_date desc
limit 0,1) -
(select salary
from salaries
where emp_no= 10001
order by to_date
limit 0,1)

) as growth

查找所有员工自入职以来的薪水涨幅情况,给出员工编号emp_no以及其对应的薪水涨幅growth,并按照growth进行升序

CREATE TABLE employees ( emp_no int(11) NOT NULL, birth_date date NOT NULL, first_name varchar(14) NOT NULL, last_name varchar(16) NOT NULL, gender char(1) NOT NULL, hire_date date NOT NULL, PRIMARY KEY (emp_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select a.emp_no,(b.salary-c.salary) as growth
from
    employees as a
    inner join salaries as b
    on a.emp_no = b.emp_no and b.to_date = '9999-01-01'
    inner join salaries as c
    on a.emp_no = c.emp_no and a.hire_date = c.from_date
order by growth

统计各个部门的工资记录数,给出部门编码dept_no、部门名称dept_name以及次数sum

CREATE TABLE departments ( dept_no char(4) NOT NULL, dept_name varchar(40) NOT NULL, PRIMARY KEY (dept_no)); CREATE TABLE dept_emp ( emp_no int(11) NOT NULL, dept_no char(4) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,dept_no)); CREATE TABLE salaries ( emp_no int(11) NOT NULL, salary int(11) NOT NULL, from_date date NOT NULL, to_date date NOT NULL, PRIMARY KEY (emp_no,from_date));

select dm.dept_no,dm.dept_name,count(s.salary) as sum
from
    salaries as s
    inner join dept_emp as de
    on s.emp_no = de.emp_no
    inner join departments as dm
    on de.dept_no = dm.dept_no
group by dm.dept_no

redis

nosql

随时大规模高并发的出现,传统关系型数据库已经碰到了很大的问题,他难以提供更快的数据访问速度了,这导致数据库成为了瓶颈。人们提出not only sql的一种理念,就是我们不能仅仅依靠关系型数据库。

非关系型数据库

指的是没有关系的数据库,即不是二纬表,而是KV对。

redis

redis 就是其中的一个非关系型数据库,他是单线程,将数据储存在内存中的数据库,他支持丰富的数据类型,包括string,list,set,hash,zset

redis持久化

第一种是rdb方案,他将内存的数据定期储存到磁盘中,由于数据的空间问题,一般15分钟一次,第二种是aof方案,他将读取的数据定期增加到磁盘中,由于我们只是添加,一般1s一次。rdb本质为整体储存,aof为懒惰式储存,他储存的是操作,而不是数据库。

redis事务

redis半支持事务,语法错误回滚,但运行错误不会。

redis主从复制

主机写,从机读,

redis哨兵模式

当主机挂了以后,通过投票在从机中选出新的主机

缓存雪崩

大量的缓存同时失效,导致原本应该访问缓存的请求由于找不到数据,都去查询数据库,造成数据库CPU和内存巨大的压力 解决方案:对数据库加锁,或者让缓存失效时间分散开

缓存穿透

查询数据库中没有的数据,导致每次都要进入数据库查询 解决方案: 布隆过滤器,或者把数据库中不存在的数据也存入缓存设为空

布隆过滤器

引入多个相互独立的哈希函数,对数据库中的数据进行哈希,然后存入位图中,这里的多个确保了精度

缓存击穿

由于缓存中某条热点数据过期,导致大量高并发的请求击穿缓存进入数据库,导致数据库巨大的压力 解决方案: 热点数据永不过期或者访问数据库前加互斥锁, 这里为什么不是依靠数据库自己的锁呢,我认为能早处理的话就早处理,不要给数据库加压。

缓存预热

系统上线以后,将某些相关的缓存数据之间加入到缓存系统中

缓存更新

根据具体业务需求去自定义缓存淘汰机制,定期清理或者当请求来临的时候更新

缓存降级

当访问量增大,非核心服务影响核心服务的性能时,自动或者人工地让缓存尽量少查询数据库,尽管这可能导致少量的错误,但是我们的目标时系统的高可用性。

memcache、mongoDB、redis

性能都高,但是redis和memcache比mongodb强一点点 memcache只能是字符串,mongodb更加类似与关系型数据库,redis支持丰富的数据类型 redis用主从一致、哨兵机制保护数据,memcache没有冗余数据,mongoDB有主从一致、内部选举、auto sharding保护数据 redis支持rdb和aof,memcache没有持久化,mongodb用binlog Memcache用cas保存一致性,redis事务性较差,mongodb没有事务

参考资料

redis面试题 缓存穿透、缓存击穿、缓存雪崩区别和解决方案 Redis与Memcached的区别

mysql游标

游标允许我们遍历结果集

不想多说,我只是感觉好复杂

create table test(id int);
delimiter //                            #定义标识符为双斜杠
drop procedure if exists test;          #如果存在test存储过程则删除
create procedure test()                 #创建无参存储过程,名称为test
begin
  declare i int;                      #申明变量
  declare id_ int;
  declare done int;
  declare cur cursor for select id from test;
  declare continue handler for not found set done = 1;
  open cur;
  read_loop: loop
    fetch cur into id_;
    if done = 1 then
      leave read_loop;
    end if;
  select id_;
  end loop;
  close cur;
end
//                                      #结束定义语句
call test();                            #调用存储过程

mysql1-入门

DB

Database 数据库

DBMS

DatabaseManagementSystem 数据库管理系统

SQL

Sturcture Query Language 结构化查询语言

SQL语言

不是某个特定数据库供应商专有的语言,DBMS都支持SQL

MySQL 安装

MySQL 卸载

MySQL 配置

my.ini

  • port 是端口
  • datadir 是文件系统路径
  • default-storage-engin 是数据库默认引擎 注意要重启服务才能生效

Design

设计模式1-总览

为什么我们需要设计模式

有一类问题会在软件设计中反复出现,我们能够提出一种抽象的方法来解决这类问题,这就是设计模式

设计模式的七大原则

  • 单一职责原则
  • 接口隔离原则
  • 依赖反转原则
  • 里氏替换原则
  • 开闭原则
  • 迪米特法则
  • 合成复用原则

23种设计模式

5个创建型

  • 单例模式
  • 工厂模式
  • 抽象工厂模式
  • 原型模式
  • 建造者模式

7个结构性

  • 适配器模式
  • 桥接模式
  • 装饰者模式
  • 组合模式
  • 外观模式
  • 享元模型
  • 代理模式

11个行为型

  • 模版方法模式
  • 命令模式
  • 访问者模式
  • 迭代器模式
  • 观察者模式
  • 中介者模式
  • 备忘录模式
  • 解释器模式
  • 状态模式
  • 策略模式
  • 责任链模式

设计模式2-单一职责原则

单一职责原则

一个类只管一个职责

例子1

如果我们创建了交通工具类,他掌管着很多工具,汽车、飞机、轮船,显然我们不适合让一个类来管理这么多种交通工具,这样导致职责太多,也不适合分别为这三种交通工具建立3个类,这样导致修改过多,正确的做法是创建三个函数,来分别管理他们。

例子2

又如我们有树、链表、数组,我们要便利他们,你肯定不适合创建一个类,一个函数来遍历。应该是一个类三个函数分别遍历树、链表、数组 但是如果这种方法级的单一职责原则导致类过于庞大,应该考虑到使用类级的单一职责原则。 这样可以降低类的复杂度,提高可读性,降低变更的风险

设计模式3-接口隔离原则

接口隔离原则

将类之间的依赖降低到最小的接口处。

例子1

接口interface有5个方法,被类B和类D实现,被类A和类C依赖,但是A使用B只依赖接口123,C使用D只依赖接口145,这就导致了你的B多实现了4、5两个方法,D多实现了2、3两个方法。我们应该把interface拆分成3个,1,23,45,B实现1和23,D实现1和45.

例子2

比方说你有一个数组类和一个链表类,都实现了一个接口类,这个接口包含插入、删除、遍历、反转、排序,然后你有一个数组操作类,他只用到了插入删除遍历排序,还有一个链表操作类,他只用到了插入删除遍历反转,这个设计就很糟糕, 你应该创建3个接口,第一个为插入删除遍历,第二个为反转,第三个为排序,让数组实现第一个接口和最后一个接口,让链表实现第一个接口和第二个接口。

设计模式4-依赖反转原则

依赖反转原则

高层模块不应该依赖底层模块,他们都应该依赖其抽象,抽象不应该依赖具体,具体应该依赖抽象, 因为具体是多变的,抽象是稳定的。

例子1

有一个email类,一个person类,person接受邮件的时候要将email作为函数参数来实现,这就导致person依赖email,这很糟糕,万一需求变了,来了个微信类,来了个QQ类,来了个丁丁类,你岂不是要实现一堆person的方法? 你应该设计一个接受者接口,让其作为接受者接口的实现,让person依赖接受者这个接口

例子2

有一个数组类和一个操作类,操作类需要操作数组的首个元素,我们将数组类作为操作类的函数的参数,这很糟糕,万一需求变了,要求操作链表怎么办? 我们应该定义一个容器接口,让数组类实现它,对于操作类,我们只需要将容器接口作为参数即可,如果需求变化,加入了链表类,也不会导致大量的修改,非常稳定。

设计模式5-里氏替换原则

里氏替换原则

子类能够替换父类,并不产生故障,子类不要重写父类的方法。如果不能满足就不要这样继承。

例子1

做了一个减法类,然后让另外一个加法类继承减法类,重写了减法类的方法为加法。。你觉得这样合适吗???你应该定一个更加基础的类,让加法类和减法类都继承它。

例子2

做了一长方形类,有个函数叫返回面积,让正方形类继承了长方形类,有个主类先定义了长为2,宽为2的长方形,然后让长扩大4倍,就成了82,如果你用正方形类来替换长方形类的位置,扩大4被以后面积成了88,这很糟糕,应该让长方形继承正方形。

设计模式6-开闭原则

开闭原则

一个模块和函数应该对扩展开放,对修改关闭

&esmp; 就是说我们尽量去扩展原有的功能,而不是修改功能。另一方面源代码应该允许扩展。

例子

有一个数组、链表,有一个排序类,我们让排序类去对数组和链表排序,这个也不是很好,如果我们加入了双端数组,则需要修改排序类, 正确的方法是将排序作为一个成员方法来实现,即在基类中就定义一个排序的虚函数。

设计模式7-迪米特法则

迪米特法则

一个类和其他类个关系越少越好。

例子

有类A,B,C,其中B是A的成员,C是B的成员,下面是这个糟糕的例子

class C {
 public:
  void f() {}
};
class B {
 public:
  C c;
};
class A {
  B b;
  void doing_some_thing() { b.c.f(); }
};
int main() {}

这里注意到b.c.f();这里导致了A和C扯上了关系,正确的做法应该是在B中声明函数cf();

class C {
 public:
  void f() {}
};
class B {
 public:
  C c;
  void cf() {}
};
class A {
  B b;
  void doing_some_thing() { b.cf(); }
};
int main() {}

现在A就和C没关系了。

设计模式8-合成复用原则

合成复用原则

尽量使用合成而不是继承 说的就是让一个类的对象做另外一个类的成员变量。

设计模式9-单例模式

单例模式

单例模式的类,只允许出现一个对象

饿汉式

构造函数私有化,在内部之间final,new创建自己或者使用静态代码块new,提供静态方法访问。 简单,避免线程同步,在类装载的时候实例化,没有达到懒加载,可能造成内存浪费

线程不安全的懒汉式

构造函数私有化,在内部创建自己的引用,设为空值,提供静态方法调用,在静态方法中有选择性地new自己 简单,线程不安全,达到了懒加载效果

线程安全的懒汉式

在if中进行同步操作,在同步中进行if最后new,注意使用volatile 简单,线程安全

静态内部类

字面意思,很强,懒加载,因为类只会加载一次,所以线程安全,这个写法最优秀

枚举方式

用枚举类型将类导入,

设计模式10-工厂模式

工厂模式

设计一个工厂类,包含一个函数,返回指定类型

工厂方法模式

我们让原先的工厂作为基类,让多个工厂继承他,这就成为了工厂方法模式,比方说最开始的时候我们有多种口味的🍕,我们使用工厂模式完成了,现在来了新的需求,要求有多个地方的🍕,这时候我们就使用继承。为每个地理位置创建一个工厂。

设计模式11-抽象工厂模式

抽象工厂模式

考虑工厂方法模式,让工厂的父类作为接口即可。

设计模式12-原型模式

原型模式

用原型实例来拷贝另一个对象,java中为.clone(),c++中为=。

深拷贝前拷贝

是否拷贝指针指向的内容。

设计模式13-建造者模式

建造者模式

将复杂对象的建造方式抽象出来,一步一步抽象的建造出来。

产品

创建的产品对象

抽象建造者

指定建造流程

具体建造者

实现抽象建造者

指挥者

隔离客户和对象的生产过程,控制产品对象的生产过程

建房子

比方你现在要建造一个房子,你需要打地基,砌墙,封顶,你可以建造矮房子,也可以建造高房子,现在你就可以使用建造者模式,房子是产品,建造者能打地基,砌墙,封顶,房子组合为建造者,建造者聚合指挥者,我们依赖指挥者。

StringBuilder

Appendable是抽象建造者,AbstractStringBuilder为建造者,不能实例化,StringBuild为指挥者和具体建造者,但是是由AbstractStringBuilder建造的。

设计模式14-适配器模式

适配器模式

将一个类的接口转化为用户可以使用的接口,如c++的queue和stack为deque的适配器

类适配器

一般为继承,一个类继承了另外一个类,通过简化接口,达到适配的效果

对象适配器

...

Docker

mac中ping-docker容器

brew cask install tunnelblick

找一个目录

git clone https://github.com/wojas/docker-mac-network.git
cd docker-mac-network
vim helpers/run.sh

修改网段和掩码

s|redirect-gateway.*|route 172.17.0.1 255.255.0.0|;

执行

docker-compose up -d

得到一个docker-for-mac.ovpn 在route 172.17.0.0 255.255.0.0 上面加

comp-lzo yes

双击docker-for-mac.ovpn,会被tunnelblick打开,一直点确定就好了

参考

mac连接docker容器 docker-mac-network docker-mac-network

docker再入门

镜像

就是类似于虚拟机中的iso文件

容器

就是运行中的系统

tar文件

将一个镜像保存为tar文件

Dockerfile

写构建的步骤来制作镜像

仓库

保存了很多镜像

免费使用

点这里

--link myng:myng

将另一个容器储存为域名,其实是在/etc/hosts中加入了一行映射

复杂的Docker

比方你有一个nginx服务,php服务,mysq服务,nginx依赖php,php依赖mysql,这个依赖关系导致我们需要先创建mysq,然后创建pho。这就很麻烦,部署、重启啊很麻烦的。

docker-compose

vim docker-compose.yml
version: "3"
services:
  nginx:
    image: nginx
    ports:
    - 80:80
    volumes:
    - /root/html:/usr/share/nginx/html
    - /root/conf/nginx.conf:/etc/nginx/nginx.conf
  php:
    image: php
    volumes:
    - /root/html:/var/www/html
  mysql:
    images: mysql
    enviroment:
    - MYSQL_ROOT_PASSWORD=123456

启动

docker-compose up -d

参考

10分钟,快速学会docker 实战~如何组织一个多容器项目docker-compose

Graph

霍尔定理

霍尔定理推论: 对于一个二分图G<V,E> 若的点可以分为两部分N和M,N内部没有边,M同理,S'是N的某个子集(可以为空),f(S')是与该子集相邻的点集, 则他的最大匹配为|N|-max(|S'|-|f(S')|),

虚树

虚树就是把树上一些节点拿下来重新建树,插入一些lca之类的点,deltree会删除一颗树,但不会删掉他的边,所以要注意边的情况

// tree 节点0不准使用
const int maxn=5e5+5;
int head[maxn];// point
int to[maxn*2],ew[maxn*2],nex[maxn*2],tot;// edge
inline void _addedge(int u,int v,int w){to[++tot]=v,ew[tot]=w,nex[tot]=head[u],head[u]=tot;}
inline void addedge(int u,int v,int w){_addedge(u,v,w),_addedge(v,u,w);}
void deltree(int rt,int father){// deltree() and also don't forget 还原tot
    for(int i=head[rt];i;i=nex[i]) if(to[i]!=father) deltree(to[i],rt);
    head[rt]=0;
}

// 树剖lca
int dep[maxn],dad[maxn],siz[maxn],son[maxn],dfn[maxn],chain[maxn],step;
void dfs1(int u,int father){// dfs(1,0)
    siz[u]=1; son[u]=0; dad[u]=father; dep[u]=dep[father]+1;
    for(int i=head[u];i;i=nex[i]){
        if(to[i]==father) continue;
        dfs1(to[i],u);
        siz[u]+=siz[to[i]];
        if(siz[to[i]]>siz[son[u]]) son[u]=to[i]; // don't care son=0 because siz[0]=0
    }
}
void dfs2(int u,int s){// dfs(1,1) step=0 at begin
    dfn[u]=++step; chain[u]=s;
    if(son[u]) dfs2(son[u],s);
    for(int i=head[u];i;i=nex[i]){
        if(to[i]==dad[u]||to[i]==son[u]) continue;
        dfs2(to[i],to[i]);
    }
}
int getlca(int x,int y){
    while(chain[x]!=chain[y]) {
        if(dep[chain[x]]<dep[chain[y]]) swap(x,y);
        x=dad[chain[x]];
    }
    return dep[x]<dep[y]?x:y;
}

// virtual tree
bool vt[maxn];// point
void buildvt(int*vert,int nums,int base){// vert -> [1,nums]
    sort(vert+1,vert+nums+1,[](int x,int y){return dfn[x]<dfn[y];});
    int top=0;
    stk[++top]=1,vt[base+1]=vert[1]==1; // root
    rep(i,vert[1]==1?2:1,nums){
        int lca=getlca(vert[i],stk[top]);
        if(lca==stk[top]) {stk[++top]=vert[i],vt[base+vert[i]]=true;continue;}//还在链上
        while(dfn[lca]<=dfn[stk[top-1]]) addedge(base+stk[top],base+stk[top-1],0),top--;
        if(lca!=stk[top]) addedge(base+stk[top],base+lca,0),stk[top]=lca,vt[base+lca]=false;
        stk[++top]=vert[i],vt[base+vert[i]]=true;
    }
    while(top>=2){
        addedge(base+stk[top],base+stk[top-1],0);
        top--;
    }
}

树hash

// tree 节点0不准使用
int head[maxn];// point
int to[maxn*2],nex[maxn*2],tot;// edge
inline void _addedge(int u,int v){to[++tot]=v,nex[tot]=head[u],head[u]=tot;}
inline void addedge(int u,int v){_addedge(u,v),_addedge(v,u);}
void deltree(int rt,int father){// deltree() and also don't forget tot
    for(int i=head[rt];i;i=nex[i]) if(to[i]!=father) deltree(to[i],rt);
    head[rt]=0;
}
//  struct tree{int rt,n;}


//tree hash
int pw[maxn*2]={1},hshmod;//pw要两倍
int *hsh,siz[maxn]; //point
int *ehsh; //edge
void dfs(int u,int father){
    siz[u]=1;
    for(int i=head[u];i;i=nex[i]){
        if(to[i]==father)continue;
        dfs(to[i],u), siz[u]+=siz[to[i]];
    }
}
void dfs1(int u,int father){// solve every edge from father->u
    for(int i=head[u];i;i=nex[i]){
        if(to[i]==father) continue;
        dfs1(to[i],u);

        vector<pii>buf;
        for(int j=head[to[i]];j;j=nex[j]){
            if(to[j]==u) continue;
            buf.emplace_back(ehsh[j],2*siz[to[j]]);
        }
        sort(buf.begin(),buf.end());
        ehsh[i]=1;// 左边放1
        for(pii x:buf) ehsh[i]=(1ll*ehsh[i]*pw[x.second]+x.first)%hshmod;
        ehsh[i]=(1ll*ehsh[i]*pw[1]+2)%hshmod;// 右边放2
    }
}
void dfs2(int u,int father,int rt){
    vector<pii>buf;
    for(int i=head[u];i;i=nex[i]) {
        if(to[i]==father) buf.emplace_back(ehsh[i],2*(siz[rt]-siz[u]));
        else buf.emplace_back(ehsh[i],2*siz[to[i]]);
    }
    sort(buf.begin(),buf.end());
    hsh[u]=1;// 左边放1
    for(pii x:buf) hsh[u]=(1ll*hsh[u]*pw[x.second]+x.first)%hshmod;
    hsh[u]=(1ll*hsh[u]*pw[1]+2)%hshmod;// 右边放2

    vector<pii>pre(buf),suf(buf);// 对后面进行处理
    int sz=suf.size();
    for(int i=1,j=sz-2;i<sz;i++,j--){
        pre[i].first=(1ll*pre[i-1].first*pw[pre[i].second]+pre[i].first)%hshmod;// merge i-1 and i
        suf[j].first=(1ll*suf[j].first*pw[suf[j+1].second]+suf[j+1].first)%hshmod;// merge j and j+1
        pre[i].second+=pre[i-1].second;
        suf[j].second+=suf[j+1].second;
    }

    for(int i=head[u];i;i=nex[i]){
        if(father==to[i]) continue;
        ehsh[i^1]=1;//左边放1
        int idx=lower_bound(buf.begin(),buf.end(),pii(ehsh[i],2*siz[to[i]]))-buf.begin();
        if(idx-1>=0) ehsh[i^1]=(1ll*ehsh[i^1]*pw[pre[idx-1].second]+pre[idx-1].first)%hshmod;// 前缀
        if(idx+1<sz) ehsh[i^1]=(1ll*ehsh[i^1]*pw[suf[idx+1].second]+suf[idx+1].first)%hshmod;// 后缀
        ehsh[i^1]=(1ll*ehsh[i^1]*pw[1]+2)%hshmod;//右边放2
        dfs2(to[i],u,rt);
    }
}
void treehash(int u,int*hsh_,int*ehsh_,int base,int hshmod_){//hash all tree of tree u
    hsh=hsh_,ehsh=ehsh_,hshmod=hshmod_;
    dfs(u,0); for(int i=1;i<=siz[u]*2;i++) pw[i]=1ll*pw[i-1]*base%hshmod;
    dfs1(u,0),dfs2(u,0,u);
}
////// end

支配树

### include<bits/stdc++.h>
using namespace std;
### define rep(i,j,k) for(int i=j;i<=(k);++i)
### define per(i,j,k) for(int i=j;i>=(k);--i)
### define repe(i,u) for(int i=head[u];i;i=nex[i])

// graph
const int V=3*1e5+5,E=3*2e5+5;
int head[V],deg[V];
int to[E],nex[E],edge=1;
inline void addedge1(int u,int v) {to[++edge]=v,nex[edge]=head[u],head[u]=edge,deg[v]++;}
void del(int u){repe(i,u) head[u]=0,deg[u]=0,del(to[i]);}

// dominator tree
int dad[V],sdom[V],idom[V],dfn[V],rnk[V],step;
void tarjan(int u,int father){ //if(father==0) step=0;
    dfn[u]=++step,rnk[step]=u,dad[u]=father;
    repe(i,u)if(dfn[to[i]]==0)tarjan(to[i],u);
}
int df[V],dw[V];//dsu
int find(int x){
    if(x==df[x])return x;
    int tmp=find(df[x]);
    if(dfn[sdom[dw[df[x]]]]<dfn[sdom[dw[x]]])dw[x]=dw[df[x]];
    return df[x]=tmp;
}
void Lengauer_Tarjan(int g1,int g2,int n,int s,int g3){// s是起点 g1是正向图,g2是反向图,g3是支配树
    rep(i,g1+1,g1+n) dfn[i]=0;
    step=g1; tarjan(g1+s,0);
    rep(i,g1+1,g1+n) df[i]=i,dw[i]=sdom[i]=i;// init dsu
    per(i,g1+n,g1+2){//以g1为主体,映射其他图
        int u=rnk[i];
        repe(j,u-g1+g2) {// 在g2中枚举反向边
            int v=to[j]-g2+g1;// 映射回g1
            find(v);
            if(dfn[sdom[dw[v]]]<dfn[sdom[u]])sdom[u]=sdom[dw[v]];
        }
        df[u]=dad[u];// 只有后向边产生影响,因为只有后向边的终点满足要求

        addedge1(sdom[u]-g1+g3,u-g1+g3);// g1->g3
        repe(j,dad[u]-g1+g3){//在g3中枚举边
            int v=to[j]-g3+g1; // 映射回g1
            find(v);
            idom[v]=sdom[dw[v]]==dad[u]?dad[u]:dw[v];
        }
    }
    rep(i,g1+2,g1+n) {
        int x=rnk[i];
        if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
    }
    del(g3+s);
    rep(i,g1+1,g1+n) addedge1(idom[i]-g1+g3,i-g1+g3);
    rep(i,g1+1,g1+n) cout<<idom[i]<<" "<<sdom[i]<<" "<<i<<endl;
}

//lca
int dep[V],siz[V],son[V],chain[V];//,dad[V],dfn[V];//
void dfs1(int u,int father){//dfs1(1,0)
    dep[u]=dep[father]+1;//ini  because dep[0]=1
    dad[u]=father, siz[u]=1, son[u]=-1;
    repe(i,u){
        int v=to[i];
        dfs1(v,u);
        siz[u]+=siz[v];
        if(son[u]==-1||siz[son[u]]<siz[v]) son[u]=v;
    }
}
void dfs2(int u,int s){
    dfn[u]=++step;
    chain[u]=s;
    if(son[u]!=-1) dfs2(son[u],s);
    repe(i,u){
        int v=to[i];
        if(v!=son[u]&&v!=dad[u]) dfs2(v,v);
    }
}
int querylca(int x,int y){
    while(chain[x]!=chain[y]){
        if(dep[chain[x]]<dep[chain[y]]) swap(x,y); //dep[chain[x]]>dep[chain[y]]
        x=dad[chain[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);// dep[x]<dep[y]
    return x;
}

inline int read(){int x;scanf("%d",&x);return x;}

int main(){
     freopen("/Users/s/Downloads/2019HDOJ多校3_UESTC/data/1002/1in.txt","r",stdin);
    int t=read();
    while(t--){
        int n=read()+1,m=read();
        int g1=0,g2=n,g3=2*n;  // g1是正向图,g2是反向图,g3是支配树
        rep(i,0,3*n) head[i]=deg[i]=0; edge=1;
        while(m--){
            int u=read(),v=read();
            addedge1(g1+v,g1+u);
            addedge1(g2+u,g2+v);
        }
        rep(i,1,n-1) if(deg[i]==0) addedge1(g1+n,g1+i),addedge1(g2+i,g2+n);
        Lengauer_Tarjan(g1,g2,n,n,g3);
        dfs1(g3+n,0),dfs2(g3+n,g3+n);
        int q=read();
        while(q--){
            int x=g3+read(),y=g3+read();
            printf("%d\n",dep[x]+dep[y]-dep[querylca(x,y)]-1);
        }
        return 0;
    }
}

最短路算法

### define rep(i,j,k) for(int i=j;i<=(k);++i)
### define per(i,j,k) for(int i=j;i>=(k);--i)
### define repe(i,u) for(int i=head[u];i;i=nex[i])

// graph
const int V=5e4+5,E=5e4+5;
int head[V];
int to[E],nex[E],ew[E],tot=1;
inline void addedge1(int u,int v,int w) {to[++tot]=v,nex[tot]=head[u],ew[tot]=w,head[u]=tot;}
void del(int u){repe(i,u) head[u]=0,del(to[i]);}

// dijkstra算法
typedef long long ll;
ll d[V];// 距离数组
typedef pair<ll,int>pii;
void dijkstra(int base,int n,int s,ll*dist){
    rep(i,base+1,base+n) dist[i]=1e18;
    priority_queue<pii,vector<pii>,greater<pii>>q;// dis and vertex
    q.emplace(dist[base+s]=0,base+s);
    while(!q.empty()){
        int u=q.top().second; q.pop();
        repe(i,u){
            int v=to[i],w=ew[i];
            if(dist[u]+w<dist[v])q.emplace(dist[v]=dist[u]+w,v);
        }
    }
}

最大流最小割算法

### define rep(i,j,k) for(int i=j;i<=(k);++i)
### define per(i,j,k) for(int i=j;i>=(k);--i)
### define repe(i,u) for(int i=head[u];i;i=nex[i])

// graph
const int V=5e4+5,E=5e4+5;
int head[V];
int to[E],nex[E],ew[E],tot=1;
inline void addedge1(int u,int v,int w) {to[++tot]=v,nex[tot]=head[u],ew[tot]=w,head[u]=tot;}
void del(int u){repe(i,u) head[u]=0,del(to[i]);}

//最大流最小割算法
int lv[V],current[V],src,dst;
int *cap=ew;//容量等于边权
bool maxflowbfs(){
    queue<int>q;
    lv[src]=0, q.push(src);
    while(!q.empty()){
        int u=q.front();q.pop();
        repe(i,u){
            if(cap[i]==0||lv[to[i]]>=0)continue;
            lv[to[i]]=lv[u]+1, q.push(to[i]);
        }
    }
    return lv[dst]>=0;
}
int maxflowdfs(int u,int f){
    if(u==dst)return f;
    for(int&i=current[u];i;i=nex[i]){//当前弧优化
        if(cap[i]==0||lv[u]>=lv[to[i]])continue;
        int flow=maxflowdfs(to[i],min(f,cap[i]));
        if(flow==0) continue;
        cap[i]-=flow,cap[i^1]+=flow;
        return flow;
    }
    return 0;
}
ll maxflow(int base,int n,int s,int t){
    src=base+s,dst=base+t;
    ll flow=0,f=0;// 计算最大流的过程中不可能爆int 唯独在最后对流量求和对时候可能会比较大 所以只有这里用ll
    while(true){
        rep(i,base+1,base+n) current[i]=head[i],lv[i]=-1;
        if(!maxflowbfs())return flow;
        while(f=maxflowdfs(src,2e9))
            flow+=f;
    }
}

生成树总结

生成树

一个无向图的生成树指的是从图中选若干边构成边集,全部点构成点集,使得这个边集加上点集恰好是一棵树。

生成树计数

一个无向无权图(允许重边不允许自环)的邻接矩阵为g,显然这是一个对称矩阵,g[u][v]代表边(u,v)的重数,即若存在一条边(u,v)则g[u][v]的值为1,若存在k条,则g[u][v]的值为k。
一个无向无权图(允许重边不允许自环)的度数矩阵为deg,显然这是一个对角矩阵,deg[u][u]代表点u的度数。
一个无向无权图(允许重边不允许自环)的基尔霍夫矩阵(拉普拉斯矩阵)为hoff,是度数矩阵减去邻接矩阵。
矩阵树定理说一个无向图的生成树的个数刚好等于基尔霍夫矩阵的行列式的任意n-1阶主子式(代数余子式)的行列式的绝对值。
生成树计数复杂度$O(V^3+E)=O(V^3)$
黑暗前的幻想乡
我们利用矩阵树定理就能轻松解决

黑暗前的幻想乡代码 {% include_code p4336 lang:cpp cpp/p4336-生成树计数.cpp %}
最小生成树

有一个无向带权图,每条边有权$x_i$,需要求出一个生成树T,并最小化$\begin{aligned}\sum_{i\in T}x_i\end{aligned}$ kruskal算法:贪心从小到大枚举边合并森林即可。这里就不证明此算法了。
最小生成树复杂度$O(V+ElgE)=O(ElgE)$
最小生成树

最小生成树代码 {% include_code p3366 lang:cpp cpp/p3336-最小生成树.cpp %}
最小生成树计数

由于最小生成树各自边权构成的多重集合是一样的,并且易证不同的边权对最小生成树的影响是独立的,所以我们可以通过将边按照边权分类,分别求出每一种边权各自对联通块的贡献,然后利用计数的乘法原理合并即可。我们需要先求出任意一个最小生成树,当我们对某一种边权进行讨论的时候,我们需要将这个生成树中这一边权的边全删掉,然后对剩余联通块进行缩点并重新编号,将待选集合中的边映射到联通块间的边,并去掉自环。这样以后待选集合中的边的边权就相等了。这时我们可以借助矩阵树定理来求解。
最小生成树计数复杂度$O(ElgE+V^3)=O(V^3)$
最小生成树计数

最小生成树计数代码 {% include_code p4208 lang:cpp cpp/p4208-最小生成树计数.cpp %}
严格次小生成树

严格次小生成树和最小生成树的边权多重集合中只有一个边权不一样,这样我们就有了一个简单的算法,先求出任意一个最小生成树,然后枚举没有被选中为构建最小生成树的边,假设这个边为$(u,v,w_1)$,我们在最小生成树上求出点$u$到点$v$这条路径上的最大边权$w_2$和严格次大边权$w_3$,若$w_1=w_2$则我们用此边换掉次大边,若$w_1>w_2$则我们用此边换掉最大边,这样我们最终能得到很多非最小生成树,从中选出最小的那个,他就是次小生成树,这个过程需要维护树上的路径信息,有算法都可以树剖、树上倍增、lct等等,我们这里使用树上倍增的办法来解决。
严格次小生成树时间复杂度$O(ElgE+ElnV)=O(ElgE)$
严格次小生成树

严格次小生成树代码 {% include_code p4180 lang:cpp cpp/p4180-严格次小生成树.cpp %}
最小乘积生成树

有一个无向带权图(权为正数),每条边有权$x_i$和权$y_i$,需要求出一个生成树T,记$\begin{aligned}X=\sum_{i\in T}x_i,Y=\sum_{i\in T}y_i\end{aligned}$,要求最小化乘积$XY$
我们假设已经求出了所有生成树,他们的权为$(X_i,Y_i)$我们把这个二元组看做二维平面上的点,则最小乘积生成树一定在凸包上。进一步分析,整个凸包都在第一象限,那么我们可以锁定出两个点了,他们一定在凸包上。分别求出最小的$X_i$对映点$A$,和最小的$Y_i$对映点$B$,那么答案就在$AB$左下方,我们求出一个点$C$,若能让叉积$AC*AB$最大且为正数,则$C$一定也在凸包上。我们递归处理$AC$和$CB$即可。凸包上的点期望为lg级别。
最小乘积生成树复杂度$O(ElgElg(V!))=O(ElgElgV)$
最小乘积生成树

最小乘积生成树代码 {% include_code p5540 lang:cpp cpp/p5540-最小乘积生成树.cpp %}
最小差值生成树

有一个无向带权图,每条边有权$x_i$,需要求出一个生成树T,让T的最大边权和最小边权的差值尽可能小。
我们对边集排序后,等价于求最短的一段区间,这个区间内部的边能够生成一棵树,这种连通性维护的问题,直接使用lct就可以了,
最小差值生成树时间复杂度$O(ElgE+Elg(E+V))=O(ElgE)$
最小差值生成树

最小差值生成树代码 {% include_code p4234 lang:cpp cpp/p4234-最小差值生成树.cpp %}
k度限制最小生成树

在最小生成树的要求下,多一个条件: 有一个定点的度数不能超过k。
k度限制最小生成树与k-1度限制最小生成树最多有一条边的区别。
时间复杂度$O(ElgE+kV)$ k度限制最小生成树

k度限制最小生成树代码 {% include_code poj1639 lang:cpp cpp/poj1639-k度限制最小生成树.cpp %}
最小直径生成树

给无向连通图,求一个直径最小的生成树。 以图的绝对中心为根的最短路树,是一个最小直径生成树。先用floyd求多源最短路,然后对每一条边,假设绝对中心在此边上,求出该绝对中心的偏心率,可以考虑从大到小枚举最短路闭包来实现,汇总得到绝对中心,最终以绝对中心为根,求最短路树。 时间复杂度$O(n^3)$

最小直径生成树代码 {% include_code spoj1479 lang:cpp cpp/spoj1479-最小直径生成树.cpp %}
最小比值生成树

有一个无向带权图(权为正数),每条边有权$x_i$和权$y_i$,需要求出一个生成树T,记$\begin{aligned}X=\sum_{i\in T}x_i,Y=\sum_{i\in T}y_i\end{aligned}$,要求最小化比值$\frac{X}{Y}$. 我们设$r=\frac{X}{Y}$则有$rY-X=0$我们定义函数$f(r)=rY-X$,为当以$ry_i-x_i$作为权值的时候的最大生成树的值,这里显然f(r)关于r单调增,当我们找到一个r使得f(r)等于0的时候,r就是我们分数规划要的答案。 时间复杂度$O(lgn)*O(MST)$

最小比值生成树代码 {% include_code poj2728 lang:cpp cpp/poj2728-最小比值生成树.cpp %}

Java

Java基础

Java基础1-Automic

Automic

是一个原子类型包,其中包含了AtomicBoolean,AtomicInteger,AtomicLong等, 原子操作说是这样说的,然而并不是所有的物理机器都支持原子指令,所以不能保证不被阻塞,一般而言,采用的CAS+volatile+native的方法,避免synchronized的使用,如果不支持CAS那就上自旋锁了

接口

我上图好不好,不想搞了

Java基础2-日志

log4j

Maven依赖
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
log4j.properties
log4j.rootLogger=all, stdout, logfile

<!--more-->

#### 日志输出位置为控制台
#### (ConsoleAppender)控制台
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.err
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH\:mm\:ss} %l %F %p %m%n

#### 日志输出位置为文件
#### (RollingFileAppender)
log4j.appender.logfile=org.apache.log4j.RollingFileAppender
#### 日志文件位置
log4j.appender.logfile.File=log/log.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.Append = true
log4j.appender.logfile.MaxFileSize=1MB
log4j.appender.logfile.layout.ConversionPattern=%d{yyyy-MM-dd HH\:mm\:ss} %l %F %p %m%n
用法
package com.wsx;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

public class Test {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Test.class);
        logger.info("info");
        logger.debug(" debug ");
        logger.debug(" debug ");
        logger.debug(" debug ");
        logger.debug(" debug ");
        logger.debug(" debug ");
        logger.debug(" debug ");
        logger.error(" error ");
        logger.error(" error ");
        logger.error(" error ");
    }
}

SLF4J

log的实现太多了,log4j,logBack,jdklog,以后想换怎么办呢? Simple Logging Facade for Java 就像和JDBC一样,SLF4J把所有的日志框架连接起来

五个级别

trace,debug,info,warn,error 啥事不干,写下面的代码

package com.wsx;

import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

public class Test {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Test.class);
        logger.trace("trace");
        logger.debug("debug");
        logger.info("info");
        logger.warn("warn");
        logger.error("error");
    }
}

得到了

22:23:01.931 [main] DEBUG com.wsx.Slf4jStudy - debug
22:23:01.940 [main] INFO com.wsx.Slf4jStudy - info
22:23:01.941 [main] WARN com.wsx.Slf4jStudy - warn
22:23:01.941 [main] ERROR com.wsx.Slf4jStudy - error
logback

写一个logback.xml appender 后面是log的名字,再往后是输出位置:文件或者控制台 level后面跟级别,表示输出哪些东西

<configuration>
    <!--
    1.起别名
    2.服务器上跑的项目多的时候可以好的区分
     -->
    <contextName>play_dice</contextName>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 调整输出的格式 -->
        <encoder>
          <pattern>%date{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>
    <!--
    1.key:对应的是动态的文件名
    2.datePattern:是动态生成的时间格式
    -->
    <timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <!-- 动态设置日志的文件名 -->
        <!--<file>D:\日志\log-${bySecond}.txt</file>-->
        <append>false</append><!-- 每次一更新就是新的 -->
        <encoder>
          <pattern>%date{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 设置输出的等级 -->
    <root level="tarce">
        <appender-ref ref="STDOUT"/><!-- 输出到控制台 -->
        <appender-ref ref="FILE"/><!-- 输出到文件中 -->
    </root>
</configuration>
简化

注释太烦了,我们给他全删掉,使用下面的vim指令 \v代表字符模式,把所有的特殊字符看着特殊意义 (.|\n)可以匹配所有的字符 {-}是*的非贪婪匹配

%s/\v\<!--(.|\n){-}--\>//g

会到logback中 %date是时间,%thread是线程,level是级别,-是左对齐,%logger指名字,%msg是日志输出 %n是换行

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>
    <timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <append>false</append>
        <encoder>
            <pattern>%date{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="tarce">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>
细节
logger.info("hello {} {} {}","I","am","wsx");

Java并发

java并发编程1-进程与线程

Where from

快来点我

进程

一个活动的程序,是程序的实例,大部分程序可以运行多个实例,有的程序只可以运行一个实例

线程

一个进程可以有多个线程,线程是最小的调度单位,进程是资源分配的最小单位,

进程与线程

进程互相独立,线程是进程的子集 进程拥有共享的资源,供其内部的线程共享 进程通信较为复杂,同一台计算机之间的进程通信叫做IPC,不同的计算机之间需要通过网络协议 线程的通信相对简单,他们共享进程的内存, 线程更加轻量,他们上下文切换的成本更低

并行与并发

单核CPU的线程都是串行,这就叫并发concurrent 多核CPU能够达到并行,一些代码同时执行,但是更多情况下,我们的计算机是并发+并行 并发concurrent是同一时间应对dealing with多件事情的能力,并行parallel是同一时间动手做doing多件事情的能力

同步和异步

比如我们有个视频转换转换格式非常耗时间,我们让新的线程去做处理,避免主线程被阻塞

java并发编程2-创建和运行线程

使用Tread创建线程

package com.wsx.test;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ThreadTest {
    @Test
    public void test1() {
        final Logger logger = LoggerFactory.getLogger(ThreadTest.class);
        Thread thread = new Thread() {
            @Override
            public void run() {
                logger.debug("running");
            }
        };
        thread.setName("t1");
        thread.start();
        logger.debug("running");
    }
}

得到输出

13:23:33.225 [t1] DEBUG com.wsx.test.ThreadTest - running
13:23:33.225 [main] DEBUG com.wsx.test.ThreadTest - running

使用Runnable

    @Test
    public void test2(){
        final Logger logger = LoggerFactory.getLogger(ThreadTest.class);
        Runnable runnable = new Runnable() {
            public void run() {
                logger.debug("runing");
            }
        };
        Thread thread = new Thread(runnable,"t2");
        thread.start();
        logger.debug("running");
    }
13:29:08.503 [t2] DEBUG com.wsx.test.ThreadTest - runing
13:29:08.503 [main] DEBUG com.wsx.test.ThreadTest - running

lambda表达式

注意到Runnable源码中有注解@FunctionalInterface,那么他就可以被lambda表达式简化,因为只有一个方法

    @Test
    public void test3() {
        final Logger logger = LoggerFactory.getLogger(ThreadTest.class);
        Runnable runnable = () -> {
            logger.debug("runing");
        };
        Thread thread = new Thread(runnable, "t3");
        thread.start();
        logger.debug("running");
    }

Thread和Runnable

Thread在用Runnable构造的时候,把他赋值给了一个target变量,然后run

FutureTask和Callable

下面的代码会阻塞

    @Test
    public void test4() throws ExecutionException, InterruptedException {
        final Logger logger = LoggerFactory.getLogger(ThreadTest.class);
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                logger.debug("runing...");
                Thread.sleep(1000);
                return 100;
            }
        });
        Thread thread = new Thread(task, "t2");
        thread.start();
        logger.debug("running");

        // 阻塞
        logger.debug("{}", task.get());
    }
13:48:00.930 [main] DEBUG com.wsx.test.ThreadTest - running
13:48:00.930 [t2] DEBUG com.wsx.test.ThreadTest - runing...
13:48:01.937 [main] DEBUG com.wsx.test.ThreadTest - 100

jps

可以查看java程序的pid jstack <pid> 查看某个java进程的所有线程状态,非动态 jconsole可以查看java进程中线程的图形界面 他还可以远程连接,注意需要在运行java程序的时候要加入一些参数,而且注意关闭防火墙

ps

ps | grep java

, ps 看进程,grep筛选 kill + PID杀死进程

top

他用表格来显示,还是动态的 -H 表示查看线程,-p表示pid

top -H -p 3456

java并发编程3-线程运行原理

每个线程都有自己的栈,

线程上下文切换

  • CPU时间片用完
  • gc
  • 有更高优先级的线程要运行
  • 线程自己sleep,yield,wait,join,park,synchronized,lock

线程中常用的方法

  • start() 线程进入就绪状态
  • run() 线程要执行的方法
  • join() 等待某个线程结束
  • join(long) 最多等待n毫秒
  • getId() 线程的长整型id
  • getName() 线程名
  • setName()
  • getPriority() 优先级
  • setPriority()
  • getState() 线程状态
  • isInterrupted() 是否被打断
  • isAlive() 是否存活
  • interrupt() 打断线程
  • interrupted() 判断是否被打断,会清楚标记
  • currentThread() 获得当前正在执行的线程
  • sleep() 睡眠
  • yield() 提示调度器让出当前线程对cpu使用

run和start

主线程也可以直接调用thread的run,但这不是多线程

sleep

    @Test
    public void f(){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

yield

让当前线程进入就绪状态,而不是sleep的阻塞

防止CPU占用100%

    @Test
    public void test5() {
        while (true) {
            try{
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

打断

打断会让打断标记变为true,sleep的时候打断会抛出异常,并清除打断标记,没有sleep的程序,可以用Thread.isInterrupted()退出

两阶段终止模式(别用stop)

每次循环前尝试判断自己是否被打断,sleep的时候被打断会被清除,就自己打断自己,最后料理后事

LockSupport.park()

打断park的线程,不会清除标记,所以连续的park其实后面的都无效了,你可以手动用Interrupted来清除。

守护线程

setDaemon(true); 只要进程的所有非守护线程都结束以后,不管守护线程有没有结束,他都会被强制结束,垃圾回收器就是守护线程。Tomcat中的Acceptor和Poller线程都是守护线程,他们用来接受请求和分发请求的,如果shutdown以后,不会等待他们处理的

线程的五种状态

初始、就绪、运行、阻塞、终止

线程的六种状态

new,runnable,blockded,wating,timedwaiting,terminated,分别是没有调用start,正在被调度,没有获得锁,join之类的等待,sleep之类的等待,执行完成 千万注意runnable包含了物种状态中的就绪、运行和阻塞

java并发编程4-同步与互斥

synchronized

锁住对象,放在静态方法前为锁类,放在普通方法前为锁类的对像。使用管程实现

线程安全类

String, Integer, StringBuffer,Random,Vector,Hashtable,juc;

加锁

把对象头的hash、Age和对象的指针放进自己的栈中,让对象头的hash、Age,位置指向自己的栈, 这时候来了另一个线程也想拿到锁,但是他肯定会失败,失败以后他就申请重量级锁,让对象头再次改变为指向管程, 当原来当线程想要释放锁的时候,依然使用cas,但是肯定失败,他发现现在的锁已经变成了重量级锁了。

自旋优化

不阻塞,使用自旋,如果自旋多次失败就阻塞了

偏向锁

可以在对象头中加入线程的ID,然后对象的锁就被这个线程所持有了。程序启动3秒以后启动偏向锁,可以通过VM参数来改变

禁用偏向锁

-XX: -UseBiasedLocking

hashcode

轻量级锁和重量级锁都不会因为调用hashcode而撤销锁状态,但是偏向锁会,因为他没有地方储存hashcode,所以调用hashcode以后,偏向锁会被撤销

wait/notify

这个是只有重量级锁才有的东西,所以也会撤销轻量锁

批量重偏向

如果连续撤销锁超过20次,jvm会批量的让类的所有对象都偏向于另一个线程

批量撤销

如果撤销次数超过40次,jvm会撤销这个类的所有对象的偏向锁,甚至新建的对象也不会有偏向锁

锁消除

JIT即时编译器会优化热点代码,如果分析出某个锁不会逃离方法,则进行锁消除

保护性暂停GuardObject

用一个中间对象把线程连接起来,注意虚假唤醒的情况发生。我们用时间相减来避免产生等待时间错误的情况

park和unpark

他们就像PV操作一样,但是信号量不能叠加 park和unpark实现的时候有三部分,_mutex,_condition,_counter,这里的_counter最多只能是 调用park : 检查_counter,如果为0,就获得_mutex锁,然后该线程进入_condition开始阻塞,如果为1,就把它设为0,然后继续执行线程 调用unpark, 把counter设为1,然后唤醒_condition中的线程

线程状态转换

start
  • NEW -> RUNNABLE 调用start()
对象锁
  • RUNNABLE -> WAITING 获得对象锁后wait()
  • WAITING -> RUNNABLE notify(),notifyAll(),interrupt()且竞争成功
  • WAITING -> BLOCKED notify(),notifyAll(),interrupt()且竞争失败
  • BLOCKED -> WAITING 当持有锁的线程执行完毕会唤醒其他BLOCKED的线程
join
  • RUNNABLE -> WAITING 调用join()
  • WAITING -> RUNNABLE join的线程执行完成或者当前线程被interrupt()
park和unpark
  • RUNNABLE -> WAITING 调用park()
  • WAITING -> RUNNABLE 调用unpark()或者interrupt()
wait(long t)
  • RUNNABLE -> TIMED_WAITING 获得对象锁后wait(long)
  • TIMED_WAITING -> RUNNABLE 超时,notify(),notifyAll(),interrupt()且竞争成功
  • TIMED_WAITING -> BLOCKED 超时,notify(),notifyAll(),interrupt()且竞争失败
join(long t)
  • RUNNABLE -> TIMED_WAITING 调用join(long)
  • TIMED_WAITING -> 超时,RUNNABLE join的线程执行完成或者当前线程被interrupt()
sleep(long)
  • RUNNABLE -> TIMED_WAITING 调用sleep(long)
  • TIMED_WAITING -> 超时,RUNNABLE sleep的线程执行完成或者当前线程被interrupt()
parkNanos和parkUntil
终止
  • RUNNABLE -> TERMINATED 当线程执行完毕

死锁

定位死锁

jconsole,jps都可以

jps

如果死锁,会提示Found One Java-level deadlock,在里面找去

jconsole

选择线程,点检测死锁,就能看到了

活锁

一个线程i++,另一个i--,难以结束了,原因是改变了互相的结束条件

饥饿

可以通过顺序加锁来避免死锁,但是这又会导致饥饿发生

ReentrantLock

可中断,可设置超时时间,可设置公平锁,支持多个条件变量,可重入

用法
reentrantLock.lock();
try{

}finally{
  reentrantLock.unlock();
}
可打断

没有竞争就能得到锁,如果进入了阻塞队列,可以被其他线程用interruput打断。

try{
  reentrantLock.lockInterruptibly();
}catch(InterruptedException e){
    e.printStackTrace();
}
try{
  //....
}finally{
  reentrantLock.unlock();
}
非阻塞

tryLock()

超时机制

tryLock(1,TimeUnit.SECONDS)

条件变量

ReentrantLock支持多个条件变量

Condition c1 = reentrantLock.newCondition()
Condition c2 = reentrantLock.newCondition()
// 获得锁之后
c1.await();
c1.signal();

同步

await和signal,park和unpark,wati和notify,

3个线程分别输出a,b,c, 要看到abcabcabcabcabc

一个整数+wait/notifyAll

轮换,1则a输出,2则b输出,3则c输出,如果不是自己的就wait,是的话就输出然后notifyAll

使用信号量+await/signal

设置3个信号量,一个线程用一个,然后a唤醒b,b唤醒c,c唤醒a

park和unpark

a unpark b, b unpark c, c unpark a;

   static Thread t1 = null, t2 = null, t3 = null;
    void show(String string, Thread thread) {
        for (int i = 0; i < 10; i++) {
            LockSupport.park();
            System.out.print(string);
            LockSupport.unpark(thread);
        }
    }

    @Test
    public void test7() throws InterruptedException {
        t1 = new Thread(() -> show("a", t2));
        t2 = new Thread(() -> show("b", t3));
        t3 = new Thread(() -> show("c", t1));
        t1.start();
        t2.start();
        t3.start();
        LockSupport.unpark(t1);
    }

java并发编程5-Java内存

JMM

Java Memory Model

  • 原子性: 保证指令不会收到线程上下文切换的影响
  • 可见性: 保证指令不会受到cpu缓存的影响
  • 有序性: 保证指令不会受到cpu指令并行优化的影响

可见性

java线程都有自己的高速缓存区,是内存的一个子集,下面的测试,不会停止运行,尝试使用volatile解决,当然加入synchronized罩住他也可以。System.out.println也可以

    boolean flag = true;
    @Test
    public void test8() throws InterruptedException {
        Logger logger = LoggerFactory.getLogger(ThreadTest.class);
        Thread thread = new Thread(() -> {
            while (flag) {
            }
            logger.debug("end");
        }, "t1");
        thread.start();

        Thread.sleep(1000);
        flag = false;
        logger.debug("end");
        thread.join();
    }

两阶段终止

用volatile来实现可见性,一个负责读,另一个负责写。

balking

犹豫 参见多线程实现的单例模式,双重检查锁,指令重排发生在构造函数和对内存赋值之间。

指令重排

为了提高CPU吞吐率,我们会做指令重排,下面的f2中,一旦发生指令重拍,r就可能变为0

    int num = 0;
    boolean ready = false;
    int r;

    public void f1() {
        if (ready) r = num + num;
        else r = 1;
    }

    public void f2() {
        num = 2;
        ready = true;
    }

压测工具

JCstress, 用大量的线程并发模拟

禁止指令重排

volatile 可以实现

volatile原理

写屏障

在该屏障之前的改动,都将被同步到主存当中

读屏障

保证该屏障以后的读取,都要加载主存中最新的数据

单例操作volatile

因为volatile加入了写屏障,构造方法不会被排到对内存赋值之后

happens-before

happens-before 规定了对共享变量的写操作对其他线程的读操作可见。线程解锁m前对变量的写,对于接来下对m加锁的其他线程可见,对volatile的写对其他线程的读可见,start之前对变量的写,对其可见,线程结束前对变量的写,对其他线程得知他结束后可见,线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见,对变量的默认值的写,对其他线程可见,还有屏障也能保证

java并发编程6-无锁并发

CAS

compareAndSet(prev,next);无锁,无阻塞

为什么效率高

失败的话会重新尝试,但是锁会进行上下文切换,代价大

原子整形

AtomicInteger
incrementAndGet();
getAndAdd(5);
updateAndGet(value -> value*10);

原子引用

AtomicReference 不能解决ABA问题 AtomicStampedReference 版本号机制 AtomicMarkableReference True和false

原子数组

字段更新器

可以保护类的成员 compareAndSet(obj,expect,update);

原子累加器

和原子整形一样,但是只支持累加并且效率更高

缓存行伪共享

缓存中的数据是按照行分的,要失效就一起失效 有数据a和b,他们被分到了一个行中,线程1改变a导致线程2的行失效,线程2改变b导致线程1的行失效,这就是伪共享 注解sum.misc.Contended , 可以在内存中加入空白,不出现伪共享

longadder

累加单元,和concurrentHashMap一样,使用分段的机制,提高并行度,用一个数组来表示累加,数组元素的和才是真正的累加值,orn

Unsafe

获得Unsafe ,他是单例且private

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
CAS
class Teacher{
  volatile int id;
  volatile String name;
}
// 1. 获得域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
Teacher t = new Teacher();
// 执行cas
unsafe.compareAndSwapInt(t,idOffset,0,1);

自己实现AutomicInteger

class MyAtomicInteger{
  private volatile int value;
  private static final long valueOffset;
  static final Unsafe UNSAFE;
  static {
    // 获得UNSAFE
  }
  public int getValue(){
    return value;
  }
  public void increment(amount){
    while(true){
      int prev = this.value;
      int next = prev+amount;
      UNSAFE.compareAndSwapInt(this,valueOffset,prev,next);
    }
  }

}

java并发编程7-不可变设计

不可变就是线程安全

如String

拷贝构造函数

之间赋值

char[]构造

拷贝一份(保护性拷贝)

子串

如果下标起点和母串起点相同,则之间引用过去,否则保护性拷贝(不明白为啥不共用)

享元模式

最小化内存的使用,共用内存

包装类

valueOf, 比如Long如果在-128到127之间,就使用一个cache数组,又如String串池,BigDecimal和BigInteger的某些数组

保护

千万要注意这些类的函数组合起来操作就不一定安全了,需要用原子引用类来保护

数据库连接池
package com.wsx;


import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicIntegerArray;

@Data
public class ConcurrentPool {
    private final Logger logger = (Logger) LoggerFactory.getLogger(Connection.class);
    private final int poolSize;
    private Connection[] connections;
    private AtomicIntegerArray states;

    public ConcurrentPool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new Connection();
        }
    }

    public Connection borrow() {
        while (true) {
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        logger.debug("return {}",i);
                        return connections[i];
                    }
                }
            }
            synchronized (this) {
                try {
                    logger.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void free(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (conn == connections[i]) {
                states.set(i, 0);
                synchronized (this) {
                    logger.debug("notifyAll...");
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

改进

动态扩容,可用性检测,等待超时处理,分布式hash

final原理

final会给变量后面加入写屏障,注意第一步是分配空间值默认为0,然后才赋予初值,写屏障保证其他对象看到他的值是赋值以后的而不是默认值 在读的时候,如果不用final用的是getstatic,否则的话如果小就复制到栈中,大就放到常量池中。

无状态

例如不要为servlet设置成员变量,这时候他就成了无状态对象,这就是线程安全的

java并发编程8-自定义线程池

自定义线程池

把main看作任务的生产者,把线程看作任务的消费者,这时候模型就建立出来了 于是我们需要一个缓冲区,采取消费正生产者模式,然后让消费者不断消费,并在适当的时候创建新的消费者,如果所有任务都做完了,就取消消费者

package com.wsx;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class TestThreadPool {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(ThreadPool.class);
        ThreadPool threadPool = new ThreadPool(3, 10, 10);
        for (int i = 0; i < 50; i++) {
            int finalI = i;
            threadPool.execute(() -> {
                logger.debug("{}", finalI);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

class ThreadPool {
    // 线程安全阻塞队列
    private final BlockingQueue<Runnable> blockingQueue;
    // 线程安全
    private final AtomicInteger runingSize = new AtomicInteger(0);
    // 线程安全final
    private final int maxSize;
    // 线程安全final
    private final long timeout;

    public ThreadPool(int maxSize, long timeout, int queueCapcity) {
        this.maxSize = maxSize;
        this.timeout = timeout;
        this.blockingQueue = new BlockingQueue<>(queueCapcity);
    }

    public void execute(Runnable task) {
        for (int old = runingSize.get(); old != maxSize; old = runingSize.get()) {
            if (runingSize.compareAndSet(old, old + 1)) {
                new Thread(() -> threadRun(task)).start();
                return;
            }
        }
        blockingQueue.put(task);
    }

    public void threadRun(Runnable task) {
        for (; task != null; task = blockingQueue.takeNanos(timeout)) {
            try {
                task.run();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 线程退出,当前线程数量降低1
        runingSize.decrementAndGet();
    }
}


class BlockingQueue<T> {
    private final Deque<T> queue = new ArrayDeque<>();
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition full = lock.newCondition();
    private final Condition empty = lock.newCondition();
    private final int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    // 带超时的等待
    public T takeNanos(long timeout) {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    if (timeout <= 0) return null;
                    // 返回剩余时间
                    timeout = empty.awaitNanos(timeout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            full.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 超时等待
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    empty.await(); // 等待空
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            full.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    public void put(T element) {
        lock.lock();
        try {
            while (queue.size() == capcity) {
                try {
                    full.await(); // 等待空
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(element);
            empty.signal();
        } finally {
            lock.unlock();
        }
    }
}

策略模式 当队列满了的时候, 死等,超时等待,让调用者放弃执行,让调用者抛出异常,让调用者自己执行 可以用函数式编程实现

java并发编程9-JDK线程池

JDK的线程池

线程池状态,RUNNING,SHUTDOWN(不会再接受新任务了),STOP(立刻停止),TIDYING(任务执行完毕,即将TERMINATED),TERMINATED

构造函数
public ThreadPollExecutor(int corePoolsize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • 核心线程数量
  • 最大线程数量
  • 就急线程生存时间
  • 时间单位
  • 阻塞队列
  • 线程工厂: 给线程起个名字
  • 拒绝策略
拒绝策略
  • AbortPolicy 让调用者抛出异常
  • CallerRunsPolicy 让调用者运行任务
  • DiscardPolicy 放弃本次任务
  • DIcardOldestPolicy 放弃队列中最先进入的任务
  • Dubbo 抛出异常并记录线程栈信息
  • Netty 创建新的线程来执行
  • ActiveMQ 等待超时
  • PinPoint 拒绝策略链, 比如先用方法A,如果失败就要方法B,...
newFixedThreadPool

固定大小的线程池 阻塞队列无界,没有就急线程,nThreads个核心线程, 是非守护线程 当然我们也可以自己创建线程工厂,自己给线程取名字

 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
newCachedThraedPool

不固定大小的线程池 阻塞队列无界,没有核心线程,全是救急线程,但不是无限个,活60秒

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
SynchronousQueue

如果没有人取出东西,放入操作会被阻塞, 如果没有人放入东西,同理拿出会被阻塞,如果有多个同时拿,这时候就像栈一样,后来的人,会先拿到东西

    void test10_f1(SynchronousQueue<Integer> integers, String string) throws InterruptedException {
        Thread.sleep(200);
        new Thread(() -> {
            try {
                logger.debug("begin");
                integers.put(1);
                logger.debug("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, string).start();
    }

    void test10_f2(SynchronousQueue<Integer> integers, String string) throws InterruptedException {
        Thread.sleep(200);
        new Thread(() -> {
            try {
                logger.debug("begin");
                integers.take();
                logger.debug("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, string).start();
    }

    @Test
    public void test10() throws InterruptedException {
        SynchronousQueue<Integer> integers = new SynchronousQueue<>();
        test10_f1(integers, "1");
        test10_f1(integers, "2");
        test10_f1(integers, "3");
        test10_f1(integers, "4");
        test10_f2(integers, "5");
        test10_f2(integers, "6");
        test10_f2(integers, "7");
        test10_f2(integers, "8");
        test10_f2(integers, "a");
        test10_f2(integers, "b");
        test10_f2(integers, "c");
        test10_f2(integers, "d");
        test10_f1(integers, "e");
        test10_f1(integers, "f");
        test10_f1(integers, "g");
        test10_f1(integers, "h");
    }

下面是输出, 可以看到,1234按顺序进入,4321按顺序出来

16:33:54.391 [1] DEBUG com.wsx.test.ThreadTest - begin
16:33:54.591 [2] DEBUG com.wsx.test.ThreadTest - begin
16:33:54.792 [3] DEBUG com.wsx.test.ThreadTest - begin
16:33:54.996 [4] DEBUG com.wsx.test.ThreadTest - begin
16:33:55.202 [5] DEBUG com.wsx.test.ThreadTest - begin
16:33:55.202 [5] DEBUG com.wsx.test.ThreadTest - end
16:33:55.202 [4] DEBUG com.wsx.test.ThreadTest - end
16:33:55.407 [6] DEBUG com.wsx.test.ThreadTest - begin
16:33:55.409 [6] DEBUG com.wsx.test.ThreadTest - end
16:33:55.409 [3] DEBUG com.wsx.test.ThreadTest - end
16:33:55.609 [7] DEBUG com.wsx.test.ThreadTest - begin
16:33:55.609 [2] DEBUG com.wsx.test.ThreadTest - end
16:33:55.609 [7] DEBUG com.wsx.test.ThreadTest - end
16:33:55.813 [8] DEBUG com.wsx.test.ThreadTest - begin
16:33:55.814 [8] DEBUG com.wsx.test.ThreadTest - end
16:33:55.814 [1] DEBUG com.wsx.test.ThreadTest - end
16:33:56.017 [a] DEBUG com.wsx.test.ThreadTest - begin
16:33:56.221 [b] DEBUG com.wsx.test.ThreadTest - begin
16:33:56.425 [c] DEBUG com.wsx.test.ThreadTest - begin
16:33:56.630 [d] DEBUG com.wsx.test.ThreadTest - begin
16:33:56.835 [e] DEBUG com.wsx.test.ThreadTest - begin
16:33:56.836 [e] DEBUG com.wsx.test.ThreadTest - end
16:33:56.836 [d] DEBUG com.wsx.test.ThreadTest - end
16:33:57.038 [f] DEBUG com.wsx.test.ThreadTest - begin
16:33:57.039 [f] DEBUG com.wsx.test.ThreadTest - end
16:33:57.039 [c] DEBUG com.wsx.test.ThreadTest - end
16:33:57.244 [g] DEBUG com.wsx.test.ThreadTest - begin
16:33:57.244 [g] DEBUG com.wsx.test.ThreadTest - end
16:33:57.244 [b] DEBUG com.wsx.test.ThreadTest - end
16:33:57.448 [h] DEBUG com.wsx.test.ThreadTest - begin
16:33:57.449 [h] DEBUG com.wsx.test.ThreadTest - end
16:33:57.449 [a] DEBUG com.wsx.test.ThreadTest - end
newSingleThreadExecutor

1个核心线程,0个救急线程,使用无界队列,于是任务可以无数个

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

1个线程的线程池能叫池吗?我干嘛不自己用? 实际上我们自己创建的话如果碰到一些错误的任务,可能线程就退出了,这里不好处理,但是线程池在该线程退出以后会帮我们重新创建线程 FinalizableDelegatedExecutorService 是一个装饰者模式,只暴露部分接口,避免后期被修改容量

TimerTask

这个不重要,他很差,他是串行执行的,如果前面的太耗时会导致后面的被推迟,如果前面发生异常,后面的不会执行

ScheduledExecutorService

用法和TimerTask很像,但是他不会出现上面的异常影响后续任务的情况

ScheduledExecutorService.scheduleAtFixedTate()

在初始延迟以后,能够在单位时间内被反复执行

ScheduledExecutorService.scheduleWithFixedDelay()

在初始延迟以后,反复执行的两个任务之间隔固定时间

函数
submit

用future来返回,future.get();

invokeAll(tasks)

提交tasks中的所有任务

invokeAll(tasks,timeout,unit)

带一个超时时间

invokeAny

谁最先执行完就返回谁,其他的就不管了

shutdown

无阻塞,不会接受新提交的任务,但已提交的任务后执行完。

shutdownNow

打断所有的线程,并返回队列中的任务,

isShutdown

只要不是running, 就返回true

isTerminated

只有TREMINATED返回真

awaitTermination

就一直等,等到超时或者线程结束

正确处理异常

如果执行过程中没有异常,future.get()正常返回,如果出现异常,future.get()会抛出异常

Fork/Join

fork能创建新的线程来执行,join会阻塞,这就实现了并行,下面是100的阶乘模998244353

    Logger logger = LoggerFactory.getLogger(RecursiveTaskTest.class);

    @Test
    public void test() {
        class Task extends RecursiveTask<Integer> {
            private int begin;
            private int end;
            private int mod;

            Task(int begin, int end, int mod) {
                this.begin = begin;
                this.end = end;
                this.mod = mod;
            }

            @Override
            protected Integer compute() {
                if (begin == end) return begin;
                int mid = begin + end >> 1;
                Task task1 = new Task(begin, mid, mod);
                Task task2 = new Task(mid + 1, end, mod);
                task1.fork();
                task2.fork();
                return Math.toIntExact(1L * task1.join() * task2.join() % mod);
            }
        }

        ForkJoinPool forkJoinPool = new ForkJoinPool(3);
        logger.debug("{}", forkJoinPool.invoke(new Task(1, 100, 998244353)));

    }

java并发编程10-异步模式

异步模式-工作线程

线程不足导致饥饿

有两个线程A,B,任务有两种,上菜和做菜,显然上菜要等待做菜,如果AB都在执行上菜,就没有更多的线程做菜了,这就导致了AB在死等,注意这不是死锁, 所以不同的任务类型应该用不同的线程池

创建多少线程

过小导致CPU不充分利用,过大导致上下文切换占用更多内存

CPU密集型运算

CPU核数+1个线程最好,+1是当某个线程由于缺页中断导致暂停的时候,额外的线程就能顶上去

IO密集型运算

核数* 期望CPU利用率 * (CPU计算时间+等待时间) / CPU计算时间

java并发编程11-JUC

JUC

AQS

AbstractQueuedSynchronizer 是阻塞式的锁和相关的同步器工具的框架

ReentrantLock
如何重入

用一个变量记录重入了多少次

NonfairSync

####### lock cas ,成功就吧ouner改为自己,否则acquire,把自己放进链表中 ####### acquire tryacquire,成功就结束,失败的话还会尝试几次,然后才park,并为前驱标记,让前驱记得唤醒自己,如果曾经被打断的话,会被忽略,再次进入aqs队列,得到锁以后会打断自己来再次重新打断

####### unlock 调用release ####### release 如果成功,unpark唤醒后继,为什么是非公平呢?因为被唤醒以后,可能会有不在链表中线程来跟自己竞争,所以这不公平 ####### acquireInterruptibly 不忽略打断,抛出异常即可

FairSync

区别再也tryAccquire,如果c=0,即没人占用锁,他还会去检测AQS队列是否为空,其实就是看一下链表队列首部是否为自己,或者链表队列是否为空

Condition

条件变量又是一个链表,当我们调用await的时候,当前线程的节点会被放进其中,然后把节点状态设为CONDITION, ####### fullrelease 拿到锁的重数,然后一次性释放,在唤醒后面的节点,然后park自己 ####### signal 当调用的时候讲条件变量的链表中第一个元素取出并加入到锁的等待链表中。

ReentrantReadWriteLock
  • ReentrantReadWriteLock rw = new ReentrantReadWriteLock()
  • rw.readLock();
  • rw.writeLock();
锁升级

下面的代码会写锁永久等待

rw.readLock().lock();
rw.writeLock().lock();
锁降级

你可以把写锁转化为读锁

// 获得写锁
rw.writeLock().lock();

// 锁降级
rw.readLock().lock();
rw.writeLock().unlock();

// 获得读锁
rw.readLock().unlock();
缓存问题

我们可以把对数据库的某些操作缓存到内存中,来降低数据库的压力 ####### query 先在缓存中找,找不到就加锁进数据库找,然后更新缓存,可以考虑双重检查锁 ####### update 先更新数据库,后清除缓存 ####### 缓存更新策略 ######## 先删缓存,后更新数据库 A删了缓存,准备更新,结果B来了,B一查缓存没用,去数据找数据,就找到了旧值。 ######## 先更新数据库,后删缓存 A更新数据库的时候,B去查了缓存的旧 ####### 保证强一致性 query查缓存套读锁,查数据库更新缓存加写锁 update直接加写锁 ####### 没有考虑到的问题

  • 上面的操作时候读多写少
  • 没有考虑缓存容量
  • 没有考虑缓存过期
  • 只适合单机
  • 并发度还是太低, 可以降低锁粒度
读写锁原理

写锁就是简单加锁,注意照顾读锁的情况就可以了 源码太复杂了,我说不清了,留个坑吧

StampedLock

乐观读,每个操作都可以返回一个戳,解锁的时候可以吧戳还回去,如果这中间有人进行了修改,会返回失败,否则成功

  • stamp = tryOptimisticRead() 乐观读
  • validate(stamp) 校验是否被修改
  • stamp = readLock() 读锁
  • stamp = writeLock() 写锁
  • unlockRead(stamp) 释放
  • unlockWriteLock() 释放 不支持条件变量,不支持重入
Semaphore

acquire、release,和PV有点像

应用

可以限流,在高峰区让线程阻塞,比如数据库连接池,

原理

state存信号量的值 acquire 用cas,如果不成功但是位置还够就一直尝试,如果位置不够的话就吧当前线程节点加入AQS队列 release依然是先cas,失败就一直尝试,绝对会成功,成功以后,依然是改状态,然后唤醒后面的线程,

CountdownLatch

可以用来同步,等待n个线程countDown以后,await的线程才会开始运行

原理

维护一个值,每当一个线程执行完成,就让他减少1,当减少为0的时候唤醒await的线程

为什么不用join?

在线程池中,线程都不带停下来的,join没用

应用1

在游戏中,每个玩家都自己加载自己的数据,当他们都加载完成的时候,游戏才能开始, 我们设置10的倒计时,当10个玩家执行完的时候,让他们各自调用countdount,然后就能唤醒游戏开始线程

应用2

在微服务中,我们可能需要请求多个信息,当信息都请求完成才能返回,如果串行,效率太低了,我们考虑并发,这里也是个倒计时

返回结果?

如果线程需要返回结果,还是用fature更为合适,CountdownLatch不太适合

cyclicbarrier

CountdownLatch 不能用多次,要多次用的话,只能反复创建才行。 await()即为CountdownLatch的countDown cyclicbarrier 构造的时候可以传进一个Runnable,当信号值降低为0的时候,运行Runnable,然后信号量再次赋值为n达到重用的效果 千万要注意nthreads和线程数要相等,不要搞*操作,不是说不行,是不太好。

java并发编程12-集合的线程安全类

集合的线程安全类

遗留的线程安全类

Hashtable,Vector直接把同步加到方法上

修饰的安全集合

装饰器模式,Syncronize*

JUC安全集合
Blocking型

大部分实现基于锁并提供阻塞的方法

CopyOnWrite

在修改的时候会拷贝一份

Concurrent

使用CAS优化,使用多个锁,但是是弱一致性,如迭代的时候得到的内容是旧的,求大小未必100%准确,读取也是弱一致性

ConcurrentHashMap

细粒度锁

LongAdder value = concurrentHashMap.computeIfAbsent(word,(key)->new LongAdder());
value.increment();
HashMap
并发死链

在jdk7中链表是头插法,插入16,35,1,得到了1->35->16 线程A准备扩容 e 1->35->16->null , next 35->16->null,然后被中断 线程B扩容完成, 导致链表成了 head->35->1->null, 然后中断 线程A继续扩容 e 1->null, next 35->1->null, 把e插入到next新的位置,得到了head->1->35->1-> 继续扩容 e = 35->1-> next = 1->35-> ,把e插入,得到了head->35->1->35, 这里已经死循环了

丢失数据

jdk8扩容会丢失数据

ConcurrentHashMap 源码

ForwardingNode, 当扩容的时候,我们搬运一次就把老位置上连接ForwardingNode, 当查询来临的时候,就会知道去新的table里面找了, TreeBin, 是红黑树来替换链表,添加值优先考虑扩容而不是转化为红黑树 ???怎么不讲了??

Java并发编程13-并发总结

Java并发

  • Tread 创建线程
  • Runnable 创建线程
  • Callable+Future创建线程
  • synchronized 加锁
  • wait/notify 释放锁并进入阻塞队列
  • park/unpark 类似上
  • ReentrantLock 重入锁
  • await/signal 信号量
  • volatile
  • happens-before
  • CAS
  • ThreadPollExecutor
  • Fork/join
  • AQS
  • ReentrantReadWriteLock
  • StampedLock
  • CountdownLatch
  • cyclicbarrier
  • CopyOnWrite
  • ConcurrentHashMap

JVM

understanding the JVM - 走进Java

what's that

这是学习《深入理解Java虚拟机》周志明 著. 的笔记

Java 的优点

结构严谨、面向对象、脱平台、相对安全的内存管理和访问机制、热点代码检测和运行时编译及优化、拥有完善的应用程序接口及第三方类库......

JDK,JRE,Java SE API 的区别

图片来源于《深入理解Java虚拟机》

Java平台

Java Card: 支持一些Java小程序运行在校内次设备(智能卡)上的平台 Java ME: 支持Java程序运行在移动终端上的平台,对Java API所精简 Java SE: 支持面向桌面级应用的平台,有完整的Java核心API Java EE:支持多层架构的企业应用的平台,出Java核心API外还做了大量补充

Sun HotSpot VM

这是Sun JDK和OpenJDK中所带的虚拟机

understanding the JVM - Java内存区域于内存溢出异常

Java运行时的数据区域

方法区、虚拟机栈、本地方法栈、堆、程序计数器

####### 程序计数器 线程私有 为了支持多线程,Java虚拟机为每个线程设置独立的程序计数器,各条线程之间计数器互不影响,独立储存。 如果线程执行的是Java方法,计数器指向正在执行的字节码指令地址,如果线程执行的是Native方法,这个计数器则为空 程序计数器区域是唯一一个没有任何OutOfMemoryError情况的区域。

####### Java虚拟机栈 线程私有 每个方法在执行的同时都会向虚拟机栈申请一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息,每个方法的调用直到执行完成,对应一个栈帧在虚拟机栈中入栈到出栈的过程,局部变量表存放了方法执行过程中的所有局部变量,编译期间就确定了。所以,这个栈帧的大小是完全固定的。 虚拟机栈会有StackOverflowError和OutOfMemoryError,前者在固定长度的虚拟机栈中出现,后者在变长虚拟机栈中出现。这要看虚拟机是哪一种虚拟机。

####### 本地方法栈 类似于Java方法使用Java虚拟机的栈,这一区域是本地方法使用的栈。 有StackOverflowError和OutOfMemoryError。

####### Java堆 线程共享 此内存唯一目的是存放对象实例,Java虚拟机规范中描述到:所有对象实例以及数组都要在堆上分配。但是随着JIT编译器等技术等发展,所有对象都分配在堆上也渐渐不那么绝对了。 Java堆允许不连续 Java堆有OutOfMemoryError

####### 方法区 线程共享 储存虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据 可以选择不进行垃圾回收,但这不明智 有OutOfMemoryError

####### 运行时常量池 是方法区的一部分 允许在程序运行时创建常量 有OutOfMemoryError

####### 直接内存 不是虚拟机的一部分,在NIO类中,允许Native函数库向操作系统申请内存,提供一些方式访问,使得在一些场景提高性能, 有OutOfMemoryError

HotSpot VM

####### 对象的创建 当虚拟机碰到new的时候,会先检查类是否被加载、解析、初始化,如果没有,则先执行相应的加载过程,当类检查通过以后,会为对象分配内存,这个内存大小是完全确定的,虚拟机会从虚拟机堆中分配这块内存并将这块内存初始化为0,然后执行init方法。 因为Java需要支持多线程,所以这里实际需要同步处理,还有一种解决方案叫做TLAB(Thread Local Allocation Buffer)预先为每个线程分配一小块内存,就不会受到多线程影响,当TLAB用完以后,需要分配新的TLAB,这时才需要同步锁定,在TLAB分配时即可提前初始化这块内存为0,当然也可以不提前。

####### 对象的内存布局 内存中储存的布局可以分为3块区域: 对象头、实例数据和对齐填充。 对象头分为两部分,第一部分储存了哈希码、GC分代年龄、锁状态、线程持有锁、偏向线程ID等等这部分在32位机器上为32bit,在64位机器上为64bit,第二部分可有可无,储存类型指针,指向类元数据。另外对于Java数组,就还有第三部分用于记录数组长度。 对齐填充就是为了让对象大小变成8字节的整数倍

####### 对象的访问定位 Java程序通过栈上的reference数据来操作堆上的具体对象,主流的对象访问方式有两种,第一种是句柄访问,Java堆会划分出一块内存用作句柄池,reference储存句柄地址,句柄储存对象实例和类型各自的具体地址;第二种是直接访问,这种情况Java对象的布局中就要考虑储存类型数据,reference就储存对象的直接地址。前者在垃圾收集时移动时快,后者访问速度快。

understanding the JVM - 垃圾收集器与内存分配策略

如何判断对象已死

####### 引用计数算法 为对象添加引用计数器,每当有一个地方引用他的时候计数器的值+1,当引用失效的时候计数器的值-1,当任何时刻计数器为0的对象就是不可能再被使用了。此算法效率高,但是无法解决相互引用的问题。 ####### 可达性分析算法 利用有向图可达性表示对象生死,作为GC Roots的对象有虚拟机栈(本地变量表)中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象。若不能从根达到的对象,则对象死亡。 ####### 引用分类 强引用: 类似“Object obj = new Object()”的引用 软引用: 有用但并非必需的对象,在系统将要发生内存溢出异常前,会对这些对象进行第二次回收。 弱引用: 弱引用只能活到下一次垃圾回收之前。 虚引用: 完全不会影响该对象是否被回收,用于在该对象被回收时收到一个系统消息。 ####### 生存还是死亡 当可达性分析算法中某个对象不可达时,他会进入缓刑阶段,如果他覆盖finalize()方法且finalize()方法没有被调用过,他就会进入F-Queue队列中,虚拟机会在一个很慢的线程Finalizer中执行他。在finalize()中对象可以通过把自己赋给某个活着的类变量或对象的成员变量来拯救自己,拯救了自己的对象不会被回收,其他的对象都会被回收掉。 ####### 回收方法区 Java虚拟机规范中可以不要求实现该部分。 回收内容包括两部分,一是废弃常量,即当前没有被引用的常量,二是无用的类,这需要满足3个条件: 1.该类的实例都被回收,2.加载该类的ClassLoader被回收,3.该类对应的java.lang.Class对象没有被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

####### 标记-清除算法 统一标记然后统一回收,这样效率不高并产生了很多内存碎片 ####### 复制算法 把内存分为相同的两块,使用其中一块,当使用完后,将有用的内存移到另外一块上,然后回收整块内存,这样效率很高,但是内存利用率低,他的一种改进是把内存分三块,一块大,两块小,每次使用一块大+一块小,整理时把有用的部分移动到另一块小的,然后清理之前的两块。这个算法在新生代中表现非常出色。但是我们总会碰到整理的时候放不下的情况,这时我们通过内存担保机制,为多余的对象分配内存,并直接进入老年代。 ####### 标记-整理算法 在老生代中一般不能使用复制算法,因为他们存活率太高了。我们可以改进标记-清除算法,回收的时候顺便将有用的对象向内存的一端移动,这样就避免了内存碎片的产生。 ####### 分代收集算法 把Java堆分为新生代和老生代,根据个个年代的特点选择适当的方法。

HotSpot的GC

####### 枚举根节点 根节点很多,有的应用仅方法区就有数百兆,逐个寻找引用会很花费时间,这里使用OopMap来直接记录下一些引用的位置。就省去了寻找的过程,变成了直接定位。 ####### 安全点 GC的时候,Java的其他线程必须处于安全的位置,以保证引用链不发生变化。虚拟机会适当标记某些安全点,GC的时候其他线程就在这些安全点上。为了保证这一点,有两种中断方式,抢先式中断和主动式中断,抢先式中断指的是首先中断全部线程,如果发现某些线程不在安全点,则让其恢复,运行到安全点在停下来。主动式中断是当GC需要中断时,设置一个标志,让其他线程主动轮流访问,发现标志为真的时候,就主动中断,这里只需要保证进程在安全点访问标志即可。 ####### 安全区域 有些Sleep或者Blocked状态的进程无法主动响应JVM的中断请求,运行到安全的地方。我们引入安全区域,在这个区域内,每个地方都是安全点,当线程执行到安全区域时,标记自己进入了安全区域,这段时间JVM可以GC,不用管这些线程,当这些线程离开安全区域的时候,线程检查JVM是否完成GC即可。

垃圾收集器

####### serial收集器 单线程收集,GC时暂停所有用户进程,新生代采取复制算法,老生代采取标记-整理算法 ####### ParNew收集器 GC时暂停所有用户进程,新生代采取多线程复制算法,老生代采取单线程标记-整理算法 ####### Parallel Scavenge收集器 这个收集器和ParNew收集器很像,但是Parallel Scavenge收集器更加关注两个信息,停顿时间和吞吐量,停顿时间指的是GC造成的停顿时间,吞吐量指的是单位时间内不停顿的比率。Parallel Scavenge还支持自动调参。 ####### CMS收集器 这个收集器强调服务的响应速度,希望停顿时间最短。 他的过程分四个步骤: 初始标记、并发标记、重新标记、并发清除。初始标记的时候要暂停所有用户进程,然后标记GC ROOT直接关联的对象,这个步骤很快就能结束,然后就可以启动用户进程和GC ROOT Tracing一起并发执行。在并发期间会导致可达性链发生变化,这需要第三个步骤:重新标记,这也会暂停用户进程。最后并发清除即可。 CMS收集器清理的时候采用的是标记-清理算法 ####### G1收集器 G1收集器要先把Java堆分成多个大小相等的独立区域,新生代和老生代都是一部分独立区域,为了避免全盘扫描,对每一个独立区域都引入Remembered Set来记录引用关系,这可以加速GC。G1步骤和CMS一样,但是Remembered Set的存在,让重新标记可以并行完成。

内存分配与回收策略

对象优先分配在Eden中,Eden就是堆中的大块,若不能分,则进行新生代都GC 大对象直接进入老年代 对象每存活于一次新生代GC,则年龄增长一岁,达到15岁的时候便进入了老年代。 如果所有年龄相同的对象所占空间超过了一半,则此年龄以上的对象全部进入老年代。 在新生代GC的时候会碰到空间不够的情况,这时需要空间分配担保机制,根据概率论设置阈值,在新生代GC的时候根据以往晋升到老年代的对象内存大小的均值和方差计算阈值,若老年代剩余空间小于阈值,则会先进行老年代GC腾出空间,若老年代剩余空间大于阈值,则直接进行新生代GC,这时会有非常小的概率,GC失败,然后出发老年代GC。这里和TCP协议中动态滑动窗口大小协议有点类似。

understanding the JVM - Java内存模型与线程

硬件间速度的差距

因为计算机各种硬件之间速度的差距实在是太大了,这严重地影响了计算机的整体效率,很多时候软件并不能够充分地利用计算机的资源,让处理器去等待内存,一种解决方案就是在内存和处理器之间引入一个缓存,来尽量减轻这个速度的差距。在多处理器系统中,往往对应着多个缓存。

缓存一致性

往往这些缓存都储存着和内存一样的数据,他们互为拷贝,我们必须保证他们的数据是同步修改的。这有很多种协议来维护。

乱序执行

为了更好的利用处理器的运算单元,处理器可能会对输入的代码片段进行乱序执行优化,Java虚拟机也是如此。

Java内存模型

Java内存模型中有两种内存,第一种是一个主内存,第二种是多个线程工作内存,线程私有。

内存间的互相操作

Java内存模型有8种原子操作 lock:作用与主内存中的变量,让其变为线程独占 unlock:和lock相反 read:把主内存中的变量传输到工作内存,准备load load:把read的值放入线程工作内存的变量副本中 use:把线程工作内存中的值拿到执行引擎中 assign:从执行引擎中获得值写入工作内存 store:把工作内存中的变量传输到主内存,准备write write:把store得到的值写入主内存。 这些操作的相互执行关系很复杂,但都能推导出,这里不赘述 long和double是64位数据,Java内存模型不强制但推荐把他们也实现为原子操作

volatile型变量

volatile类型有两种特点,第一是保证对所有线程的可见行,即所有线程使用其前都要在工作内存中刷新他的值,但是这些操作并非原子性,所以不能保证并发安全。第二是禁止语义重排。他前面的代码不能排到他后面去,他后面的代码不能重排到他前面。

Java线程调度

Java有10种优先级,但是操作系统却不一定是10种,若<10则导致Java线程某些级别无差距,若>10则导致有些系统线程优先级无法使用。

Java线程状态

新建、运行、无限期等待(不能主动苏醒,能被唤醒)、期限等待、阻塞、结束

understanding the JVM - Java线程安全与锁优化

Java共享数据的分类
  • 不可变: 不可变数据是绝对线程安全的
  • 绝对线程安全: “不管运行时环境如何,调用者都不需要任何额外的同步措施”
  • 相对线程安全: 对一个对象单独对操作是线程安全对
  • 线程兼容: 本身并非线程安全,但我们可以在调用端使用同步手段来确保在并发环境中可以安全得使用
  • 线程对立: 对象在调用端无论使用何种同步手段,都无法确保安全
线程安全的实现方法
  • 互斥同步: 使用互斥量
  • 非阻塞同步 : 使用原子操作
锁优化
  • 自旋锁与自适应自旋锁: 使用多个忙循环而不是挂起,当忙循环超过某个固定阈值的时候挂起,自适应指得是动态选择阈值
  • 锁清除: 消除不必要的锁
  • 锁粗化: 扩大锁的范围,避免在循环中频繁加锁
  • 轻量级锁: 使用CAS操作,避免互斥加锁,若CAS操作失败,则使用互斥加锁
  • 偏向锁: 让第一个使用对象对线程持有,在第二个线程到来前,不进行任何同步操作。

understanding the JVM - 早期(编译期)优化

Java 编译
  • 解析与填充符号表过程
  • 插入式注解处理器的注解处理过程
  • 分析与字节码生成过程
Java 语法糖
  • 自动拆箱装箱
  • 遍历循环
  • 条件编译 : 类似c++的宏
  • 范型与类型擦除 : Java的范性和c++范型不一样,Java是用类型擦除来实现的,然后使用类型强制转化。
拆箱陷阱

####### 先看代码

class test{
  public static void main(String[] args){
    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Integer d = 3;
    Integer e = 321;
    Integer f = 321;
    Long g = 3L;
    System.out.println(c == d);
    System.out.println(e == f);
    System.out.println(c == (a + b));
    System.out.println(c.equals(a + b));
    System.out.println(g == (a + b));
    System.out.println(g.equals(a + b));
  }
}

####### 输出

true
false
true
true
true
false

####### 解释

  • = 会导致自动拆箱和装箱
  • +,-,*,/混合运算会拆箱
  • >,<,==比较运算会拆箱
  • euqals会装箱
  • 向集合类添加数据会装箱
  • Integer类有缓存区域,储存了[-128,127],所有这些值都共享缓存区的对象指针

understanding the JVM - 晚期(运行期)优化

解释器与编译器

Java在运行的时候,他的解释器和编译器会同时工作,解释器解释运行代码,编译器有选择性地编译部分代码为机器代码,以加速Java代码的执行过程

编译器

Java编译器有两种,一种是客户端编译器,他进行简单的可靠的优化,并适时加入性能检测的逻辑,另一种是服务器编译器,他进行耗时较长的优化,甚至根据性能检测信息,加入一些不可靠的激进的优化

编译器编译的对象
  • 被多次调用的方法
  • 被多次执行的循环体

####### 方法

  • 基于采样的热点探测:虚拟机周期性的检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这就是一个热点方法,优点是简单高效,缺点是容易受到线程阻塞等其他因素的影响。
  • 基于计数器的热点探测:为每一个方法添加一个计数器,每当方法被调用,则计数器增大1,每经过一定时间(可以与gc相关)就让计数器变为原来的一半,这是一种和式增加,积式减少的策略,这个在计算机网络中的滑动窗口大小控制也有应用,当计数器超过某个阈值的时候,就让编译器来把这个方法编译成机器码即可。

####### 循环体 和上文的计数器热点探测相似,但计数器永远不会变小。若超过一个阈值,整个方法都会被编译成机器码

编译优化

####### 各语言通用优化 内联、冗余访问清除、复写传播、无用代码清除、公共子表达式消除 ####### Java编译器优化

  • 隐式异常优化: 使用Segment Fault信号的异常处理器代替数组越界异常、空指针异常、除0异常等
  • 方法内联: 由于Java基本都是虚函数,这导致方法内联不太容易实现,对于非虚函数,直接内联,对于虚函数,CHA(类型继承关系分析)会检测,当前程序的这个方法是否对应了多个版本,若没有,则进行激进优化,强行内联并预留一个逃生门,以便在新类加载的时候,抛弃此代码,使用解释器,如果CHA查出有多个版本,也会为进行一个缓存处理,保留上一次的信息,若下一次进来的版本相同,则内联可以继续使用,否则就只能回到解释器了。
逃逸分析

逃逸分析是一种分析技术,而不是直接优化代码的手段。 ####### 栈上分配 如果分析出一个对象不会被他被定义的方法以外的方法用到,那个这个对象会被分配到栈上。 ####### 同步消除 如果分析出一个对象不会被他被定义的所在线程以外的线程用到,那么这个对象的同步指令会被清除。 ####### 标量替换 如果分析出一个对象不会被外部访问,他甚至会被拆成多个基本数据类型,分配到栈上,甚至被虚拟机直接分配到物理机的高速寄存器中

Spring

spring学习1 - spring入门

学习

spring 是一个轻量级框架

他最重要的地方时AOP和IOC,他的目的是降低耦合度,减少代码量

AOP

面向切面编程,

IOC

控制反转,即将对象的创建交给spring,配置文件+注解

耦合问题

比方说我们要在B类中使用A类,就会在B类中A a=new A();然后这样就导致了B依赖A

工厂模式解决耦合

用工厂来控制A类,在B中就能 A a=factory.getA(); 这又导致了B和工厂耦合。

ioc方案

解析xml配置文件中的类,在工厂类中利用反射创建类的对象,这就降低了类的耦合度,我们想要换类的时候,只要将xml中的类名称改掉就可以了。

一站式框架

springMVC+ioc+jdbcTemplate

spring学习2-spring介绍2

Spring 模块

Spring有六大模块,测试、容器、面向切面编程、instrumentation、数据访问与集成、Web与远程调用。

  • 测试: Test
  • 容器: Beans,Core,Context,Expression,ContextSupport
  • 面向切面编程: AOP,Aspects
  • instrumentation: instrument,instrumentTomcat
  • 数据访问与集成: JDBC,Transaction,ORM,OXM,Messaging,JMS
  • Web与远程调用: Web,WebServlet,WebPortlet,WebSocket 但是Spring远不止这些

Spring配置

Spring有三种配置方式,第一是通过XML配置,第二是通过JAVA配置,第三是隐式的bean返现机制和自动装配。建议优先使用三,而后是二,最后是一

自动化装配bean

有两种方法,组件扫描和自动装配,

spring3-耦合

耦合

我们考虑一个web应用,使用三层架构: 视图层+业务层+持久层, 视图层依赖业务层,业务层依赖持久层,这是非常不好的现象,当我们的持久层需要改变的时候,整个项目都要改变,项目非常不稳定。

怎么解决

工厂!

Bean

Bean就是可重用组件

JavaBean

JavaBean不是实体类,JavaBean远大于实体类,JavaBean是Java语言编写的可重用组件

解决

使用配置文件来配置service和dao,通过读取配置文件,反射创建对象,这样程序就不会在编译器发生错误了。 考虑用一个BeanFactory来实现读取配置文件和反射 但是注意到我们实现的时候,如果每次都去创建一个新的对象,我们的BeanFactory可能会非常大,所以我们需要在工厂中用一个字典来保存对象,这就成了一个容器。

IOC

控制反转,我们不需要自己new了,让工厂给我们提供服务,这就是IOC,把对象的控制权交给工厂。

spring4-创建IOC容器

创建IOC容器

ApplicationContest

单例对象适用

  • ClassPathXmlApplicationContext 可以加载类路径下的配置文件,要求配置文件在类路径下
  • FileSystemXmlApplicationContext 可以加载任意路径下的配置文件(要有访问权限)
  • AnnotationConfigApplicationContext 读取注解创建容器
ApplicationContest什么时候创建对象
  • 当加载配置文件的时候就创建了对象
BeanFactory

多例对象适用

  • XmlBeanFactory 使用的时候才创建对象

spring5-XML配置IOC

XML配置IOC

使用默认构造函数创建Bean

在spring的配置文件中使用Bean标签, 只配置id个class属性

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="myclass" class="com.wsx.spring.Myclass"></bean>
</beans>
使用某个类中的方法创建

加入一个方法

<bean id="myfactory" factory-bean="com.wsx.spring.Myfactory"
    factory-method="function"></bean>
使用类的静态方法创建
<bean id="myfactory" class="com.wsx.spring.Myfactory"
    factory-method="function"></bean>
Bean的作用范围
  • singleton 单例(默认值)
  • prototype 多例
  • request web应用的请求范围
  • session web应用的会话范围
  • global-session 集群环境的会话范围,一个集群只有一个全局会话
Bean的生命周期
单例

当容器创建对象出生,当容器存在,对象活着,当容器销毁,对象消亡 init-method 是创建以后调用的, destory-method是销毁之前调用的的

<bean id="myclass" class="com.wsx.spring.Myclass"
    scope="singleton" init-method="init"
    destory-method="destory"></bean>
多例

当我们使用的时候spring为我们创建,当我们一直使用,对象就一直活着,对象等着被垃圾回收机制删掉

spring6-依赖注入

sprint的依赖注入

dependency injection IOC是降低程序之间的依赖关系的,我们把依赖关系交给spring维护,依赖关系的维护就叫做依赖注入 注入的类型 基本类型和Sring、 bean类型、集合类型 注入的方法 构造函数、set、注解

构造函数注入

使用constructor-arg标签

type标签

我们很容易想到

<bean id="myclass" class="com.wsx.spring.Myclass">
    <constructor-arg type="java.lang.String" value="wsx"></constructor-arg>
</bean>
index 标签

使用下标,位置从0开始

<bean id="myclass" class="com.wsx.spring.Myclass">
    <constructor-arg index="0" value="wsx"></constructor-arg>
</bean>
name 标签

使用参数的名称

<bean id="myclass" class="com.wsx.spring.Myclass">
    <constructor-arg name="name" value="wsx"></constructor-arg>
</bean>
使用ref

使用其他的bean

<bean id="myclass" class="com.wsx.spring.Myclass">
    <constructor-arg name="myobj" ref="myobj"></constructor-arg>
</bean>
set方法注入

property标签

<bean id="myclass" class="com.wsx.spring.Myclass">
    <property name="name" value="wsx"></property>
    <property name="myobj" ref="myobj"></property>
</bean>
构造函数注入和set方法注入

set注入可以有选择性地注入,构造函数强制了必要的数据

集合的注入

当我们碰到集合的时候,使用ref就不合适了,我们发现property内部还有标签

<bean id="myclass" class="com.wsx.spring.Myclass">
    <property name="mylist">
        <list>
            <value>1</value>
            <value>2</value>
            <value>3</value>
            <value>4</value>
            <value>5</value>
        </list>
    </property>
</bean>

注意上面的 我们甚至可以使用其他的例如 同理 和也可以互换

spring7-注解配置IOC

注解配置IOC

先总结一下之前的东西,曾经的XML配置,有标签id和class用于构造,有scope用来表示作用范围,有init-method和destroy-method用来表示生命周期,有property用来表示依赖注入

告知spring去扫描
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
    <context:component-scan base-package="com.wsx.spring"></context:component-scan>
</beans>
@Component

讲当前类的对象存入spring的容器,有一个value表示id,如果不写的话会让当前类名的首字母变小写作为id

@Component(value = "myclass")
public class Myclass {
    void function(){
        System.out.println("hello");
    }
}
@Component("myclass")
public class Myclass {
    void function(){
        System.out.println("hello");
    }
}
@Component
public class Myclass {
    void function(){
        System.out.println("hello");
    }
}
@Controller @Service @Repository

他们和Component的作用一样,但是分别用于表现层、业务层、持久层当然你乱用也不要紧

@Autowired

自动注入,如果容器中有唯一一个bean对象,就可以成功注入,如果一个都没有就报错,如果有多个,先用类型匹配,再用变量名字(首字母变大些)去匹配,

@Component
class Node{
    void show(){
        System.out.println("Node");
    }
}

@Component
public class Myclass {
    @Autowired
    private Node node=null;
    void nodeShow(){
        node.show();
    }
    void function(){
        System.out.println("hello");
    }
}
@Qualifier

必须和@Autowired配合使用,在Qualifier的value中写类型就可以了,注意首字母小写。

@Resource

用name表示id

@Component
class Node{
    void show(){
        System.out.println("Node");
    }
}


@Component
public class Myclass {
    @Resource(name = "node")
    private Node node=null;
    void nodeShow(){
        node.show();
    }
    void function(){
        System.out.println("hello");
    }
}
@Value

注入基本类型和string类型 $(表达式),需要有配置文件properties,详细的后面会讲

@Scope

写在类的上面, 常常取值singleton prototype

@PreDestory

指定destroy方法

@PostConstruct

写在init方法的上面

spring中的新注解
@Configuration

用于指定当前类是一个配置类,当配置类作为AnnotationConfigApplication的参数时候,可以不写,其他的配置类要写

@ComponentScan

用于指定spring在创建容器时要扫描的包

@Bean

把当前方法的返回值作为bean对象存入spring的ioc容器中 ,属性为name表示ioc容器中的键,当我们使用注解配置方法的时候,如果方法有参数,spring会去容器中寻找,和Autowired一样

@Configuration
public class MyAppConfig{
  @Bean
  public HelloService helloService(){
    return new HelloService();
  }
}
现在你可以删xml了
ApplicationContext ac = new AnnotationConfigApplication(SpringConfiguration.class);
@Import

如果我们有多个配置类的时候,有很多做法,一是在使用AnnotationConfigApplication创建对象的时候把类都加进去,二是在主配置类的@ComponentScan中加入配置类(这个类需要使用@Configuration),三是在主配置类中使用@Import直接导入

@PropertySource

还记得前面说的@Value注解吗,那里需要一个properties配置文件,这里我们在主类中使用PropertySource就可以指定properties配置文件了

@PropertySource(classpath:jdbconfig.properties)
总结

没有选择以公司为主,全xml配置复杂,全注解也不太好,所以xml+注解更方便,自己写的类用注解,导入的类用xml

spring8-spring整合junit

spring整合junit

@RunWith

我们需要切换junit的main

@ContextConfiguration

指定配置类或者配置文件

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.3.8.RELEASE</version>
</dependency>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Springconfig.class)
public class MainTest {
    @Autowired
    private Main m = null;
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:bean.xml")
public class MainTest {
    @Autowired
    private Main m = null;
}

spring9-动态代理

account案例

我们有一个转账方法: 根据名称找到账户,转出账户减钱,转入账户加钱,更新转出账户,更新转入账户,这个方法没有事务的控制,可能出现问题

案例问题

实际上我们需要维护一个和线程绑定的数据库连接,我们做一个工具类,让其支持回滚,于是我们在上诉案例中可以使用trycatch,一旦碰到问题,在catch中回滚即可,这个可以解决问题,但是太复杂了。

动态代理

字节码随用随创建,随用随加载,不修改远么的基础上对方法增强, 有两种,基于接口的动态代理和基于类的动态代理

基于接口的动态代理

Proxy.newProxyInstance 参数1 类加载器: 固定写法 是被代理对象的类加载器 参数2 字节码数组: 固定写法 让代理对象和被代理对象有相同的方法 &emps; 参数3 增强的代码 ,是一个匿名内部类 ####### 内部类 实现一个invoke(proxy,method,args); method.invoke(producer,args); 如果被代理的类没有实现任何接口,则此方法无用

动态代理的另一种实现方式

cglib

基于子类的动态代理

Enhancer.create(class,callback); &emps; 要求类不能是最终类 class是被代理对象的字节码, 第二个参数是MethodInterceptor是一个内部匿名类

动态代理的作用

&emps; 用动态代理增强connect,让其加回连接池

spring10-配置AOP

spring中的AOP

连接点,被拦截到的点,在spring中指的是方法 切入点,被增强的连接点 通知 在调用方法前的是前置通知,在后面的是后置通知,在catch中的是异常通知,在final中的是最终通知,整个invoke方法就是环绕通知 Target 被代理的对象

proxy 代理对象 织入 把被代理对象增强的过程 切面 通知+切入点

spring中的AOP要明确的事情

编写核心代码,抽取公共代码制作为通知,在配置文件中声明切入点和通知之间的关系

spring中AOP的配置
XML配置AOP

aop:config 表明aop配置开始, aop:aspect 切面配置开始 id是切面的名字,ref是通知类bean aop:before 前置通知 method用于指定中的方法 pointcut是切入点

<bean id='logger' class="com.wsx.utils.logger"></bean>
<aop:config>
    <aop:aspect id="logAdvice" ref="logger">
        <aop:before method="pringLog" porint="execution(public void com.wsx.wsx.wsx.saveAccount())"></aop:before>
    </aop:aspect>
</aop:config>

####### 通配写法 访问修饰符可以省略 如public 返回值可以是通配符,表示任意返回值 包名可以是通配符表示任意包,几个包就有几个*, 可以用..*表示当前包的所有子包 方法可以用* 参数可以用通配符,或者类型名 * *..**.*(..)

<bean id='logger' class="com.wsx.utils.logger"></bean>
<aop:config>
    <aop:aspect id="logAdvice" ref="logger">
        <aop:before method="pringLog" porint="execution(* *..*.*(..)"></aop:before>
    </aop:aspect>
</aop:config>

####### 实际开发怎么写呢 * com.wsx.wsx.wsx.*.*(..)

####### 各种通知都加进来

<bean id='logger' class="com.wsx.utils.logger"></bean>
<aop:config>
    <aop:aspect id="logAdvice" ref="logger">
        <aop:before method="before" porint="execution(public void com.wsx.wsx.wsx.saveAccount())"></aop:before>
        <aop:after-returning method="after-returning" porint="execution(public void com.wsx.wsx.wsx.saveAccount())"></aop:after-returning>
        <aop:after-throwing method="after-throwing" porint="execution(public void com.wsx.wsx.wsx.saveAccount())"></aop:after-throwing>
        <aop:after method="after" porint="execution(public void com.wsx.wsx.wsx.saveAccount())"></aop:after>
    </aop:aspect>
</aop:config>

####### 配置切点 减少代码量,写在aop:aspect外可以所有切面都可以使用(写在aspect之前),写在aop:aspect内只在内部有用

<bean id='logger' class="com.wsx.utils.logger"></bean>
<aop:config>
    <aop:aspect id="logAdvice" ref="logger">
        <aop:before method="before" pointcut-ref="pt1"></aop:before>
        <aop:after-returning method="after-returning" pointcut-ref="pt1"></aop:after-returning>
        <aop:after-throwing method="after-throwing" pointcut-ref="pt1"></aop:after-throwin>
        <aop:after method="after" pointcut-ref="pt1"></aop:after>
        <aop:pointcut id="pt1" expression="execution(public void com.wsx.wsx.wsx.saveAccount())"></aop:pointcut>
    </aop:aspect>
</aop:config>

####### 配置环绕通知 当我们配置了环绕通知以后,spring就不自动帮我们调用被代理对象了

<aop:around method="?" pointcut-ref="pt1"></aop:around>
public Object arroundPringLog(ProceedingJoinPoint pjp){
    Object rtValue = null;
    try{
        Object[] args = pjp.getArgs(); // 获得参数
        rtValue = pip.proceed(args); //调用函数
        return rtValue;
    }catch(Throwable t){
    }finally{
    }
}
注解配置AOP

####### 开启注解

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

####### @Aspect 表明当前类是一个切面类 ####### @Pointcut 切入点表达式

@pointcut("execution(public void com.wsx.wsx.wsx.saveAccount())")
private void pt1(){}

####### @Before 表明当前方法是一个前置通知, AfterReturning、AfterThrowing、After、Arount同理

@Before("pt1()")
public void f(){}

####### 注解调用顺序的问题 前置、最终、后置/异常

####### 纯注解 加在类的前面即可

@Configuration
@ComponentScan(..)
@EnableAspectJAutoProxy

spring11-Jdbctemplate

JdbcTemplate

测试写法
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate",JdbcTemplate.class);
// 保存
jt.update("insert into account(name,money)values(?,?)","eee",3333f);
// 更新
jt.update("update account set name=?,money=? where id=?","test",4567,7);
// 删除
jt.update("delete from account where id=?",8);
// 查询
List<Account> account = jt.query("select * from account where money>?",new BeanPropertyRowMapper<Account>(Account.class),1000f);
DAO中的JdbcTemplate

上面的代码实际上只能用于简单的测试,我们正确的做法应该还是使用DAO实现,注意到使用DAO实现的时候肯定要在类中创建jdbcTemplate,如果我们有多个DAO就会导致份重复的代码,这时可以让他们继承一个JdbcDaoSupport来实现,而这个类spring又恰好为我们提供了。但是只能通过xml注入,你想要用注解注入的话就只能自己写一个。

spring12-事务

spring支持的事务

似乎都是关于数据库的,可能也是我的水平问题,不知道其他的东西 大概需要实现两个,一个commit,另一个是rollback 事务是基于AOP实现的

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springTransaction</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>


        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>


    </dependencies>
</project>
package com.wsx.spring.Service;

import com.wsx.spring.Account;
import com.wsx.spring.Dao.IAccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
public class AccountService implements IAccountService {
    @Autowired
    private IAccountDao accountDao;

    public Account findAccountById(Integer accountId) {
        return accountDao.findAccountById(accountId);
    }


    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public void transfer(String sourceName, String targetName, Float money) {
        System.out.println("start transfer");
        // 1.根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        // 2.根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        // 3.转出账户减钱
        source.setMoney(source.getMoney() - money);
        // 4.转入账户加钱
        target.setMoney(target.getMoney() + money);
        // 5.更新转出账户
        accountDao.updateAccount(source);

        int i = 1 / 0;

        // 6.更新转入账户
        accountDao.updateAccount(target);
    }
}

SpringBoot

springboot

SpringBoot与Web

先在idea中选择场景 springboot已经默认将这些常见配置好了,我们只需要在配置文件中指定少量配置就可以运行起来 然后我们可以开始编写业务代码了

SpringBoot1-介绍

微服务

讲大应用拆分成多个小应用

springboot介绍

创建maven工程
导入依赖
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
写主类
package com.wsx.springbootstudy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


/**
 * @SpringBootApplication 标注一个类,说明这个是SpringBoot应用
 */
@SpringBootApplication
public class HelloWorldMainApplication {
    public static void main(String[] args) {
        // 启动应用
        SpringApplication.run(HelloWorldMainApplication.class,args);
    }
}
写Controller
package com.wsx.springbootstudy.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        return "Hello World!";
    }
}
Hello World!
部署我们的helloworld
    <build>
        <plugins>
            <!--            spring-boot打包-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

然后再maven中点击package,有如下输出

Building jar: /Users/s/Documents/untitled/target/untitled-1.0-SNAPSHOT.jar

然后点击这个jar就开始跑了 如果你想要关闭他就在终端中输入

ps -ef | grep /Users/s/Desktop/untitled-1.0-SNAPSHOT.jar

然后看左边的进程号

kill -9 pid

分析

pom

parent父项目,他管理springboot的所有依赖,又叫做springboot版本仲裁中心,以后我们导入依赖默认不需要添加版本号

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
    </parent>
spring-boot-starter-web

spring-boot-starter 是spring-boot场景启动器,他帮我们导入了web模块正常运行所依赖的组件 SpringBoot将所有的功能场景都抽取出来,做成一个starters启动器,只需要在项目中引入这些 starter,相关场景的所有依赖都会被导入进来,要什么功能就导入什么场景启动器。

主类

@SpringBootApplication ,SpringBoot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用

@SpringBootConfiguration

Spring Boot 的配置类,标注在某个类上,表示这是一个SpringBoot的配置类 ####### Configuration 配置类上来标识这个注解,配置类和配置文件差不多,用于注入,这是个spring的注解, ####### EnableAutoConfiguration 开启自动配置,SpringBoot帮我们自动配置 ######## @AutoConfigurationPackage 自动配置包 ######### @import(AutoConfigurationPackage.Registrar.class) Spring的注解@import,给容器中导入一个组件,导入的组件由AutoConfigurationPackage.Registrar.class 指定 把主配置类的所在包的所有子包的所有组件扫描到Spring容器中 ######## @import(EnableAutoConfigurationImportSelect.class) EnableAutoConfigurationImportSelect: 导入的选择性,讲所有需要导入的组件一全类名的方式返回,这些组件会被添加到容器中,最终会给容器中导入非常多的自动配置类***AutoConfiguration,就是导入场景所需要的组件。有了自动配置类,就免去了我们手动编写配置注入等功能组件的工作, SpringFactoryLoader.loadFactoryNames(EnableAutoConfiguration.class,classLoader); 从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效了,帮我们进行自动配置工作,以前我们需要自己配置的东西,自动配置类帮我们做了,都在spring-boot-autoconfigure下,见spring.factories和org.springframework.boot.autoconfigure

SpringInitial

idea中选择SpringInitial,点继续,选择Springweb,生成,然后加入下面的代码,就可以启动了

package com.wsx.springboothelloworld.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

//@Controller
//@ResponseBody // 把这个类的所以方法返回给浏览器,转化为json数据
@RestController // 这一个顶上边两个
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        return "hello world quick!";
    }
}

resources中目录结构的static保存静态资源,如css、js、images,templates保存所以的模版页面(spring boot默认jar包使用嵌入式tomcat,默认不支持jsp),但我们可以使用模版引擎(freemarker,thymeleaf), application.properties中放了springboot的默认配置文件,比如你想换web的端口

server.port=8081

SpringBoot2-配置

springboot配置

配置文件

配置文件的名字是固定的

application.properties
applicstion.yml

YAML 是一个标记语言,不是一个标记语言 ####### 标记语言 以前的配置文件大多是xml文件,yaml以数据为中心,比json、xml等更适合做配置文件 这是yml

server:
  port: 8081

这个是xml

<server>
    <port>8081</port>
</server>
yml语法

####### 基本语法 k:(空格)v 表示一对键值对 用空格锁进来控制层级关系,只要左对齐就都是一个层级的,属性和值也是大小写敏感的

server:
  port: 8081
  path: /hello

####### 值的写法 ######## 字面量: 普通的值、字符串、bool, 字符串默认不用加上双引号和单引号

s1: 'a\nb'
s2: "a\nb"

等加于下面等js

{s1: 'a\\nb',s2: 'a\nb'}

######## 对象、map 对象的写法

friends:
  lastName: zhangsan
  age: 20

行内写法

friends: {lastName: zhangsan,age: 18}

######## 数组 list set 用-表示数组中的元素

pets:
 - cat
 - dog
 - pig

行内写法

pest: [cat,dog,pig]
配置文件注入

@ConfigurationProperties 告诉springboot将本类中的所有属性和配置文件中相关的配置进行绑定, prefix = "person": 配置文件中哪个下面的所有属性一一映射 @Data 来自动生成tostring,@Component来把这个类放到容器中,@ConfigurationProperties来从配置文件注入数据

@Data
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String lastName;
    private Integer age;
    private Boolean boss;
    private Date birth;

    private Map<String, Object> maps;
    private List<Object> lists;
    private Dog dog;
}

Dog同理

@Data
@Component
public class Dog {
    private String name;
    private Integer age;
}

导入依赖

 <!--        导入配置文件处理器,配置文件进行绑定就会有提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

开始测试

@SpringBootTest
class SpringBootHelloworldApplicationTests {

    @Autowired
    Person person;

    @Test
    void contextLoads() {
        System.out.println(person);
    }

}

我们看到输出

Person(lastName=zhangsan, age=18, boss=false, birth=Tue Dec 12 00:00:00 CST 2017, maps={k1=v1, k2=v2}, lists=[lisi, zhaoliu], dog=Dog(name=dogname, age=2))

改写为properties

person.last-name=zhangsan
person.age=18
person.birth=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=v2
person.lists=a,b,c
person.dog.name=dog
person.dog.age=15
注解注入

详见@Value @Value("$(person.last-name)") 从环境变量和配置文件中获得值 @Value("#{111213}") 从表达式中获得值 @Value("true")

@PropertySource和@ImportResource

@PropertySource可以指定配置文件,还可以写数组加载多个配置文件, @ImportResource导入Spring的配置文件,让配置文件中的内容生效,即我们以前写的spring的那些东西springboot是不会识别的,必须通过ImportResource才能成功,springboot不推荐这个注解 springboot推荐全注解形式,使用@Configuration,这个配置类就是来替代spring的配置文件的,当然这个就是spring的注解,然后在方法上加入@Bean注解就能吧这个方法的返回值注入到容器中,注意这里的都是spring中的注解

@Configuration
public class MyAppConfig{
  @Bean
  public HelloService helloService(){
    return new HelloService();
  }
}
配置文件占位符

${random.value},${random.int},${random.long},${random.int[1024,65536]}表示随机数,${..}中间写之前配置的值可以取出来

person.last-name=zhangsan${random.uuid}
person.age=${random.int}
person.birth=2017/12/15
person.boss=false
person.maps.k1=v1
person.maps.k2=v2
person.lists=a,b,c
person.dog.name=${person.last-name}_dog
person.dog.age=15
多profile

创建多个配置文件application-{profile}.properties/yml

####### 激活profile 在主配置文件中写 spring.profile.active = dev, 就可以激活application-dev.properties

####### yml多文档块 下面定义了三个文档块,并激活了第三个文档块

server:
  port: 8081
spring:
  profiles:
    action: prod

server:
  port: 8083
spring:
  profiles: dev

server:
  port: 8084
spring:
  profiles: prod

用命令行激活

--spring.properties.active=dev

用虚拟机参数激活

-Dspring.properties.active=dev

####### 配置文件加载顺序 1 file:./config/ 2 file:./ 3 classpath:/config 4 classpath:/ 从上到下,优先级从高到低,高优先级的会覆盖低优先级的内容,注意是覆盖,而不是看了高优先级的配置以后就不看低优先级的配置了,还可以通过命令行参数设置--spring.config.localtion指定配置文件路径,这里也是互补配置 ####### 外部配置文件 优先加载profile的,由外部到内部加载

####### 自动配置原理 去查官方文档 SpringBoot启动的时候加载主配置类,开启了自动配置功能@EnableAutoConfiguration ,利用EnableAutoConfigurationImportSelect导入组件,每一个xxxAutoConfiguration都是容器中的一个组件,都加入到容器中,用他们来做自动配置,每一个自动配置类进行自动配置功能 ######## HttpEncodingAutoConfiguration 根据当前不同的条件判断,决定当前这个配置类是否生效
Configuration 表明配置类 EnableConfigurationProperties 启动指定类的ConfigurationProperties功能,到HttpProperties中看到这个类上有ConfigurationProperties ConditionalOnWebApplication Conditionalxx是spring中的注解,根据不同的条件,如果满足指定条件,整个配置类中的配置才会生效,这里判断当前应用是否为web应用 ConditionalOnClass 判断当前项目中有没有这个类, CharacterEncodingFilter SpringMVC中进行乱码解决的过滤器 ConditionalOnProperties 判断配置文件中是否存在某个配置

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HttpProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

所有在配置文件中能配置的属性都是在xxxProperties中封装着

@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {

######## xxxAutoConfiguration 自动配置类 ######## xxxProperties 封装配置文件中相关属性

####### Condition

@Conditional 作用
@ConditionalOnjava java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 满足SpEL表达式
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionalOnWebApplication 当前项目为web项目
@ConditionalOnNotWebApplication 当前不是web项目
@ConditionalOnJndi JNDI存在指定项

####### 自动配置生效 只有在特定的条件下才能生效 启用debug=true让控制台打印自动配置报告

debug=true

SpringBoot3-日志

Springboot和日志

考虑和jdbc和数据库驱动一样,我们抽象出一个日志的接口

常见的java日志

JUL,JCL,JBoss-logging,logback,log4j,log4j2,slf4j

Java抽象

JCL,SLF4j,Jboss-logging

Java实现

Log4j,JUL,Log4j2,logback

怎么选择

选择SLF4j+Logback

SpringBoot怎么搞?

Spring选择了JUL,SpringBoot选择了SLF4j+Logback

SLF4j使用

调用日志抽象层的方法,而不是实现

Logger logger = LoggerFactory.getLogger(?.class);
logger.info("hello world")
log4j

log4j出现的早,没想过会有slf4j的出现,那我们要怎么用它呢?实际上是实现了一个适配器,用适配器调用log4j,用slf4j调用适配器,这里是一个设计模式

遗留问题

我们用了多个框架,这些框架有用了不同的日志系统,我们该怎么办? ####### 统一日志记录 偷天换日,你趁框架不注意,把jar包换一下,如Commons loggingAPI就用jcl-over-slf4j.jar, 如log4jAPI就用log4j-over-slf4j.jar来替换,就可以了,这些jar其实调用了slf4j。 ######## 具体操作 先排除日志框架,然后用中间包替换原用的日志框架,最后导入slf4j其他的实现。

SpringBoot和日志
spring-boot-starter-logging
logback-classic
3个狸猫包偷梁换柱
jul-to-slf4j
log4j-ober-slf4j
jcl-ober-slf4j

Springboot给我们做好了偷梁换柱,所以我们在引入其他框架的时候一定要把这个框架的默认日志依赖移除掉。

使用日志

springboot都集成了这些

logging.level.com.wsx.springboothelloworld = debug
logging.path= log
logging.pattern.console=%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %logger{50} -%msg%n
    @Test
    void contextLoads() {
//        System.out.println(person);
        Logger logger = LoggerFactory.getLogger(getClass());
        logger.error("hi");
        logger.warn("hi");
        logger.info("info hi");
        logger.debug("debug hi");
        logger.trace("trace hi");
    }

想用自己的配置文件直接把它放到resources文件夹下面就可以了,推荐使用xxx-spring.xml, 比如你使用了logback.xml, 那么这个xml就直接被日志框架识别了,绕开了spring,如果你是用logback-spring.xml, 那么日志框架无法加载,有springboot接管,springboot就可以根据环境来安排不同的配置,在开发环境和非开发环境使用不同的配置。

SpringBoot4-Web1-静态资源

SpringBoot与Web

先在idea中选择场景 SpringBoot已经默认将这些常见配置好了,我们只需要在配置文件中指定少量配置就可以运行起来 然后我们可以开始编写业务代码了

SpringBoot与静态资源
WebMvcAutoConfiguration

打开WebMvcAutoConfiguration.java

		@Override
		public void addResourceHandlers(ResourceHandlerRegistry registry) {
			if (!this.resourceProperties.isAddMappings()) {
				logger.debug("Default resource handling disabled");
				return;
			}
			Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
			CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
			if (!registry.hasMappingForPattern("/webjars/**")) {
				customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
						.addResourceLocations("classpath:/META-INF/resources/webjars/")
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
						.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}
		}
配置jquery

所有/webjars/下的资源,都去classpath:/MEFA-INF/resources/webjars/找 在Webjars中选择Maven,然后就可以导入你想要jquery的依赖了 比方安装了这个以后就可以通过下面的地址访问jquery了localhost:8080/webjars/jquery/3.3.1/jquery.js

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.0</version>
        </dependency>
默认映射

ResourceProperties 可以设置静态资源的配置,如缓存时间

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

还会在下面的路径中找(静态资源的文件夹) 比方说你要访问一个localhost:8080/myjs.js,如果找不到的话,就在下面的文件夹中寻找

	private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
			"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
欢迎界面

欢迎页面, 静态资源文件夹的/index.html, 见下面的代码

		@Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
				FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
			WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
					new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
					this.mvcProperties.getStaticPathPattern());
			welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
			return welcomePageHandlerMapping;
		}
    		private Optional<Resource> getWelcomePage() {
			String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
			return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
		}

		private Resource getIndexHtml(String location) {
			return this.resourceLoader.getResource(location + "index.html");
		}
图标

配置自己的favicon.ico SpringBoot2中没有这个东西,可能移到其他位置去了

定义自己的映射

利用配置文件来自己定义/的映射

spring.resources.static-locations = classpath:/hello/,classpath:/hello2/

SpringBoot4-Web2-模版引擎

模版引擎

常见的模版引擎有JSP,Velocity,Freemarker,Thymeleaf

SpringBoot推荐的Thymeleaf
        <!--        模版引擎-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

视频中说这个版本有点低,是2.16的 然鹅我用的SpringBoot2,已经是3.x了 修改版本号,这招估计学了有用,这个能覆盖版本

<properties>
  <thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
  <thymeleaf-layout-dialect.versoin>2.1.1</thymeleaf-layout-dialect.version>
</properties>
Thymeleaf语法

还是去autoconfigure中找thymeleaf的ThymeleafAutoDConfigution,这里可以看到配置源码

/**
 * Properties for Thymeleaf.
 *
 * @author Stephane Nicoll
 * @author Brian Clozel
 * @author Daniel Fernández
 * @author Kazuki Shimizu
 * @since 1.2.0
 */
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;

	public static final String DEFAULT_PREFIX = "classpath:/templates/";

	public static final String DEFAULT_SUFFIX = ".html";

只要我们吧HTML页面放在class:/templates/下,thymeleaf就可以渲染。 继续修改我们的代码,注意这里不要用RestController注解,

package com.wsx.springboothelloworld.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class HelloController {
    @RequestMapping("/hello")
    @ResponseBody // 把这个类的所以方法返回给浏览器,转化为json数据
    public String hello() {
        return "hello world quick!";
    }

    @RequestMapping("/templates_hello")
    public String templates_hello() {
        return "templates_hello";
    }
}

然后在templates下创建一个templates_hello.html这样就能返回那个html了

####### 使用 thymeleafspring.pdf 在3.1中找到如下片段 导入名称空间

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link rel="stylesheet" type="text/css" media="all" href="../../css/gtvg.css" th:href="@{/css/gtvg.css}"/>
</head>
<body><p th:text="#{home.welcome}">Welcome to our grocery store!</p></body>
</html>

修改我们的Controller

    @RequestMapping("/templates_hello")
    public String templates_hello(Map<String,Object> map) {
        map.put("hello","map.put(hello,hello)");
        return "templates_hello";
    }

我们这样写templates_hello.html,这里的text值得是改变当前div中的内容的

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Good Thymes Virtual Grocery</title>
</head>
<body>
    <h1>这个来自templates_hello.html</h1>
    <div th:text="${hello}"></div>
</body>
</html>

然后我们就得到了hello的内容 ####### th th:text改变div文本,th:id改变id,th:class改变class,th可以改变所有的属性,更多的信息查看官方文档10 Attribute Precedence ######## Fragment inclusion 片段包含,如jsp的include,有th:insert和th:replace ######## Fragment iterator 遍历,如jsp的forEach, 有th:each ######## Conditional evaluation 条件判断, 如jsp的if, 有th:if,th:unless,th:saitch,th:case, ######## 后边的还有很多,这里就不展开、 ####### 表达式 参见文档4 Standard Experssion Syntax 文档我就不贴过来了。。挺清楚的,这个应该不是我目前的重点。

SpringBoot4-Web3-SpringMVC

扩展SpringMVC
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <mvc:view-controller path="/hello" view-name="succcess"></mvc:view-controller>
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/hello"/>
            <bean></bean>
        </mvc:interceptor>
    </mvc:interceptors>
</beans>

编写一个配置类(@Configuration),是WebMvcConfigurerAdapter,不标注@EnableWebMvc

package com.wsx.springboothelloworld.config;

import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        super.addViewControllers(registry);
        registry.addViewController("/wsx").setViewName("templates_hello");
    }
}
原理
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

	public static final String DEFAULT_PREFIX = "";

	public static final String DEFAULT_SUFFIX = "";

	private static final String[] SERVLET_LOCATIONS = { "/" };

里面也是这个类,注意又个EnableWebMvcConfiguration

	// Defined as a nested config to ensure WebMvcConfigurer is not read when not
	// on the classpath
	@Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class)
	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {

		private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);

		private final ResourceProperties resourceProperties;

静态资源映射

		@Override
		public void addResourceHandlers(ResourceHandlerRegistry registry) {
			if (!this.resourceProperties.isAddMappings()) {
				logger.debug("Default resource handling disabled");
				return;
			}
			Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
			CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
			if (!registry.hasMappingForPattern("/webjars/**")) {
				customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
						.addResourceLocations("classpath:/META-INF/resources/webjars/")
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
						.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
						.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
			}
		}
	/**
	 * Configuration equivalent to {@code @EnableWebMvc}.
	 */
	@Configuration(proxyBeanMethods = false)
	public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {

		private final ResourceProperties resourceProperties;

		private final WebMvcProperties mvcProperties;

从容器中获取所有的webmvcconfigurer,然后全部调用一遍

/*
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 */
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();


	@Autowired(required = false)
	public void setConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.configurers.addWebMvcConfigurers(configurers);
		}
	}

springmvc的自动配置和我们的扩展配置都会起作用

全面接管mvc

不要Springboot的mvc了,完全自己接管,使用@EnableWebMvc,那么web的自动配置全部失效,甚至静态资源都无法使用

为什么enablewebmvc就全部失效呢
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

当容器中没有WebMvcConfigurationSupport的时候,自动配置才开始生效,enablewebmvc帮我们导入了这个,所以失效了

如何修改SpringBoot的默认配置

springboot先看容器中有没有用户自己配置的,如果有就用用户配置的,没有才自动配置

在springboot中有很多xxxConfiguier帮助我们扩展配置,

在*一点
@Configuration
//@EnableWebMvc
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        super.addViewControllers(registry);
        registry.addViewController("/wsx").setViewName("templates_hello");
    }

    @Bean
    public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController("/wsx2").setViewName("templates_hello");
                registry.addViewController("/wsx3").setViewName("templates_hello");
            }
        };
    }
}
引入bootstrap的webjars

官网

<!--        bootstrap-->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>4.0.0</version>
        </dependency>
@{/webjars/bootstrap/4.0.0/css/bootstrap.css}

这个语法很好,因为当我们的项目名字变了的时候,不需要去修改所有的url, server.context-path=/crud

SpringBoot4-Web4-国际化

国际化
  • 编辑国际化配置文件
  • 使用ResourceBundleMessageSource管理国际化资源文件
  • 在页面使用fmt:message取出国际化内容
创建resources/i18n

然后创建login_zh_CN.properties 选择Resouerce Bundle SpringBoot自动创建了管理国际化资源文件的组件

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

	private static final Resource[] NO_RESOURCES = {};

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

	@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(properties.getBasename())) {
			messageSource.setBasenames(StringUtils
					.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}
/**
 * Configuration properties for Message Source.
 *
 * @author Stephane Nicoll
 * @author Kedar Joshi
 * @since 2.0.0
 * /
public class MessageSourceProperties {

	/**
	 * Comma-separated list of basenames (essentially a fully-qualified classpath
	 * location), each following the ResourceBundle convention with relaxed support for
	 * slash based locations. If it doesn't contain a package qualifier (such as
	 * "org.mypackage"), it will be resolved from the classpath root.
	 */
	private String basename = "messages";
spring.messages.basename = i18n.login
thymeleaf 取国际化信息

使用#{}

<h1 class="..." th:text="#{login.tip}">Please sign in</h1>
解决乱码

setting - editor - fileEncoding - utf8 - 自动转阿斯克码

测试

在浏览器中选择浏览器默认的语言就可以了,即他可以根据浏览器的语言信息设置语言了

如何实现点按钮实现不同语言呢

####### 国际化原理 locale: LocaleResolver 根据请求头的区域信息来进行国际化

		@Bean
		@ConditionalOnMissingBean
		@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
		public LocaleResolver localeResolver() {
			if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
				return new FixedLocaleResolver(this.mvcProperties.getLocale());
			}
			AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
			localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
			return localeResolver;
		}
	@Override
	public Locale resolveLocale(HttpServletRequest request) {
		Locale defaultLocale = getDefaultLocale();
		if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
			return defaultLocale;
		}
		Locale requestLocale = request.getLocale();
		List<Locale> supportedLocales = getSupportedLocales();
		if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
			return requestLocale;
		}
		Locale supportedLocale = findSupportedLocale(request, supportedLocales);
		if (supportedLocale != null) {
			return supportedLocale;
		}
		return (defaultLocale != null ? defaultLocale : requestLocale);
	}

先写个链接把区域信息加上去

<a class="..." th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="..." th:href="@{/index.html(l='en_US')}">English</a>

然后自己实现区域信息解析器

    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
package com.wsx.springboothelloworld.component;

import org.springframework.cglib.core.Local;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;


public class MyLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String l = request.getParameter("l");
        if(!StringUtils.isEmpty(l)) {
            String[] split = l.split("_");
            return new Locale(split[0], split[1]);
        }
        return Locale.getDefault();
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

处理post

package com.wsx.springboothelloworld.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

public class LoginController {
    @RequestMapping(value="",method= RequestMethod.POST)
    public String login(){
        return "dashborad";
    }
}

简化写法

package com.wsx.springboothelloworld.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;


public class LoginController {
//    @RequestMapping(value="",method= RequestMethod.POST)
    @PostMapping("")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password){
        return "dashborad";
    }
}

如果成功就进入dashborad,否则就提示用户名密码错误, 考虑使用map实现 这一步后我们可能会碰到一些问题,这是缓存导致的,加入下面的配置

spring.thymeleaf.cache=false

然后ctrl+f9手动加载html到编译的文件中,这不必重新开启Spring了

做一个判断来决定标签是否生效

<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

表单重复提交问题。需要用重定向、视图、拦截器解决,重定向加视图能确保没有重复提交,但是会导致直接跳过登陆的问题,

拦截器

创建拦截器

package com.wsx.springboothelloworld.component;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginHandlerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser == null) {
            request.setAttribute("msg","没有权限,请先登录");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false;
        } else {
            return true;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

将登陆成功的用户放入seesion

public class LoginController {
    //    @RequestMapping(value="",method= RequestMethod.POST)
    @PostMapping("")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Map<String, Object> map,
                        HttpSession httpSession) {
        if (password.endsWith("123456")) {
            httpSession.setAttribute("loginUser", username);
            return "dashborad";
        } else {
            return "404";
        }
    }
}

springboot已经做好了静态资源,不用管他们,不会被拦截, 注意addPathPatterns.excludePathPatterns可以一直搞下去,拦截所有的页面,放行两个html

    @Bean
    public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                registry.addViewController("/wsx2").setViewName("templates_hello");
                registry.addViewController("/wsx3").setViewName("templates_hello");
            }

            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                super.addInterceptors(registry);
                //
                registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/index.html","/user/login");
            }
        };
    }

SpringBoot4-Web5-烂尾

需求
员工列表
普通CRUD restfulCRUD
查询 getEmp emp...GET
添加 addEmp? emp...POST
修改 updateEmp? emp/{id}...PUT
删除 deleteEmp? emp/{id}...DELETE
架构
请求URL 请求方式
查询所有员工 emps GET
查询单个员工 emp/{id} GET
来到添加页面 emp GET
添加员工 emp POST
来到修改页面 emp/{id} GET
修改员工 emp PUT
删除员工 emp/{id} DELETE

修改|updateEmp?|emp/{id}...PUT 删除|deleteEmp?|emp/{id}...DELETE

<footer th:fragment="copy">
hello
</footer>

<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>

insert 是将整个元素插入到引用中 replace 是替换 include 是包含进去

    <div th:fragment="topbar"> 这里测试 include replace 和insert</div>
    <div id="include" th:include="~{templates_hello::topbar}">hi</div>
    <div id="replace" th:replace="templates_hello::topbar">hi</div>
    <div id="insert" th:insert="templates_hello::topbar">hai</div>
    <div id="include"> 这里测试 include replace 和insert</div>
    <div> 这里测试 include replace 和insert</div>
    <div id="insert"><div> 这里测试 include replace 和insert</div></div>

挺前端的

错误响应

如何定制错误页面? 这个是ErrorMvcAutoConfiguration

	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
			ObjectProvider<ErrorViewResolver> errorViewResolvers) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				errorViewResolvers.orderedStream().collect(Collectors.toList()));
	}

	@Bean
	public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
		return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
	}


  	@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean(ErrorViewResolver.class)
		DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
		}

系统出现错误以后去/error处理请求

这老师是源码杀手,我要炸了,我现在开始怕源码了 这里分两类,一个返回html,另一个返回json,区分浏览器,浏览器优先接受html,但是客户端优先接受/*, 没有要求,所以对浏览器返回,html,对客户端返回json

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}
所以到底如何定制?

有模版引擎的话,就在error下写一个404.html就可以了,你甚至可以用4xx.html来批评所有的4开头的错误

<h1>status:[[${status}]]</h1>

没有模版引擎就在静态资源文件夹找

嵌入式servlet容器

默认是tomcat

如何定制修改servlet容器
  • 方法1 使用server.port = 8081 server.tomcat.xxx
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
  • 方法2 使用Bean, springboot中有很多xxxConfiguier来拓展配置 有很多xxxCustomizer来定制配置
    @Bean
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryWebServerFactoryCustomizer(){
        return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>(){
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                factory.setPort(8083);
            }
        };
    }
  • 注册Servlet、Filter、Listener 使用ServletReristrationBean、FilterRegistrationBean、Listener...把他们Bean到容器中就可以了
  • 切换Servlet容器 Jetty 适用长链接 Undertow 适用于高并发不带jsp
  1. 排除tomcat依赖
  2. 引入其他依赖
嵌入式的tomcat如何实现

源码警告

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
	/**
	 * Nested configuration if Tomcat is being used.
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
	public static class TomcatWebServerFactoryCustomizerConfiguration {

		@Bean
		public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
				ServerProperties serverProperties) {
			return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
		}

	}

源码变了。。。

<iframe src="//player.bilibili.com/player.html?aid=38657363&bvid=BV1Et411Y7tQ&cid=67953935&page=48" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe> 以前是放一个嵌入式的容器工厂,然后配置tomcat,最后传给TomcatEmbeddedServletContainer,并且启动tomcat容器
配置修改是如何生效的

配置文件+定制器, 他们本质上都是定制器 有一个BeanPostProcessorRegistrar, 导入了EmbeddedServletContainerCustomizerBeanPostProcessor

SpringBoot根据导入的依赖情况,给容器添加相应的容器工厂, 容器中某个组件要创建对象就会惊动后置处理器, 只要是嵌入式的servlet容器工厂,后置处理器就会工作, 后置处理器会从容器中获取所有的定制器,调用定制器的方法。

嵌入式servlet什么时候创建

springboot启动, 运行run, 创建IOC容器, 并初始化, 创建容器中的每个bean, 如果是web应用就创建web容器,否则创建AnnotationConfigApplicationContext, 在web的IOC容器中, 重写了onRefresh, 在这里创建了嵌入式的Servlet, 获取容器工厂, tomcatembeddedservletcontainerfactory创建对象以后,后置处理器就开始配置,然后获得新的servlet容器,最后启动

优点

简单便携

缺点

不支持jsp

SpringBoot5-数据访问

创建项目

选择MySQL+JDBC+Web

链接数据库

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/jdbc
    driver-class-name: com.mysql.jdbc.Driver
package com.wsx.study.springboot.demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        System.out.println(dataSource.getClass());
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        connection.close();
    }
}

SpringCloud

SpringCloud1-入门

集群/分布式

集群是多台计算机为了完成同一个工作,组合在一起达到更高的效率的系统

分布式是为了完成一个工作,将工作拆分为多个服务,分别运行在不同机器上的系统

分布式系统的CAP理论

强一致性、高可用性、分区容错性无法同时达到极致

强一致性指的是多个节点间的数据是实时一致的

高可用性指的是单位时间内我们的分布式系统能够提供服务的时间

分区容错性指的是分布式系统中一部分节点出现问题以后,我们仍然可以提供正常服务。

SpringCloud的基础功能

  • 服务治理:Eureka
  • 客户端负载均衡:Ribbon
  • 服务容错保护:Hystrix
  • 声明式服务调用:Feign
  • API网关服务:Zuul
  • 分布式配置中心:Config

Eureka(服务治理)

这是一个根据服务名字提供IP和端口的服务器,和DNS服务器比较像,我们的节点分为3类,服务提供者、服务消费者、EurekaServer

服务提供者

这些节点提供服务,他们在EurekaServer上注册自己,并定时通过心跳机制来表明自己正常,当他下机的时候也会通知EurekaServer, 这些分别叫做服务注册、服务续约、服务下线

服务消费者

这些节点调用服务提供者的服务,但是他们不知道IP和Port是多少,所以他们需要先向EurekaServer询问自己想要调用的服务在哪些机器上,然后才可以调用服务。这些叫做获取服务和服务调用

EurekaServer

他们支持服务提供者的注册,支持服务消费者的询问,还要支持监视服务提供者的心跳,当他们发现某服务提供者心跳出现问题的时候,将其剔除,如果某些服务提供者的心跳不正常但是不致死,他们就会将这些服务提供者的信息保护起来,尽量不让他们过期。

Ribbon(客户端负载均衡)

摘自撸一撸Spring Cloud Ribbon的原理

说起负载均衡一般都会想到服务端的负载均衡,常用产品包括LBS硬件或云服务、Nginx等,都是耳熟能详的产品。 而Spring Cloud提供了让服务调用端具备负载均衡能力的Ribbon,通过和Eureka的紧密结合,不用在服务集群内再架设负载均衡服务,很大程度简化了服务集群内的架构。

Hystrix(服务器容错)

问题提出

在高并发的情况下,某个服务延迟,导致其他请求延迟,最终引发雪崩

断路器

当某个服务单元故障,即50%的请求失败的时候,就会触发熔断,直接返回错误调用,熔断期间的请求都会直接返回错误,5秒以后重新检测该服务是否正常,判断熔断是否关闭

线程隔离

为了保证服务之间尽量不要相互影响,每个服务的线程池是独立的

Feign(声明式服务调用)

我们可以直接使用注解构造接口来指定服务,非常简单,你只需要声明一下就可以了 当然他整合了Ribbon和Hystrix

Zuul(API网关服务)

看得不是太懂

我们的微服务实现了Eureka,但是入口的第一个服务缺没有,,Nginx必须手动维护IP,然后Nginx执行负载均衡以后,我们还需要对每一个请求验证签名、登陆校验冗余,这就导致了重复的代码。

Zuul出现了,他整合了Eureka、Hystrix、Ribbon,通过Eureka解决IP问题,通过调用微服务解决代码重复的问题

Config(分布式配置中心)

功能和Zookeeper比较相似

入门结束

下次再深入学习,我康康SpringMVC去

参考资料

外行人都能看懂的SpringCloud,错过了血亏! Spring Cloud Feign设计原理 SOA和微服务架构的区别? 分布式、集群、微服务、SOA 之间的区别 微服务Springcloud超详细教程+实战(一)

SpringMVC

SpringMVC

SpringMVC

少写博客,多思考,多看官方文档, 那我就写一篇算了

MVC

model(dao,service) + view(jsp) + controller(servlet)

实体类

我们的实体类可能有很多字段,但是前端传输的时候可能只会传输一两个数据过来,我们就没有必要吧前端传过来的数据封装成为一个实体类,这样很多字段都是空的,浪费资源,实际上我们会对pojo进行细分,分为vo、dto等,来表示实体类的一部分的字段

回顾jsp+servlet

创建项目

卧槽,还能直接创建一个空的maven项目,然后在其中创建子项目,惊呆了 maven-空骨架-name 导入公共依赖

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
    </dependencies>

然后右键你的项目-new-module nb nb nb 新建一个子项目以后,右键子项目,添加框架支持。 nb nb nb 然后做普通的servlet就可以了 在main.java中创建helloservlet, 然后继承httpservlet即可 然后配置servlet-name + servlet-class (我现在看到这个就觉得没有springboot的注解爽)

MVC框架要完成的事情

将URL映射到java类或者java方法 封装用户提交的数据 处理请求-调用相关的业务处理-封装响应数据 将响应的数据进行渲染

SpringMVC

多看官网 官网

SpringMVC的优点

轻量、简单、高效、兼容Spring、约定优于配置、功能强大

莫名其妙的开始

  • 配置web.xml在其中注册DispatcherServlet
  • 写springmvc-servlet.xml 添加前后缀映射
  • 写controller,然后就结束了 404? 注意缺少依赖, 你的项目有,但是编译到tomcat中就没有了,去看看target里面的东西。 视频
解释
  • 用户请求发到DispatcherServlet
  • DispatcherServlet调用HandlerMapping查找url对应的Handler
  • DispatcherServlet调用执行Handler,得到model和view
  • DispatcherServlrt配置视图解析器,返回视图
再写一遍

确定maven中有依赖,确定projectstructrue中的artifacts也有依赖 写web.xml , 注意/ 匹配的不包含jsp,/*是全部

<?xml version="1.0" encoding="utf-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--    处理器、适配器、解析器-->
    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <bean id="/hello" class="com.wsx.controller.HelloController"/>

</beans>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">



    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

坑真多,我还碰到另外一个坑了,tomcat10也太秀了,居然是他的原因,换成tomcat9就不会404,我服了

还有第二个坑,我绝望了,项目名字不能叫做SpringMVC,你要是取这个名字,你的src目录就是没有颜色的,坑的一批,后面你创建多个moudle的时候,他就给你目录全搞灰色,这个问题只需要不把名字设为SpringMVC就可以了。

注解配置Controller

这里的19行是spring中的注解扫描,21行是不去处理静态资源,23行是配置处理器的适配器

<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <context:component-scan base-package="com.wsx.controller"/>
    <!--    不处理静态资源-->
    <mvc:default-servlet-handler/>
    <!--    配置处理器和适配器-->
    <mvc:annotation-driven/>

    <!--    解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

第7行是配置controller,第9行是映射url,

被controller注解配置的类,会被注入到IOC容器,它里面的方法如果有返回值是String,并且有具体页面可以跳转,就会被视图解析器解析

还可以直接在类上面注解RequestMapping,可以指定一个url,和下面的url拼接

package com.wsx.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String hello(Model model) {
        // 封装数据
        model.addAttribute("msg", "hello,Spring");
        // 返回视图
        return "hello";
    }
}

RestFul风格

就是不再使用http://xxxx/?id=1&name=2 这种url RestFul就是直接使用http://xxxx/1/2

@GetMapping("/add/{a}/{b}")
public String test2(@PathVariable int a,@PathVariable String b,Model model){
  String res = a + b;
  model.addAttribute("msg","结果为"+res);
  return "test";
}

SpringMVC2-注解

注解配置Controller

这里的19行是spring中的注解扫描,21行是不去处理静态资源,23行是配置处理器的适配器

<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

    <context:component-scan base-package="com.wsx.controller"/>
    <!--    不处理静态资源-->
    <mvc:default-servlet-handler/>
    <!--    配置处理器和适配器-->
    <mvc:annotation-driven/>

    <!--    解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

第7行是配置controller,第9行是映射url,

被controller注解配置的类,会被注入到IOC容器,它里面的方法如果有返回值是String,并且有具体页面可以跳转,就会被视图解析器解析

还可以直接在类上面注解RequestMapping,可以指定一个url,和下面的url拼接

package com.wsx.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String hello(Model model) {
        // 封装数据
        model.addAttribute("msg", "hello,Spring");
        // 返回视图
        return "hello";
    }
}

RestFul风格

就是不再使用http://xxxx/?id=1&name=2 这种url RestFul就是直接使用http://xxxx/1/2

@GetMapping("/add/{a}/{b}")
public String test2(@PathVariable int a,@PathVariable String b,Model model){
  String res = a + b;
  model.addAttribute("msg","结果为"+res);
  return "test";
}

前端传入参数

为了避免麻烦,请写上@RequestParam

    @RequestMapping("/user")
    public String user(@RequestParam("name") String name, Model model){
        model.addAttribute("msg",name);
        return "hello";
    }

然后访问下面这个,显然成功了 http://localhost:8080/annotation_war_exploded/user?name=hi

前端传入对象

SpringMVC回去匹配对象的字段,你的参数必须和对象的字段名保持一致

Model、ModelMap、LinkedHashMap

Model 只有几个方法储存数据,简化了新手对于model对象的操作和理解 ModelMap继承了LinkedMap, ModelAndView 可以在储存数据的同时,设置返回的逻辑视图(几乎不用)

乱码配置

在web.xml中配置下面的过滤器, 然后在tomcat的配置文件中查看tomcat是否配置UTF-8 千万要注意,下面的/ 一定要改为/*

    <filter>
        <filter-name>encoding</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

SpringMVC3-JSON

maven

maven依赖管理

maven工程可以帮助我们管理jar包的依赖,他有一个jar包仓库,这导致我们自己的项目会非常小。

maven启动

mvn tomcat:run

maven仓库启动

先本地,然后私服,然后**仓库

Java代码

核心代码+配置文件+测试代码+测试配置文件

传统项目

workspace
  src
  config

maven项目

workspace
  src
    main
      java(核心代码)
      config(配置文件)
      webapp(css,js)
    test
      java
      config

maven命令

mvn clean # 清除编译文件
mvn compile # 编译
mvn test # 编译+测试
mvn package # 编译+测试+打包
mvn install # 编译+测试+打包+放入本地仓库

pom.xml

自身信息,依赖的jar包信息,运行环境信息

依赖管理

公司名,项目名,版本号

<dependency>
  <groupld>javax.servlet.jsp</groupld>
  <artifacid>jsp-api</artifactid>
  <version>2.0</version>
</dependency>

maven生命周期(一键构建)

清理生命周期

清除

默认生命周期

编译-测试-打包-安装-发布

站点生命周期

用的不多

使用骨架

mvn archetype:generate

不使用骨架

mkdir src
cd src
mkdir -p main/java test/java main/resources test/resources
echo "<project>" >> pom.xml
echo "  <groupId>com.project</groupId>" >> pom.xml
echo "  <artifacId>project</artifacId>" >> pom.xml
echo "  <version>1.0-SNAPSHOT</version>" >> pom.xml
echo "</project>" >> pom.xml
cd ..

Spring全家桶的xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

Linux

linux指令学习1-init

linux运行级别

linux一共有7个级别,分别为 0关机、 1单用户、 2无网多用户、 3有网多用户, 4保留, 5图形界面, 6重启。 在文件/etc/inittab中指定了级别。

查看运行级别

查看文件/etc/inittab

修改运行级别

init 3

如何找回root密码

进入单用户模式,然后修改密码,因为进入单用户模式不需要密码就可以登陆。 进入grub中,按e编辑指令,修改kernel,输入1进入单用户级别,输入b启动,用passwd root修改密码

linux指令学习2-mkdir和rmdir

mkdir

在用户文件夹下创建hello

mkdir ~/hello 

多级目录需要加上-p参数

mkdir ~/h/h/h

rmdir

删除空文件夹

rmdir ~/hello

删除非空文件夹

rm -rf

linux指令学习3-touch和cp

touch

创建文件,我常用vim

touch a.txt b.txt c.txt 

cp

将a.txt拷贝到用户目录下

cp a.txt ~/

将a这个文件夹全部拷贝到用户目录,-r指的是递归

cp -r a/ ~/

\cp可以强制覆盖不提示,在mac中直接覆盖了,不需要\cp

linux指令学习4-rm和mv

rm

删除a.txt,

rm a.txt

删除目录a, -r为递归

rm -r a/

删除目录a,-f为不提示 可与-r合并为-rf

rm -r -f a/

mv

将a.txt重命名为b.txt

mv a.txt b.txt

将a.txt一定到用户目录,如果那有的话,mac不提示是否替换,直接替换,有点不人道了。

mv a.txt ~/

linux指令学习5-cat-more和less

cat

cat是浏览文件 就能看到配置文件了

cat ~/.vimrc

-n 能够显示行号

cat -n ~/.vimrc

more是一个类似于vim的东西,能够把文件分页,用空格看下一行,用enter看下一页,用<C-F>和<C-B>翻页,用=输出行号,用fb也可以翻页。

cat -n ~/.vimrc | more

more

直接完成

more ~/.vimrc 

less

基于显示的懒加载方案,打开文件非常快 几乎和more一样,就是开大文件快一点,可以用来打开日志。

less ~/.vimrc

linux指令学习6-重定向和追加

> 和>>

>是输出重定向,会覆盖内容,>>是追加,不会覆盖

例子

ls -l 会输出一些内容,这些叫输出,>a.txt会写入a.txt,当然也可以用>>来追加,后面只演示>,不演示>>了

ls -l > a.txt

例子2

将cat的输出重定向到b.txt中

cat a.txt > b.txt

echo

输出 abcde

echo "abcde"

将abcde写入a.txt

echo "abcde" > a.txt

cal

cal显示日历 将日历输出到a.txt

cal > a.txt 

linux指令学习7-echo head 和tail

echo

一般用于输出信息, 输出了abc

echo "abc"

输出环境变量,

echo $PATH

head

查看文件的前几行 看vim配置文件前10行

head ~/.vimrc

看vim配置文件的前20行,-n表示行数

head -n 20 ~/.vimrc

tail

查看结尾几行,同上 监控a.txt,当他被追加的时候,输出追加的信息

tail -f a.txt

linux指令学习8-软链接和history

ln

建立软链接(快捷方式) 创建一个用户目录的软链接到当前目录,这个软链接叫mylink

ln -s ~ mylink

history

查看最近执行的指令 mac中不太一样,history 10 表示查看第10条指令到现在的指令 查看最近执行的10条指令

history 10

执行第10调指令

!10

linux指令学习9-时间日期

date

date可以看到时间,后面是格式设置

date "+%Y-%m-%d 星期%w %H:%M:%S"

设置日期

-s 表示设置时间

date -s "2021-1-1 1:1:1"

cal

cal直接查看当前月的日历 看2020n年的日历

cal 2020 

linux指令学习10-搜索查找

find

在用户文件夹下找名为.vimrc的文件

find ~ -name .vimrc

在用户文件夹下找名为.vimrc属于用户s的文件

find ~ -user s -name .vimrc

在用户文件夹下找大于100M的文件

find ~ -size +100M

在用户文件夹下找小于100M的文件

find ~ -size -100M

在用户文件夹下找等于100M的文件

find ~ -size 100M

通配符

find ~ -name *.txt

locate

根据数据库快速定位文件的位置, 更新数据库

updatedb

根据数据库快速定位a.txt

locate a.txt 

管道

将前一个指令的输出传递给后一个指令处理

|

grep

寻找let,并输出行号和行数据,-n表示输出行号,-i表示不区分大小写,

grep -n -i let ~/.vimrc

通过管道将cat的结果传递给grep,同上

cat ~/.vimrc | grep -ni let

linux指令学习11-压缩与解压

gzip gunzip

将hello.txt压缩为hello.txt.gz

gzip hello.txt 

将hello.txt.gz解压为hello.txt

gunzip hello.txt.gz

zip 与 unzip

把用户目录下的所有文件压缩到res.zip中

zip -r res.zip ~

把res.zip解压到~/res中

unzip -d ~/res res.zip

rar 与 unrar

有这东西,很少用

tar

-z是打包同时压缩,-c是产生.tar文件,-v是显示详细信息,-f是指定压缩后的文件名 res.tar.gz是打包后的文件,其后为打包文件

-zcvf res.tar.gz a.txt b.txt

对a文件夹打包

-zcvf res.tar.gz a/

解压到当前目录

-zxvf res.tar.gz 

指定解压到~中

-zxvf res.tar.gz -c ~ 

linux指令学习12-git安装和初始化

Git安装

官网下去git官网

创建工作空间

我们先创建一个工作空间myGit,在其中创建一个项目project,植入两个文件a.txt和b.txt,并分别写入"a"和"b"

cd ~ 
mkdir -p myGit/project
cd myGit/project
touch a.txt b.txt
echo "a" >> a.txt
echo "b" >> b.txt

初始化git

紧接着我们用git初始化这个项目

git init

我们看到了输出,他的意思是我们创建了一个空的git仓库

Initialized empty Git repository in /Users/s/myGit/project/.git/

他的意思是说我们的git没有追踪任何一个文件,我们可以通过下面对指令来查看git的状态

git status

紧接着我们得到了反馈,他说没有提交过,并且a.txt和b.txt没有被追踪,他让我们使用add来添加追踪。

On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	a.txt
	b.txt

nothing added to commit but untracked files present (use "git add" to track)

我们尝试使用下面的指令为a.txt追踪,然后再查看状态

git add a.txt
git status

这时候我们的反馈就不一样了,他说我们的a.txt已经进入了git的暂存区

On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   a.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	b.txt

git的结构和状态

git的三层结构

工作区,即我们文件默认的地方,暂存区,即git暂时保留文件的地方,版本库,git保存文件版本的地方

git中文件的状态

文件分为4个状态,untracked未被追踪,modified工作区修改了文件,但没有添加进暂存区,staged添加到了暂存区但是没有提交到版本库,conmitted数据安全的储存在了本地库中。

配置git

git config --global user.email "246553278@qq.com"
git config --global user.name "fightinggg"

查看git配置

我们可以输入如下指令来查看当前的git配置情况

git config --list

之后我们就会看到下面的输出

credential.helper=osxkeychain
user.name=fightinggg
user.email=246553278@qq.com
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true

linux指令学习13-git提交

提交

然后我们就可以尝试去提交我们的

git commit -m 'first commit'

我们得到了如下输出

[master (root-commit) 913bc88] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 a.txt

查看git日志

git log

得到了输出

commit 913bc886088dabee0af5b06351450cad60102c23 (HEAD -> master)
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:45:19 2020 +0800

    first commit

我们尝试将b.txt也提交上去

git add b.txt
git commit -m 'second commit'

再次查看log

commit fbdd818849343a78d0e6ccd8d5ce0f35d9d8b123 (HEAD -> master)
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:48:56 2020 +0800

    second commit

commit 913bc886088dabee0af5b06351450cad60102c23
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:45:19 2020 +0800

    first commit

更多的文件

加入更多的文件

touch a2.txt a3.txt a4.txt a5.txt

将他们全部提交

git add .
git commit -m 'third commit'
git log

我们现在看到有了3次提交

commit 9d1f0b1c3ecd11e5c629c0dd0bfdf4118ad4e999 (HEAD -> master)
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:52:36 2020 +0800

    third commit

commit fbdd818849343a78d0e6ccd8d5ce0f35d9d8b123
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:48:56 2020 +0800

    second commit

commit 913bc886088dabee0af5b06351450cad60102c23
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:45:19 2020 +0800

    first commit

修改后的文件

如果我们修改了一个文件

echo "hellp" >> a.txt
git status

我们看到了git提示有文件被修改了

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   a.txt

no changes added to commit (use "git add" and/or "git commit -a")

将它提交

git commit -am 'modified a.txt'

看到了输出

commit 2e625b6f5de426675e4d2edf8ce86a75acc360de (HEAD -> master)
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:57:43 2020 +0800

    modified a.txt

commit 9d1f0b1c3ecd11e5c629c0dd0bfdf4118ad4e999
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:52:36 2020 +0800

    third commit

commit fbdd818849343a78d0e6ccd8d5ce0f35d9d8b123
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:48:56 2020 +0800

    second commit

commit 913bc886088dabee0af5b06351450cad60102c23
Author: fightinggg <246553278@qq.com>
Date:   Sun Mar 29 16:45:19 2020 +0800

    first commit

追加提交

如果我们发现上一次的提交是没有用的,或者说不想让它出现,又或者说想把它删了,我们使用如下指令

echo "b" >> b.txt
git commit --amend

我们发现我们进入到了vim中

modified a.txt

### Please enter the commit message for your changes. Lines starting
### with '#' will be ignored, and an empty message aborts the commit.
###
### Date:      Sun Mar 29 16:57:43 2020 +0800
###
### On branch master
### Changes to be committed:
### 	modified:   a.txt
###
### Changes not staged for commit:
### 	modified:   b.txt
###

我们将它修改为

modified a.txt b.txt

### Please enter the commit message for your changes. Lines starting
### with '#' will be ignored, and an empty message aborts the commit.
###
### Date:      Sun Mar 29 16:57:43 2020 +0800
###
### On branch master
### Changes to be committed:
### 	modified:   a.txt
###
### Changes not staged for commit:
### 	modified:   b.txt
###

最后再次查看log

git log --oneline

我们得到了下面的输出,上一次的提交被现在的提交覆盖了

105a02a (HEAD -> master) modified a.txt b.txt
9d1f0b1 third commit
fbdd818 second commit
913bc88 first commit

linux指令学习14-git撤销

撤销

假设你犯了一个严重的错误

rm *.txt

代码没了我们来看看git的状态

git status

看到了如下的输出

On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    a.txt
	deleted:    a2.txt
	deleted:    a3.txt
	deleted:    a4.txt
	deleted:    a5.txt
	deleted:    b.txt

no changes added to commit (use "git add" and/or "git commit -a")

check

git checkout

看到了这些输出,他说我们删了很多东西,其实和git status的一样

D	a.txt
D	a2.txt
D	a3.txt
D	a4.txt
D	a5.txt
D	b.txt

从暂存区恢复指定文件

git checkout -- a.txt
cat a.txt

我们发现a.txt已经恢复了,输出如下

a.txt

恢复所有文件

git checkout -- .
ls 

看到了输出,终于我们的文件全部恢复,

a.txt	a2.txt	a3.txt	a4.txt	a5.txt	b.txt

恢复更老的版本?使用reset将暂存区的文件修改为版本913bc886088dabee0af5b06351450cad60102c23的a.txt

git reset 913bc886088dabee0af5b06351450cad60102c23 a.txt
git status

我们注意下面的输出,有两条提示,第一条说改变没有被提交,是因为暂存区和版本区的文件不一致,第二条说修改没有储存到暂存区,这是因为工作区和暂存区的文件不一致造成的。

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   a.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   a.txt

这时候我们就可以使用checkout将暂存区的文件拿出来放到工作区,

git checkout -- a.txt
cat a.txt
git status

我们发现a.txt已经恢复到初始的版本的了。我们查看状态发现工作区和暂存区的差异已经消失了,这就已经达到了恢复文件的目的。

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   a.txt

linux指令学习15-git删除

git删除

将文件删除

git rm a.txt

我们看到了如下的输出,我们看到他说文件被修改了,即暂存区和版本库中的文件不一致

error: the following file has changes staged in the index:
    a.txt
(use --cached to keep the file, or -f to force removal)

我们提交,然后删除,这里就直接成功了

git commit -m 'recover a.txt'
git rm a.txt

下面考虑另外一种情况,我们先撤销这次删除,并对a.txt进行修改,然后再次删除

git reset -- a.txt
git checkout a.txt
echo "add">> a.txt
git rm a.txt

又遇到问题了,我们的暂存区和工作区的文件不一致

error: the following file has local modifications:
    a.txt
(use --cached to keep the file, or -f to force removal)

这些的删除本身就是危险的,不建议删除,但我们依然可以使用-f来强制删除

git rm a.txt -f

linux指令学习16-git分支

git分支

先看看如何查看分支

git branch

得到了输出

* master

创建分支

git branch dev

得到下面的输出,其中*表示当前分支

  dev
* master

切换分支,再次查看分支

git checkout dev
git branch

我们发现dev现在成为了当前分支了

* dev
  master

删除dev分支,直接报错了,因为当前分支是dev分支

git branch -d dev

切换分支并删除dev

git checkout master
git branch -d dev

创建分支,然后修改分支名

git branch b1
git branch -m b1 b2

注意到执行两次这个操作后,报错了,他说名字不能重复

fatal: A branch named 'b2' already exists.

现在我们的分支为

  b1
  b2
* master

创建分支并切换

git checkout -b b3  

分支控制

比较工作区和暂存区

git diff

比较暂存区和版本库

git diff --staged

比较版本

git diff 0923131  105a02a

比较分支

git diff b1 b2

合并分支

git merge b1

linux指令学习17-git保存

保存

将工作区和暂存区的资料保存到栈

git stash

查看栈保存的资料

git stash list

从栈恢复资料

git stash apply 0

删除栈中的资料

git stash drop 0

linux指令学习18-git远程

推入远程仓库

先建立一个快捷访问

git remote add unimportant git@github.com:fightinggg/unimportant.git
git remote -v

看到了输出

unimportant	git@github.com:fightinggg/unimportant.git (fetch)
unimportant	git@github.com:fightinggg/unimportant.git (push)

推入

git push unimportant master

看到是成功了的

Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Delta compression using up to 4 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (13/13), 1.00 KiB | 1.00 MiB/s, done.
Total 13 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), done.
To github.com:fightinggg/unimportant.git
 * [new branch]      master -> master

拉回

git pull unimportant master

也看到成功了

From github.com:fightinggg/unimportant
 * branch            master     -> FETCH_HEAD
Already up to date.

在服务器搭建远程仓库

mkdir /wsx.com.git 
cd wsx.com.git
git init --bare

使用方式

git push ssh://root@<IP>/wsx.com.git master

linux指令学习19-netstat

netstat

netstat可以显示网络状态,

netstat -a

netstat可以显示网卡

netstat -i

我们看到了输出

Name       Mtu   Network       Address            Ipkts Ierrs    Opkts Oerrs  Coll
lo0   16384 <Link#1>                        529008     0   529008     0     0
lo0   16384 127           localhost         529008     -   529008     -     -
lo0   16384 localhost   ::1                 529008     -   529008     -     -
lo0   16384 s-2.local   fe80:1::1           529008     -   529008     -     -
gif0* 1280  <Link#2>                             0     0        0     0     0
stf0* 1280  <Link#3>                             0     0        0     0     0
XHC20 0     <Link#4>                             0     0        0     0     0
XHC0* 0     <Link#5>                             0     0        0     0     0
en0   1500  <Link#6>    f0:18:98:04:fb:91  2816466     0  2459809     0     0
en0   1500  s-2.local   fe80:6::18f3:f6a:  2816466     -  2459809     -     -
en0   1500  192.168.0     192.168.0.106    2816466     -  2459809     -     -
en1   1500  <Link#7>    82:37:90:29:8c:01        0     0        0     0     0
en2   1500  <Link#8>    82:37:90:29:8c:00        0     0        0     0     0
bridg 1500  <Link#9>    82:37:90:29:8c:01        0     0        1     0     0
p2p0  2304  <Link#10>   02:18:98:04:fb:91        0     0        0     0     0
awdl0 1484  <Link#11>   3e:c1:fd:7a:da:c8      131     0       99     0     0
awdl0 1484  fe80::3cc1: fe80:b::3cc1:fdff      131     -       99     -     -
llw0  1500  <Link#12>   3e:c1:fd:7a:da:c8        0     0        0     0     0
llw0  1500  fe80::3cc1: fe80:c::3cc1:fdff        0     -        0     -     -
utun0 1380  <Link#13>                            0     0        4     0     0
utun0 1380  s-2.local   fe80:d::a38c:4185        0     -        4     -     -
utun1 2000  <Link#14>                            0     0        4     0     0
utun1 2000  s-2.local   fe80:e::1c71:618a        0     -        4     -     -
utun2 1380  <Link#15>                            2     0       36     0     0
utun2 1380  s-2.local   fe80:f::d494:4c0e        2     -       36     -     -
utun3 1380  <Link#16>                            0     0        4     0     0
utun3 1380  s-2.local   fe80:10::b7d4:8de        0     -        4     -     -

netstat查看udp连接

netstat -a -p udp

看到了如下输出

Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
udp4       0      0  *.65006                *.*
udp4       0      0  *.49684                *.*
udp4       0      0  *.53824                *.*
udp4       0      0  *.63924                *.*
udp4       0      0  *.52738                *.*
udp4       0      0  *.59184                *.*
udp4       0      0  *.55333                *.*
udp4       0      0  *.52971                *.*
udp4       0      0  *.*                    *.*
udp4       0      0  *.*                    *.*
udp4       0      0  *.61025                *.*
udp4       0      0  *.xserveraid           *.*
udp4       0      0  *.mdns                 *.*
udp6       0      0  *.62311                *.*
udp4       0      0  *.62311                *.*
udp6       0      0  *.63490                *.*
udp4       0      0  *.63490                *.*

linux指令学习20-chmod

用户分组

linux中每个文件和目录都有访问权限,分别是只读、只写、可执行

权限分类

用户权限即自己的权限,用户组权限即同组人的权限,其他权限即和自己不同组的人的权限,所有人的权限即所有人的权限

权限

符号 操作 数字
r 4
w 2
x 执行 1
d 目录
+ 增加权限
- 取消权限
= 赋予权限并取消其他权限

chmodu

修改文件的权限

参考

文件权限中 chmod、u+x、u、r、w、x分别代表什么

Math

两分数间分母最小的分数

给你两个分数,让你找一个分数在他们俩之间,要求分母最小, 这个问题很显然,我们应该转移到Stern Brocot Tree上面去做,对于给定的两个分数,我们把他们在树上标记出来,可能他们不再树的同一层,但是我们可以找到一个合适的层数,并且把他们标记在这一层,可能标记后,他们之间没有其他分数,那我们就选择更深的一层,直到他们在同一层,且中间有其他数字。 这时我们来分析答案在哪,首先很容易证明答案就在他们俩之间的那些分数之间,因为这些分数已经满足了值在他们俩之间,对于另一个要求-分母最小,这就要求我们在这些分数中取出一个分母最小的。 有一个很简单的做法可以帮助我们找到答案,那就是,把这些可能的答案全部标记为红色,真正的答案就是这些标记的lca。 当我们发现答案是lca的时候,我们也发现了另一个现象,分子分母具有轮换对称性当分母取到最小值的时候,分子可能有多个解,如果我们选择了最小的分子,我们将得到一个分数 $\frac{a1}{b1}$ 我们发现如果不考虑分母最小,此时的分子也是所有解中最小的分子。 换句话说,在$(\frac{u}{v},\frac{x}{y})$中所有分母最小的分数中选择一个分子最小的分数和$(\frac{u}{v},\frac{x}{y})$中所有分子最小的分数中选择一个分母最小的分数,选出的结果一定是相同的。 于是我们就可以利用此特征来解决上诉问题了,代码如下,若区间包含了一个整数z,那么答案一定是$\frac{z}{1}$,否则我们可以将区间向左移动,理由是,尽管分子变了,但是区间移动不影响分母的大小,再根据分母最小时的分子最小的答案 等于 分子最小时分母最小的答案 即分母能唯一确定分子,通过区间移动后的分母的最小值推出区间移动前的分母最小值,进而推出区间移动前的分子的最小值,我们就能解决这个问题了。 用辗转相除加速。

void solve(ll u1,ll u2,ll&r1,ll&r2,ll v1,ll v2){ // u1/u2<r1/r2<v1/v2
    if((u1+u2-1)/u2<=v1/v2) r1=(u1+u2-1)/u2,r2=1;
    else{
        ll d=u1/u2; //u1/u2-d<r1/r2-d<v1/v2-d
        solve(v2,v1-v2*d,r2,r1,u2,u1-u2*d);
        r1+=d*r2;
    }
}

积性函数线性筛

### include<bits/stdc++.h>
using namespace std;

/*                        数论函数表
 i phi(i) PHI(i) muu(i) MUU(i) ddd(i) DDD(i) sig(i) SIG(i)
 1   1      1      1      1      1      1      1      1
 2   1      2     -1      0      2      3      3      4
 3   2      4     -1     -1      2      5      4      8
 4   2      6      0     -1      3      8      7     15
 5   4     10     -1     -2      2     10      6     21
 6   2     12      1     -1      4     14     12     33
 7   6     18     -1     -2      2     16      8     41
 8   4     22      0     -2      4     20     15     56
 9   6     28      0     -2      3     23     13     69
10   4     32      1     -1      4     27     18     87
11  10     42     -1     -2      2     29     12     99
12   4     46      0     -2      6     35     28    127
13  12     58     -1     -3      2     37     14    141
14   6     64      1     -2      4     41     24    165
15   8     72      1     -1      4     45     24    189
16   8     80      0     -1      5     50     31    220
17  16     96     -1     -2      2     52     18    238
18   6    102      0     -2      6     58     39    277
19  18    120     -1     -3      2     60     20    297
20   8    128      0     -3      6     66     42    339
21  12    140      1     -2      4     70     32    371
22  10    150      1     -1      4     74     36    407
23  22    172     -1     -2      2     76     24    431
24   8    180      0     -2      8     84     60    491
25  20    200      0     -2      3     87     31    522
26  12    212      1     -1      4     91     42    564
27  18    230      0     -1      4     95     40    604
28  12    242      0     -1      6    101     56    660
29  28    270     -1     -2      2    103     30    690
30   8    278     -1     -3      8    111     72    762*/

/****  * 超级积性函数线性筛 *  ****/
typedef long long ll;
const ll maxn=5e6;
ll no_pri[maxn]={0,1,0},pri[maxn],low[maxn];
ll PHI[maxn],DDD[maxn],XDX[maxn],MUU[maxn],SIG[maxn];
void f_ini(){
    for(ll i=2;i<maxn;i++){
        if(!no_pri[i]) low[i]=pri[++pri[0]]=i;
        for(ll j=1;pri[j]*i<maxn;j++){
            no_pri[pri[j]*i]=1;
            if(i%pri[j]==0) {
                low[pri[j]*i]=low[i]*pri[j];
                break;
            }
            else low[pri[j]*i]=pri[j];
        }
    }

    DDD[1]=PHI[1]=MUU[1]=SIG[1]=1;// 改这里
    for(ll i=1;i<=pri[0];i++){
        for(ll mul=pri[i],ct=1;mul<maxn;mul*=pri[i],ct++){
            DDD[mul]=ct+1;// 改这里
            SIG[mul]=SIG[mul/pri[i]]+mul;// 改这里
            MUU[mul]=ct==1?-1:0;// 改这里
            PHI[mul]=mul/pri[i]*(pri[i]-1);// 改这里
        }
    }

    for(ll i=2;i<maxn;i++){
        for(ll j=1;pri[j]*i<maxn;j++){
            ll x=low[i*pri[j]], y=i*pri[j]/x;
            DDD[x*y]=DDD[x]*DDD[y];
            MUU[x*y]=MUU[x]*MUU[y];
            PHI[x*y]=PHI[x]*PHI[y];
            SIG[x*y]=SIG[x]*SIG[y];
            if(i%pri[j]==0) break;
        }
    }

    for(ll i=1;i<maxn;i++) {
        DDD[i]+=DDD[i-1];
        MUU[i]+=MUU[i-1];
        PHI[i]+=PHI[i-1];
        SIG[i]+=SIG[i-1];
         XDX[i]=(DDD[i]-DDD[i-1])*i+XDX[i-1];
    }
}

int main(){
    f_ini();
    printf("数论函数表\n");
    printf(" i phi(i) PHI(i) muu(i) MUU(i) ddd(i) DDD(i) sig(i) SIG(i)\n");
    for(ll i=1;i<=30;i++) {
        printf("%2lld %3lld %6lld %6lld %6lld %6lld %6lld %6lld %6lld\n",i,PHI[i]-PHI[i-1],PHI[i],MUU[i]-MUU[i-1],MUU[i],DDD[i]-DDD[i-1],DDD[i],SIG[i]-SIG[i-1],SIG[i]);
    }
    return 0;
}

二次剩余

typedef long long ll;
struct cp{
    static ll p,w;
    ll x,y;// x+y\sqrt(w)
    cp(ll x,ll y):x(x),y(y){}
    cp operator*(cp rhs){
        return cp((x*rhs.x+y*rhs.y%p*w)%p,(x*rhs.y+y*rhs.x)%p);
    }
};
ll cp::p,cp::w;

cp qpow(cp a,ll b){
    cp res(1,0);
    for(;b;b>>=1,a=a*a) if(b&1)res=res*a;
    return res;
}
ll qpow(ll a,ll b,ll p){
    ll res=1;
    for(;b;b>>=1,a=a*a%p) if(b&1)res=res*a%p;
    return res;
}
ll sqrt(ll x,ll p){ // return sqrt(x)%p
    if(x==0) return 0;
    if(qpow(x,(p-1)/2,p)==p-1)return -1;
    ll a=1,w=(1-x+p)%p;
    while(qpow(w,(p-1)/2,p)!=p-1) ++a,w=(a*a-x+p)%p;
    cp::w=w,cp::p=p;
    return qpow(cp(a,1),(p+1)/2).x;
}

**剩余定理

### define I __int128
void exgcd(I a,I&x,I b,I&y,I c){ //  assert(__gcd(a,b)==c)
    if(b==0) x=c/a,y=0;
    else exgcd(b,y,a%b,x,c),y-=a/b*x;
}

inline bool merge(I x1,I p1,I x2,I p2,I&x,I&p){
    I a,b,d=__gcd(p1,p2);// ap1+x1=bp2+x2     a+k(p2/gcd)
    if((x2-x1)%d!=0) return false;
    exgcd(p1,a,p2,b,x2-x1);
    p=p1/d*p2; //lcm
    x=((a*p1+x1)%p+p)%p;//
    return true;
}
public class Main {
    static BigInteger[] exgcd(BigInteger a, BigInteger b, BigInteger c) { // ax+by=c  res[0]=x,res[1]=y
        if (b.compareTo(BigInteger.ZERO) == 0) return new BigInteger[]{c.divide(a), BigInteger.ZERO};
        BigInteger[] r = exgcd(b, a.mod(b), c);
        return new BigInteger[]{r[1], r[0].subtract(a.divide(b).multiply(r[1]))};
    }

    static BigInteger[] merge(BigInteger x1, BigInteger p1, BigInteger x2, BigInteger p2) {
        BigInteger d = p1.gcd(p2);
        if (x2.subtract(x1).mod(d).compareTo(BigInteger.ZERO) != 0) return null;
        BigInteger[] r = exgcd(p1, p2, x2.subtract(x1));
        BigInteger p = p1.divide(d).multiply(p2); //     p=p1/d*p2
        BigInteger x=r[0].multiply(p1).add(x1).mod(p).add(p).mod(p);
        return new BigInteger[]{x, p};
    }
}

广义斐波那契循环节

广义斐波那契数递推公式 $$f_i=af_{i-1}+bf_{i-2}(\mod p) (p是奇素数)$$

他的转移矩阵 $$ \left[ \begin{matrix} a & b \ 1 & 0 \end{matrix} \right]^n

\left[ \begin{matrix} f_{2} \ f_{1} \end{matrix} \right]\mod p=

\left[ \begin{matrix} f_{n+2} \ f_{n+1} \end{matrix} \right] $$

如果存在循环节则存在n使得 $$ \left[ \begin{matrix} a & b \ 1 & 0 \end{matrix} \right]^n= \left[ \begin{matrix} k_1p+1 & k_2p+0 \ k_3p+0 & k_4p+1 \end{matrix} \right] $$

我们尝试把左边变成相似对角矩阵 先求特征值 $$ (\lambda-a)\lambda-b=0 \Leftrightarrow \lambda=\frac{a\pm\sqrt{a^2+4b}}{2} $$

当且仅当$a^2+4b=0$的时候,$\lambda_1=\lambda_2$,易证尽管此时$\lambda$是二重特征值,但是它对应的特征向量只有一个,即上诉矩阵不可对角化,我们不考虑这种复杂的情况。 当$a^2+4b\neq0$的时候,两个特征向量分别为 $$ \left[ \begin{matrix} \lambda_1 \ 1 \end{matrix} \right] 和 \left[ \begin{matrix} \lambda_2 \ 1 \end{matrix} \right] $$ 那么就有了 $$ \left[ \begin{matrix} a & b \ 1 & 0 \end{matrix} \right]= \left[ \begin{matrix} \lambda_1 & \lambda_2 \ 1 & 1 \end{matrix} \right] \left[ \begin{matrix} \lambda_1 & 0 \ 0 & \lambda_2 \end{matrix} \right] \left[ \begin{matrix} \frac{1}{\lambda_1-\lambda_2} & \frac{-\lambda_2}{\lambda_1-\lambda_2} \ \frac{-1}{\lambda_1-\lambda_2} & \frac{\lambda_1}{\lambda_1-\lambda_2} \end{matrix} \right] $$ 进而有了 $$ \left[ \begin{matrix} \lambda_1 & \lambda_2 \ 1 & 1 \end{matrix} \right] \left[ \begin{matrix} \lambda_1^n & 0 \ 0 & \lambda_2^n \end{matrix} \right] \left[ \begin{matrix} \frac{1}{\lambda_1-\lambda_2} & \frac{-\lambda_2}{\lambda_1-\lambda_2} \ \frac{-1}{\lambda_1-\lambda_2} & \frac{\lambda_1}{\lambda_1-\lambda_2} \end{matrix} \right]= \left[ \begin{matrix} k_1p+1 & k_2p+0 \ k_3p+0 & k_4p+1 \end{matrix} \right] $$ 右乘$T$消掉那个一堆分数的矩阵 $$ \left[ \begin{matrix} \lambda_1 & \lambda_2 \ 1 & 1 \end{matrix} \right] \left[ \begin{matrix} \lambda_1^n & 0 \ 0 & \lambda_2^n \end{matrix} \right]= \left[ \begin{matrix} k_1p+1 & k_2p+0 \ k_3p+0 & k_4p+1 \end{matrix} \right] \left[ \begin{matrix} \lambda_1 & \lambda_2 \ 1 & 1 \end{matrix} \right] $$ 乘开 $$ \left[ \begin{matrix} \lambda_1^{n+1} & \lambda_2^{n+1} \ \lambda_1^{n} & \lambda_1^{n} \end{matrix} \right]= \left[ \begin{matrix} \lambda_1(k_1p+1)+k_2p & \lambda_2(k_1p+1)+k_2p \ \lambda_1k_3p+k_4p+1 & \lambda_2k_3p+k_4p+1 \end{matrix} \right] $$

在这之后我们分两部分讨论
$a^2+4b是二次剩余$

如果$a^2+4b$是二次剩余,那么$\lambda_1$和$\lambda_2$可以直接写成模意义下对应的整数,则上诉矩阵等式在$n=p-1$的时候根据费马小定理恒成立

$a^2+4b不是二次剩余$

在这里,是绝对不能够直接取模的,因为$\lambda$中一旦包含了根号下非二次剩余,这里就是错的,我们不可以取模,直接用根式表达即可。 ####### 两矩阵相等条件1: $$ \lambda^n=\lambda k_3p+k_4p+1 $$ 先看$\lambda_1$ $$ \lambda_1=\frac{a+\sqrt{a^2+4b}}{2}=\frac{a}{2}+\frac{1}{2}\sqrt{a^2+4b} $$ 则 $$ (\frac{a}{2}+\frac{1}{2}\sqrt{a^2+4b})^n=(\frac{a}{2}+\frac{1}{2}\sqrt{a^2+4b})k_3p+k_4p+1 $$ 分母有点难受,把它移到右边去 $$ (a+\sqrt{a^2+4b})^n=2^n* ((\frac{a}{2}+\frac{1}{2}\sqrt{a^2+4b})k_3p+k_4p+1)\ \sum_{i=0}^nC_n^ia^i\sqrt{a^2+4b}^{n-i}=2^n* (\frac{a}{2}k_3p+k_4p+1)+2^n* (\frac{1}{2}k_3p)\sqrt{a^2+4b} $$ 我们在这里引入一些概念,我们在实数领域解决这个问题,在实数领域,我们把数分为两部分来表示,一部分是有理数部分,称之为有理部,另一部分是无理数部分,称之为无理部,即$1+2\sqrt{16}$中,我们称1为有理部,2位无理部。 上式左边显然能够唯一表示为$x+y\sqrt{a^2+4b}$,那么两式相等的充要条件就是 $$ 存在k_3,k_4使得x=2^n* (\frac{a}{2}k_3p+k_4p+1), y=2^n* \frac{1}{2}k_3p $$ 上面的式子的某个充分条件为 $$ \frac{x}{2^n} \equiv1 \mod p\ \frac{y}{2^n} \equiv0 \mod p $$ 更加具体一点如果n是(p-1)的倍数则下面的式子也是充分条件 $$ x \equiv1 \mod p\ y \equiv0 \mod p $$ 为了利用这点,我们保证后面n一定是p-1的倍数,让我们先遗忘掉这些充分条件

然后我们来看看这个规律,注意到$\sum_{i=0}^nC_n^ia^i\sqrt{a^2+4b}^{n-i}$中,当$n=p且i\neq0且i\neq n$的时候,$C_n^i|p$,所以 $$ x\equiv a^p \equiv a\ y\equiv \sqrt{a^2+4b}^p $$ 即 $$ \begin{aligned} &(a+\sqrt{a^2+4b})^p \=&(a+c_1p)+(\sqrt{a^2+4b}^{p-1}+c_2p)\sqrt{a^2+4b},c_1c_2是整数 \=& (a+c_1p)+((a^2+4b)^{\frac{p-1}{2}}+c_2p)\sqrt{a^2+4b} \=& a+(a^2+4b)^{\frac{p-1}{2}}\sqrt{a^2+4b}+c_1p+c_2p\sqrt{a^2+4b} \end{aligned} $$ 这时候因为$a^2+4b$是一个非二次剩余,所以上式可以表达为 $$ a-\sqrt{a^2+4b}+c_1p+c_2p\sqrt{a^2+4b} $$ 我们让他乘上$\frac{a+\sqrt{a^2+4b}}{2}$,他的无理部就彻底与0同余了,此时的$n=(p+1)$,在让这个数幂上$p-1$,他的有理部就与1同余了,并且我们达到了之前的约定,n是p-1的倍数,此时的$n=(p+1)(p-1)$ ####### 两矩阵相等条件2: $$ \begin{aligned} &\lambda^{n+1}=\lambda(k_1p+1)+k_2p\ \Leftrightarrow&\lambda^{n+1}=(\frac{a}{2}+\frac{1}{2}\sqrt{a^2+4b})(k_1p+1)+k_2p\ \Leftrightarrow&\lambda^{n+1}=(\frac{a}{2}+\frac{1}{2}\sqrt{a^2+4b})+\frac{a}{2}k_1p+\frac{1}{2}\sqrt{a^2+4b}k_1pk_2p+k_2p \end{aligned} $$ 之前我们证明了$\lambda^{(p+1)(p-1)}$的有理部与1同余,无理部与0同余,这里显然$\lambda^{(p+1)(p-1)+1}$的有理部与$\frac{a}{2}$同余,无理部与$\frac{1}{2}$同余, 至于$\lambda_2$是同理的。

至此证明了当$a^2+4b$是二次剩余的时候,循环节至多为$n-1$,当$a^2+4b$不是二次剩余的时候,循环节至多为$n^2-1$ 当$a^2+4b=0$的时候还有待挖掘

类欧几里得算法

先考虑一个简单的问题 $$f(a,b,c,n)=\sum_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor$$ 我们这样来解决 $$ \begin{aligned} \&f(a,b,c,n) \&=\sum_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor \&=f(a%c,b%c,c,n)+\sum_{i=0}^{n}(i\lfloor\frac{a}{c}\rfloor+\lfloor\frac{b}{c}\rfloor) \&=f(a%c,b%c,c,n)+\frac{n(n+1)}{2}\lfloor\frac{a}{c}\rfloor+(n+1)\lfloor\frac{b}{c}\rfloor \ \&令m=\lfloor\frac{an+b}{c}\rfloor \&则f(a,b,c,n) \&=\sum_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor \&=\sum_{i=0}^n\sum_{j=1}^m[\lfloor\frac{ai+b}{c}\rfloor\geq j] \&=\sum_{i=0}^n\sum_{j=0}^{m-1}[\lfloor\frac{ai+b}{c}\rfloor\geq j+1] \&=\sum_{i=0}^n\sum_{j=0}^{m-1}[ai+b \geq cj+c] \&=\sum_{i=0}^n\sum_{j=0}^{m-1}[ai \gt cj+c-b-1] \&=\sum_{i=0}^n\sum_{j=0}^{m-1}[i \gt \lfloor\frac{cj+c-b-1}{a}\rfloor] \&=\sum_{j=0}^{m-1}n-\lfloor\frac{cj+c-b-1}{a}\rfloor \&=nm-f(c,c-b-1,a,m-1) \&可以开始递归,递归出口 m=0 \end{aligned} $$

然后我们考虑两个难一点的题目,同时解决这两个问题 $$ \begin{aligned} &h(a,b,c,n)=\sum_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor^2 \quad\quad\quad\quad g(a,b,c,n)=\sum_{i=0}^ni\lfloor\frac{ai+b}{c}\rfloor \end{aligned} $$

先来看h $$ \begin{aligned} &h(a,b,c,n)\ =&\sum_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor^2\ =&\sum_{i=0}^n(\lfloor\frac{(a%c)i+(b%c) }{c}\rfloor+\lfloor\frac{a}{c}\rfloor i+\lfloor\frac{b}{c}\rfloor)^2\ =&\sum_{i=0}^n(\lfloor\frac{(a%c)i+(b%c) }{c}\rfloor^2+\lfloor\frac{a}{c}\rfloor^2i^2+\lfloor\frac{b}{c}\rfloor^2+2\lfloor\frac{a}{c}\rfloor i\lfloor\frac{b}{c}\rfloor+2\lfloor\frac{(a%c)i+(b%c) }{c}\rfloor\lfloor\frac{a}{c}\rfloor i+2\lfloor\frac{(a%c)i+(b%c) }{c}\rfloor\lfloor\frac{b}{c}\rfloor\ =&h(a%c,b%c,c,n)+2\lfloor\frac{a}{c}\rfloor g(a%c,b%c,c,n)+2\lfloor\frac{b}{c}\rfloor f(a%c,b%c,c,n)+\lfloor\frac{a}{c}\rfloor^2\frac{n(n+1)(2n+1)}{6}+2\lfloor\frac{a}{c}\rfloor \lfloor\frac{b}{c}\rfloor\frac{n(n+1)}{2}+(n+1)\lfloor\frac{b}{c}\rfloor^2 \end{aligned} $$

这里我们只用关心第一项 $$ \begin{aligned} &令m=\lfloor\frac{an+b}{c}\rfloor则\ &h(a,b,c,n)\ =&\sum_{i=0}^n\lfloor\frac{ai+b}{c}\rfloor^2\ =&\sum_{i=0}^n(\sum_{j=1}^m[\lfloor\frac{ai+b}{c}\rfloor\geq j])^2\ =&\sum_{i=0}^n(\sum_{j=0}^{m-1}[i \gt \lfloor\frac{cj+c-b-1}{a}\rfloor])^2\ =&\sum_{i=0}^n\sum_{j=0}^{m-1}[i \gt \lfloor\frac{cj+c-b-1}{a}\rfloor]\sum_{k=0}^{m-1}[i \gt \lfloor\frac{ck+c-b-1}{a}\rfloor]\ =&\sum_{i=0}^n\sum_{j=0}^{m-1}\sum_{k=0}^{m-1}[i \gt \lfloor\frac{cj+c-b-1}{a}\rfloor][i \gt \lfloor\frac{ck+c-b-1}{a}\rfloor]\ =&\sum_{i=0}^n\sum_{j=0}^{m-1}\sum_{k=0}^{m-1}[i \gt max(\lfloor\frac{cj+c-b-1}{a}\rfloor,\lfloor\frac{ck+c-b-1}{a}\rfloor)]\ =&\sum_{i=0}^n\sum_{j=0}^{m-1}\sum_{k=0}^{m-1}[i \gt max(\lfloor\frac{cj+c-b-1}{a}\rfloor,\lfloor\frac{ck+c-b-1}{a}\rfloor)]\ =&nm^2-\sum_{j=0}^{m-1}\sum_{k=0}^{m-1} max(\lfloor\frac{cj+c-b-1}{a}\rfloor,\lfloor\frac{ck+c-b-1}{a}\rfloor)\ =&nm^2-2\sum_{j=0}^{m-1}j\lfloor\frac{cj+c-b-1}{a}\rfloor-\sum_{j=0}^{m-1}\lfloor\frac{cj+c-b-1}{a}\rfloor\ =&nm^2-2g(c,c-b-1,a,m-1)-f(c,c-b-1,a,m-1) \end{aligned} $$ 推出来了。。。。。 然后我们来怼g $$ \begin{aligned} &g(a,b,c,n)\ =&\sum_{i=0}^ni\lfloor\frac{ai+b}{c}\rfloor\ =&\sum_{i=0}^ni\lfloor\frac{(a%c)i+b%c}{c}+\lfloor\frac{a}{c}\rfloor i+\lfloor\frac{b}{c}\rfloor\rfloor \ =&\sum_{i=0}^ni(\lfloor\frac{(a%c)i+b%c}{c}\rfloor+\lfloor\frac{a}{c}\rfloor i+\lfloor\frac{b}{c}\rfloor)\ =&\sum_{i=0}^ni\lfloor\frac{(a%c)i+b%c}{c}\rfloor+\sum_{i=0}^n\lfloor\frac{a}{c}\rfloor i^2+\sum_{i=0}^n\lfloor\frac{b}{c}\rfloor i\ =&\frac{n(n+1)(2n+1)}{6}\lfloor\frac{a}{c}\rfloor +\frac{n(n+1)}{2}\lfloor\frac{b}{c}\rfloor +\sum_{i=0}^ni\lfloor\frac{(a%c)i+b%c}{c}\rfloor\ =&g(a%c,b%c,c,n)+\frac{n(n+1)(2n+1)}{6}\lfloor\frac{a}{c}\rfloor +\frac{n(n+1)}{2}\lfloor\frac{b}{c}\rfloor \end{aligned} $$ 同理我们只关心第一项 $$ \begin{aligned} &g(a,b,c,n)\ =&\sum_{i=0}^ni\lfloor\frac{ai+b}{c}\rfloor\ =&\sum_{i=0}^n(i\sum_{j=1}^m[\lfloor\frac{ai+b}{c}\rfloor \geq j])\ =&\sum_{i=0}^n(i\sum_{j=0}^{m-1}[i \gt \lfloor\frac{cj+c-b-1}{a}\rfloor])\ =&\sum_{i=0}^n\sum_{j=0}^{m-1}i[i \gt \lfloor\frac{cj+c-b-1}{a}\rfloor]\ =&\sum_{j=0}^{m-1}\sum_{i=\lfloor\frac{cj+c-b-1}{a}\rfloor+1}^ni\ =&\sum_{j=0}^{m-1}\frac{(n+\lfloor\frac{cj+c-b-1}{a}\rfloor+1)(n-(\lfloor\frac{cj+c-b-1}{a}\rfloor+1)+1)}{2}\ =&\sum_{j=0}^{m-1}\frac{(n+\lfloor\frac{cj+c-b-1}{a}\rfloor+1)*(n-\lfloor\frac{cj+c-b-1}{a}\rfloor)}{2}\ =&\sum_{j=0}^{m-1}\frac{n^2-\lfloor\frac{cj+c-b-1}{a}\rfloo