派大星的博客

很多事情不是会了才能做,而是做了才能学会


  • 首页

  • 标签

  • 分类

  • 关于

  • 搜索

使用docker搭建MySQL主从架构集群

发表于 2019-08-29 | 分类于 数据库

拉取mysql镜像

这里拉取的是mysql 5.7版本的镜像

1
docker pull mysql:5.7

启动两个MySQL容器

启动两个MySQL容器,一个作为master,一个作为slave

master节点,将本地的 3307端口映射到容器的3306端口,密码为123456

1
docker run -p 3307:3306 --name mysql-master -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

slave节点,将本地的 3308端口映射到容器的3306端口,密码为123456

1
docker run -p 3308:3306 --name mysql-slave -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

启动完成之后,可通过docker ps 查看是否启动成功。

登录到mysql中

登录到master节点中

1
mysql -uroot -h127.0.0.1 -P3307 -p123456

再启动一个命令窗口,进入到 slave节点

1
mysql -uroot -h127.0.0.1 -P3308 -p123456

配置master

通过docker exec进入容器内部

1
docker exec -it mysql-master /bin/bash

修改my.cnf文件

1
echo -e "[mysqld]\nserver-id=100\nlog-bin=mysql-bin" >> etc/mysql/my.cnf

其实也就是新增下面两句配置

1
2
3
4
5
[mysqld]
## 同一局域网内注意要唯一
server-id=100
## 开启二进制日志功能,可以随便取(关键)
log-bin=mysql-bin

重启mysql服务

1
service mysql restart

重启时容器会退出,我们需要重新启动一次容器

1
docker start mysql-master

配置slave

和master一样,修改my.cnf文件

1
echo -e "[mysqld]\nserver-id=101\nlog-bin=mysql-slave-bin\nrelay_log=edu-mysql-relay-bin" >> etc/mysql/my.cnf

其实也就在文件中加入下面的配置

1
2
3
4
5
6
7
[mysqld]
## 设置server_id,注意要唯一
server-id=101
## 开启二进制日志功能,以备Slave作为其它Slave的Master时使用
log-bin=mysql-slave-bin
## relay_log配置中继日志
relay_log=edu-mysql-relay-bin

然后重启mysql服务

1
service mysql restart

重启过程中,容器会自动退出,我们需要重新启动一次容器

1
docker start mysql-master

下一步在Master数据库创建数据同步用户,授予用户
slave REPLICATION SLAVE权限和REPLICATION CLIENT权限,用于在主从库之间同步数据。

1
2
CREATE USER 'slave'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%';

将slave链接到master

先去master中查看下binlog的状态

1
mysql -uroot -h127.0.0.1 -P3307 -p123456

查看状态

1
show master status;

File和Position字段的值后面将会用到,在后面的操作完成之前,需要保证Master库不能做任何操作,否则将会引起状态变化,File和Position字段的值也会变化。

进入到slave中

1
mysql -uroot -h127.0.0.1 -P3308 -p123456

链接到master

1
2
3
4
5
6
7
change master to master_host='172.17.0.2', 
master_user='slave',
master_password='123456',
master_port=3306,
master_log_file='mysql-bin.000001',
master_log_pos= 0,
master_connect_retry=30;

注:

  • master_host Master的地址,指的是容器的独立ip,可以通过下面命令查看

    1
    docker inspect --format='{{.NetworkSettings.IPAddress}}'  mysql-master
  • master_port 容器的端口号,指的是容器内部运行mysql的端口号

  • master_user 是我们在master中配置的用户主从复制的用户名

  • master_log_file 是上一步中从master中查出File字段的值

  • master_log_pos 是复制起始点,如果为0,为从头复制

开启主从复制

在 slave 中查看主从复制状态,可以看到目前主从复制是还没有开启的。

1
show slave status \G;

开启主从复制

1
start slave;

再次查看,我们看到主从复制已经开启了。

测试主从复制

我们在master中创建一个数据库,看是否在slave中也会创建

进入到master中的mysql执行创建数据库命令

1
create database test default character set utf8;

然后进入到slave节点中查看是否有该数据库

1
show databases;


我们看到从库中已经创建了test数据库,至此我们的主从MySQL集群架构已经搭建完毕了。

使用GitHub创建个人官网

发表于 2019-08-23 | 分类于 工具使用

1.新建项目

项目名称为:你的github用户名.github.io
项目名字必须叫这个,比如我的GitHub用户名叫pibigstar
那么新建项目为:pibigstar.github.io

2. 将网页上传到这个项目即可

1
2
3
4
5
6

git add .

git commit -m "first commit"

git push origin master

3. 访问

访问地址为:

https://pibigstar.github.io

JVM学习总结

发表于 2019-08-21 | 分类于 Java

转载自:https://github.com/crossoverJie/Java-Interview

1. Java 运行时的内存划分

1.1 程序计数器

记录当前线程所执行的字节码行号,用于获取下一条执行的字节码。

当多线程运行时,每个线程切换后需要知道上一次所运行的状态、位置。由此也可以看出程序计数器是每个线程私有的。

1.2 虚拟机栈

虚拟机栈是有一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。

每一个栈帧由局部变量区、操作数栈等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。

如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 stackoverflow 异常。
这块内存区域也是线程私有的。

1.3 Java 堆

Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。

这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用分代回收算法,所有堆内存也分为 新生代、老年代,可以方便垃圾的准确回收。

这块内存属于线程共享区域。

1.4 方法区

方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量。 这块区域也被称为永久代。

1.5 运行时常量池

运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用。

2. 类的加载机制

2.1 双亲委派模型

模型如下图:

双亲委派模型中除了启动类加载器之外其余都需要有自己的父类加载器

当一个类收到了类加载请求时: 自己不会首先加载,而是委派给父加载器进行加载,每个层次的加载器都是这样。

所以最终每个加载请求都会经过启动类加载器。只有当父类加载返回不能加载时子加载器才会进行加载。

双亲委派的好处 : 由于每个类加载都会经过最顶层的启动类加载器,比如 java.lang.Object这样的类在各个类加载器下都是同一个类(只有当两个类是由同一个类加载器加载的才有意义,这两个类才相等。)

如果没有双亲委派模型,由各个类加载器自行加载的话。当用户自己编写了一个 java.lang.Object类,那样系统中就会出现多个 Object,这样 Java 程序中最基本的行为都无法保证,程序会变的非常混乱。

3. 垃圾回收

垃圾回收主要思考三件事情:

  • 哪种内存需要回收?
  • 什么时候回收?
  • 怎么回收?

3.1 对象是否存活

引用计数法

这是一种非常简单易理解的回收算法。每当有一个地方引用一个对象的时候则在引用计数器上 +1,当失效的时候就 -1,无论什么时候计数器为 0 的时候则认为该对象死亡可以回收了。

这种算法虽然简单高效,但是却无法解决循环引用的问题,因此 Java 虚拟机并没有采用这种算法。

可达性分析算法

主流的语言其实都是采用可达性分析算法:

可达性算法是通过一个称为 GC Roots 的对象向下搜索,整个搜索路径就称为引用链,当一个对象到 GC Roots 没有任何引用链 JVM 就认为该对象是可以被回收的。

如图:Object1、2、3、4 都是存活的对象,而 Object5、6、7都是可回收对象。

可以用作 GC-Roots 的对象有:

  • 方法区中静态变量所引用的对象。
  • 虚拟机栈中所引用的对象。

3.2 垃圾回收算法

标记-清除算法

标记清除算法分为两个步骤,标记和清除。 首先将需要回收的对象标记起来,然后统一清除。但是存在两个主要的问题:

标记和清除的效率都不高。
清除之后容易出现不连续内存,当需要分配一个较大内存时就不得不需要进行一次垃圾回收。
标记清除过程如下:

复制算法

复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,然后对之前的区域进行全部回收。

这样简单高效,而且还不存在标记清除算法中的内存碎片问题,但就是有点浪费内存。

在新生代会使用该算法。
新生代中分为一个 Eden 区和两个 Survivor 区。通常两个区域的比例是 8:1:1 ,使用时会用到 Eden 区和其中一个 Survivor 区。当发生回收时则会将还存活的对象从 Eden ,Survivor 区拷贝到另一个 Survivor 区,当该区域内存也不足时则会使用分配担保利用老年代来存放内存。

复制算法过程:

标记整理算法

复制算法如果在存活对象较多时效率明显会降低,特别是在老年代中并没有多余的内存区域可以提供内存担保。

所以老年代中使用的时候分配整理算法,它的原理和分配清除算法类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。

分代回收算法

现代多数的商用 JVM 的垃圾收集器都是采用的分代回收算法,和之前所提到的算法并没有新的内容。

只是将 Java 堆分为了新生代和老年代。由于新生代中存活对象较少,所以采用复制算法,简单高效。

而老年代中对象较多,并且没有可以担保的内存区域,所以一般采用标记清除或者是标记整理算法。

4. OOM 分析

4.1 Java 堆内存溢出

在 Java 堆中只要不断的创建对象,并且 GC-Roots 到对象之间存在引用链,这样 JVM 就不会回收对象。

只要将-Xms(最小堆),-Xmx(最大堆) 设置为一样禁止自动扩展堆内存。

当使用一个 while(true) 循环来不断创建对象就会发生 OutOfMemory,还可以使用 -XX:+HeapDumpOutofMemoryErorr 当发生 OOM 时会自动 dump 堆栈到文件中。

伪代码:

1
2
3
4
5
6
public static void main(String[] args) {
List<String> list = new ArrayList<>(10) ;
while (true){
list.add("1") ;
}
}

当出现 OOM 时可以通过工具来分析 GC-Roots 引用链 ,查看对象和 GC-Roots 是如何进行关联的,是否存在对象的生命周期过长,或者是这些对象确实改存在的,那就要考虑将堆内存调大了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.crossoverjie.oom.HeapOOM.main(HeapOOM.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 1

java.lang.OutOfMemoryError: Java heap space表示堆内存溢出。

4.2 MetaSpace (元数据) 内存溢出

JDK8 中将永久代移除,使用 MetaSpace 来保存类加载之后的类信息,字符串常量池也被移动到 Java 堆。

PermSize 和 MaxPermSize 已经不能使用了,在 JDK8 中配置这两个参数将会发出警告。

JDK 8 中将类信息移到到了本地堆内存(Native Heap)中,将原有的永久代移动到了本地堆中成为 MetaSpace ,如果不指定该区域的大小,JVM 将会动态的调整。

可以使用-XX:MaxMetaspaceSize=10M 来限制最大元数据。这样当不停的创建类时将会占满该区域并出现 OOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
while (true){
Enhancer enhancer = new Enhancer() ;
enhancer.setSuperclass(HeapOOM.class);
enhancer.setUseCache(false) ;
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,objects) ;
}
});
enhancer.create() ;

}
}

使用 cglib 不停的创建新类,最终会抛出:

1
2
3
4
5
6
7
8
9
10
11
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
... 11 more
Caused by: java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
... 16 more

注意:这里的 OOM 伴随的是 java.lang.OutOfMemoryError: Metaspace 也就是元数据溢出。

5. 对象的创建与内存分配

5.1 创建对象

当 JVM 收到一个 new 指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被加载过了,如果没有的话则要进行一次类加载。

接着就是分配内存了,通常有两种方式:

  • 指针碰撞
  • 空闲列表

使用指针碰撞的前提是堆内存是完全工整的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可。

当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式。虚拟机会维护一个空闲的列表,用于记录哪些内存是可以进行分配的,分配时直接从可用内存中直接分配即可。

堆中的内存是否工整是有垃圾收集器来决定的,如果带有压缩功能的垃圾收集器就是采用指针碰撞的方式来进行内存分配的。

分配内存时也会出现并发问题:

这样可以在创建对象的时候使用 CAS 这样的乐观锁来保证。

也可以将内存分配安排在每个线程独有的空间进行,每个线程首先在堆内存中分配一小块内存,称为本地分配缓存(TLAB : Thread Local Allocation Buffer)。

分配内存时,只需要在自己的分配缓存中分配即可,由于这个内存区域是线程私有的,所以不会出现并发问题。

可以使用 -XX:+/-UseTLAB 参数来设定 JVM 是否开启 TLAB 。

内存分配之后需要对该对象进行设置,如对象头。对象头的一些应用可以查看 Synchronize 关键字原理。

对象访问

一个对象被创建之后自然是为了使用,在 Java 中是通过栈来引用堆内存中的对象来进行操作的。

对于我们常用的 HotSpot 虚拟机来说,这样引用关系是通过直接指针来关联的。

如图:

这样的好处就是:在 Java 里进行频繁的对象访问可以提升访问速度(相对于使用句柄池来说)。

5.2 内存分配

####Eden 区分配

简单的来说对象都是在堆内存中分配的,往细一点看则是优先在 Eden 区分配。

这里就涉及到堆内存的划分了,为了方便垃圾回收,JVM 将堆内存分为新生代和老年代。

而新生代中又会划分为 Eden 区,from Survivor、to Survivor区。

其中 Eden 和 Survivor 区的比例默认是8:1:1,当然也支持参数调整 -XX:SurvivorRatio=8。

当在 Eden 区分配内存不足时,则会发生 minorGC ,由于 Java 对象多数是朝生夕灭的特性,所以 minorGC 通常会比较频繁,效率也比较高。

当发生 minorGC 时,JVM 会根据复制算法将存活的对象拷贝到另一个未使用的 Survivor 区,如果 Survivor 区内存不足时,则会使用分配担保策略将对象移动到老年代中。

谈到 minorGC 时,就不得不提到 fullGC(majorGC) ,这是指发生在老年代的 GC ,不论是效率还是速度都比 minorGC 慢的多,回收时还会发生 stop the world 使程序发生停顿,所以应当尽量避免发生 fullGC 。

老年代分配

也有一些情况会导致对象直接在老年代分配,比如当分配一个大对象时(大的数组,很长的字符串),由于 Eden 区没有足够大的连续空间来分配时,会导致提前触发一次 GC,所以尽量别频繁的创建大对象。

因此 JVM 会根据一个阈值来判断大于该阈值对象直接分配到老年代,这样可以避免在新生代频繁的发生 GC。

对于一些在新生代的老对象 JVM 也会根据某种机制移动到老年代中。

JVM 是根据记录对象年龄的方式来判断该对象是否应该移动到老年代,根据新生代的复制算法,当一个对象被移动到 Survivor 区之后 JVM 就给该对象的年龄记为1,每当熬过一次 minorGC 后对象的年龄就 +1 ,直到达到阈值(默认为15)就移动到老年代中。

可以使用-XX:MaxTenuringThreshold=15 来配置这个阈值。

###总结

虽说这些内容略显枯燥,但当应用发生不正常的 GC 时,可以方便更快的定位问题。

6. volatile 关键字

前言

不管是在面试还是实际开发中 volatile都是一个应该掌握的技能。

首先来看看为什么会出现这个关键字。

6.1 内存可见性


由于 Java 内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。

线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。

这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如下图所示:

所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。

显然这肯定是会出问题的,因此 volatile 的作用出现了:

当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。

volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。

内存可见性的应用

当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile 来修饰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Volatile implements Runnable{

private static volatile boolean flag = true ;

@Override
public void run() {
while (flag){
System.out.println(Thread.currentThread().getName() + "正在运行。。。");
}
System.out.println(Thread.currentThread().getName() +"执行完毕");
}

public static void main(String[] args) throws InterruptedException {
Volatile aVolatile = new Volatile();
new Thread(aVolatile,"thread A").start();


System.out.println("main 线程正在运行") ;

TimeUnit.MILLISECONDS.sleep(100) ;

aVolatile.stopThread();

}

private void stopThread(){
flag = false ;
}
}

主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。

但这里有个误区,这样的使用方式容易给人的感觉是:

对 volatile 修饰的变量进行并发操作是线程安全的。
这里要重点强调,volatile 并不能保证线程安全性(不能保证其原子性)!

如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class VolatileInc implements Runnable{

private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性

//private static AtomicInteger count = new AtomicInteger() ;

@Override
public void run() {
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet() ;
}
}

public static void main(String[] args) throws InterruptedException {
VolatileInc volatileInc = new VolatileInc() ;
Thread t1 = new Thread(volatileInc,"t1") ;
Thread t2 = new Thread(volatileInc,"t2") ;
t1.start();
//t1.join();

t2.start();
//t2.join();
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet();
}


System.out.println("最终Count="+count);
}
}

当我们三个线程(t1,t2,main)同时对一个 int 进行累加时会发现最终的值都会小于 30000。

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。

也可以使用 synchronize 或者是锁的方式来保证原子性。

还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

6.2 指令重排

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。

举一个伪代码:

1
2
3
int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3

一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。

如果下一段的语句没有使用上一段语句的结果,JVM就认为两句语句是没有关联的就可能进行重排,JVM不会保证代码的执行顺序,但会保证最终的结果都是一样的

可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。

可能这里还看不出有什么问题,那看下一段伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static Map<String,String> value ;
private static volatile boolean flag = fasle ;

//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
value = getMapValue() ;//1
flag = true ;//2
}


//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}

这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value 都还没有被初始化就有可能被线程 B 使用了。

所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {

private static volatile Singleton singleton;

private Singleton() {
}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}

这里的 volatile 关键字主要是为了防止指令重排。

如果不用 ,singleton = new Singleton();,这段代码其实是分为三步:

  • 分配内存空间。(1)
  • 初始化对象。(2)
  • 将 singleton 对象指向分配的内存地址。(3)

加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

总结

volatile 在 Java 并发中用的很多,比如像Atomic包中的 value、以及 AbstractQueuedLongSynchronizer中的 state都是被定义为 volatile 来用于保证内存可见性。

将这块理解透彻对我们编写并发程序时可以提供很大帮助。

QQ每天定时领取群礼物

发表于 2019-08-21 | 分类于 go

本脚本采用Go语言进行编写,主要原因是简单粗暴,几行代码就搞定了,后面部署也非常方便

目录结构

1. 代码编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"encoding/json"
"fmt"
"github.com/robfig/cron"
"io/ioutil"
"net/http"
"strings"
"time"
)

type Gift struct {
Name string `json:"name"`
}


func main() {
c := cron.New()
// 每天凌晨5点执行一次
spec := "0 0 5 * * ?"
c.AddFunc(spec, func() {
client := &http.Client{}

req, err := http.NewRequest("POST", "https://pay.qun.qq.com/cgi-bin/group_pay/good_feeds/draw_lucky_gift", strings.NewReader("bkn=183506344&from=0&gc=40636692&client=1&version=7.7.0.3645"))
if err != nil {
fmt.Println("构建request失败")
}

req.Header.Set("Content-Type", "application/json;charset=utf-8")
req.Header.Set("Cookie", getCookie())

resp, err := client.Do(req)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("请求内容失败")
}
defer resp.Body.Close()
var gift = Gift{}
err = json.Unmarshal(body, &gift)
if err!=nil {
fmt.Println("JSON解析失败",err.Error())
}
now := time.Now()
date := now.Format("2006-01-02")
fmt.Printf(date+" 获得礼物:"+gift.Name)
})
c.Start()

select{}
}

func getCookie() string {
bytes, err := ioutil.ReadFile("cookie")
if err != nil {
fmt.Println("读取Cookie失败:",err.Error())
}
return string(bytes)
}

2. 编译

为了能跑在Linux上,所以这里要把它编译为在Linux上可执行文件。

新建脚本 build.bat,输入下面内容

1
2
3
4
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build auto_get_gift.go

执行编译

3. 部署

  1. 把生成的auto_get_gift和cookie上传到Linux服务器上

  2. 添加可执行权限

    1
    chmod +x auto_get_gift
  3. 启动

    1
    nohup ./auto_get_gift > gift.log &
  4. 查看是否在运行

    1
    jobs -l

SpringBoot上传下载文件

发表于 2019-08-17 | 分类于 springboot,SpringBoot技能大全

1 配置文件

1
2
3
4
5
6
7
########## 文件上传配置 #########
spring:
servlet:
multipart:
enabled: true # 开启多文件上传
max-file-size: 5MB
max-request-size: 5MB

2 Controller编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package com.pibigstar.web;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.pibigstar.common.Constant;

/**
* 文件上传Controller
* @author pibigstar
*
*/
@Controller()
@RequestMapping("/file")
public class FileController {

private final String upload_path = Constant.DEFAULT_FILE_UPLOAD_PATH;

@GetMapping("toUpload")
public String index() {
return "upload";
}

/**
* 上传
* @param file
* @param redirectAttributes
* @return
*/
@PostMapping("upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "请选择文件进行上传");
return "redirect:/file/uploadStatus";
}

try {
byte[] bytes = file.getBytes();
Path path = Paths.get(upload_path+file.getOriginalFilename());
Files.write(path, bytes);

redirectAttributes.addFlashAttribute("message","成功上传文件: '" + file.getOriginalFilename() + "'");

} catch (IOException e) {
e.printStackTrace();
}

return "redirect:/file/uploadStatus";
}

/**
* 下载
* @param res
* @throws IOException
*/
@GetMapping("download")
public void download(HttpServletResponse res) throws IOException {

String fileName = "Main.jpg";
res.setHeader("content-type", "application/octet-stream");
res.setContentType("application/octet-stream");
res.setHeader("Content-Disposition", "attachment;filename=" + fileName);
byte[] buff = new byte[1024];
BufferedInputStream bis = null;
OutputStream os = null;
try {
os = res.getOutputStream();
bis = new BufferedInputStream(new FileInputStream(new File("E://temp/"+ fileName)));
int i = bis.read(buff);
while (i != -1) {
os.write(buff, 0, buff.length);
os.flush();
i = bis.read(buff);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("success");
}

@GetMapping("uploadStatus")
public String uploadStatus() {
return "uploadStatus";
}
}

3 前台

upload.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>

<h1>文件上传示例</h1>

<form method="POST" action="/file/upload" enctype="multipart/form-data">
<input type="file" name="file" /><br/><br/>
<input type="submit" value="Submit" />
</form>

</body>
</html>

uploadStatus.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>

<h1>上传成功</h1>

<div th:if="${message}">
<h2 th:text="${message}"/>
</div>

<div th:if="${path}">
<h2 th:text="${path}"/>
</div>

</body>
</html>

SpringBoot读取配置文件并注入到静态变量中

发表于 2019-08-17 | 分类于 springboot,SpringBoot技能大全

1 读取配置文件到常量中

大家熟知的方式是将配置文件注入到一个bean中去访问,但是这种方式每次使用这个bean都要写一个注入@Autowired去引用这个bean不是很方便,如果将配置文件注入到一个配置常量用,那么每次访问用Constant.NAME就可以了,这样是不是方便了很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.pibigstar.common;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
* 常量
* @author pibigstar
*
*/
@Configuration
public class Constant {
//此数据是读取的配置文件
public static String DEFAULT_FILE_UPLOAD_PATH;
//注入
@Autowired(required = false)
public void setUploadPath(@Value("${parsevip.file.path}")String DEFAULT_FILE_UPLOAD_PATH) {
Constant.DEFAULT_FILE_UPLOAD_PATH = DEFAULT_FILE_UPLOAD_PATH;
}

}

配置文件application.yml中:

1
2
3
parsevip:
file:
path: D://upload/

使用

1
2
//任意处
String filePath = Constant.DEFAULT_FILE_UPLOAD_PATH;

2 读取自定义的配置文件

那我们可不可以读取自定义的配置文件呢,答案是肯定的,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.pibigstar.common.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

/**
* 读取配置文件的自定义常量
* @author pibigstar
*
*/
@Configuration
@PropertySource("classpath:myconfig.properties")
@ConfigurationProperties(prefix="parsevip")
public class MyConfiguration {

private String filePath;

public String getFilePath() {
return filePath;
}

public void setFilePath(String filePath) {
this.filePath = filePath;
}

}

使用:

1
2
@Autowired
private MyConfiguration myConfig;

myconfig.properties (将此文件放到src/main/resources目录下)

1
parsevip.filePath=D://temp

有一点要注意:用此方法必须为*.properties 文件,不能是yml文件,不知道yml文件应该怎么读取,有知道的麻烦留言告诉我一声,不胜感激。。。。

yml文件操作

用两个类去实现,一个类实现配置自动注入,另一个使用常量引用它

1
2
3
4
5
6
7
8
@Component("DefaultProperties")
@ConfigurationProperties(prefix = MessageProperties.MESSAGE_PREFIX)
public class DefaultProperties {
public static final String MESSAGE_PREFIX = "app.default";
private String name;
private String version;
// setter,getter方法
}

引用它

1
2
3
4
5
public class Constants {
private static final DefaultProperties properties = BeanFactory.getBean("DefaultProperties");
public static final String NAME = properties.getName();
public static final String VERSION = properties.getVersion();
}

yml文件

1
2
3
4
app:
default:
name: test
version: 1.0

使用

1
Constants.NAME

NodeJs安装使用教程

发表于 2019-08-17 | 分类于 nodejs

安装nvm

它是用来管理本机多个nodejs的,使用它可以很方便的下载其他版本的nodejs,并在这些版本之间来回切换
下载地址:https://github.com/coreybutler/nvm-windows/releases

使用

  1. 安装最新版本的nodejs

    1
    nvm insatll latest
  2. 切换到指定版本

    1
    nvm use 12.6.0
  3. 查看当前有哪些node

    1
    nvm list
  4. 安装最新版本

    1
    nvm install stable

1.下载

官网下载 :https://nodejs.org/en/download/

下载好之后,一路next即可

2. 更改配置

2.1 更改目录

2.1.1 更改缓存目录

1
npm config set cache "D:\nodejs\node_cache"

2.1.2 更改全局安装目录

以后安装的模块都会安装到此目录下

1
npm config set prefix "D:\nodejs\node_global"

2.2 更改镜像

设置为国内的镜像,以便于更快的下载模块

1
npm config set registry=http://registry.npm.taobao.org

2.3 查看当前配置

1
npm config list

3. 安装一些常用模块

3.1 安装最新的npm

加-g参数是全局安装,也就是将模块安装到D:\nodejs\node_global目录下

1
npm install npm -g

3.2 安装vue

1
npm install vue -g

3.3 安装es-lint

这是一个代码规范检查工具

1
npm install eslint -g

4. 查看当前已安装模块

1
npm list -global

sed和awk常用操作

发表于 2019-08-11 | 分类于 Linux,shell

@[toc]

1. sed命令说明

sed是流式文本处理,它是对文本进行一行一行的处理,处理完一行之后,再读取下一行,sed默认不会修改源文件的

命令格式:

1
sed 参数 '动作' 文本文件

参数有:

  • -n 仅显示sed处理后的结果。
  • -i 将修改保存到源文件中
  • -e <script> 以选项中指定的script来处理输入的文本文件。
  • -f<script文件> 以选项中指定的script文件来处理输入的文本文件。

动作有:

动静基本和 vim 中一样,a 是新增,i 是插入,d 是删除,s 是替换

  • a:新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)~
  • i:插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行);
  • s:替换,替换字符串,需要使用分隔符把新旧文本分开,分隔符可以为:/ , #
  • d:删除,因为是删除啊,所以 d 后面通常不接任何咚咚;
  • c:取代, c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行!
  • p:打印,亦即将某个选择的数据印出。通常 p 会与参数sed -n一起运行
  • q:退出, 退出sed处理,一般用来找到第一个匹配项就退出处理了

eg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 打印第10行,p 一般和 -n配合使用
sed -n '10p' test.txt

# 打印1到10行
sed -n '1,10p' test.txt

# 每隔2行查看,查看1,3,5....行
sed -n '1~2p' test.txt

# 新增,在第1行后新增 ========
sed '1a ========' test.txt

# 插入,在1行前面插入 =======
sed '1i =======' test.txt

# 删除, 删除最后一行,$表示最后一行
sed '$d' test.txt

# 退出,找到pibigstar就退出
sed '/pibigstar/q' test.txt

# 替换,一行只会替换第一个匹配的,后面的不会被替换
sed 's/dev/prod/' test.txt

# 全局替换,添加 -g,那么一行中如果符合将全部替换
sed 's/dev/prod/g' test.txt

一般先不用-i参数,当我们执行 sed之后会显示执行后的结果,如果结果是符合我们预期的,那么再加上 -i 参数来达到修改源文件的目的

1.2 Sed高级操作

  • {}:多个动作,{}中可放多个sed动作,用 ;分开
  • &:替换固定字符串,主要是优化替换操作,& 代表前面替换的字符
  • \u\U:大小写转换,\u将其转换为小写,\U将其转换为大写
  • ():取括号中的值,替换时可根据\1来标识取那个()中的值,\2取第二个括号中的值,注意使用时都是需要用\( \),其实主要也是为了优化替换操作
    1
    2
    3
    4
    5
    6
    7
    8
    # 多个动作,先删除第一行,然后将后面的 dev 替换为 prod
    sed '{1d;s/dev/prod/g}' test.txt
    # & 操作,将 = 后面添加一个空格
    sed 's/=/& /' test.txt
    # \u操作,将ROOT转换为root
    sed 's/ROOT/\u&/' test.txt
    # ()操作,取出username
    sed 's/\([a-z_-]\+\):.*$/\1/' passwd

1.3 Sed常用命令

1
2
3
4
5
# 配置文件尾添加配置
sed '$a port=8080 \nhost=127.0.0.1' test.txt

# 删除文本空行
sed '^$d' test.txt

2. awk命令说明

awk的命令格式

1
awk -F '分隔符' '命令' file

内置函数
$0: 代表所有域
$1: 代码第一个域,下面依次类推 $2,就是第二个域
NR: 行号
NF: 字符数
print: 打印
printf: 格式化打印

2.1 切换分割符

-F 是可选参数,可以不写,不写的话,默认分隔符为空格

以冒号:作为分隔符

1
awk -F ':' '{print $1}' /etc/passwd

{} 中可直接可以放代码块,if…else…

1
awk -F ':' '{printf("user:%s Line:%s Col:%s \n",$1,NR,NF)}' /etc/passwd

2.2 条件输出

初次之外我们还可以有条件的将其输出

1
2
# 当$3 大于 100 才输出
awk -F ':' '$3>100{print $1}' /etc/passwd

2.3 BEGIN…END循环

统计文件夹下所有文件一共多大

1
ls -l | awk 'BEGIN{size=0}{size+=$5}END{print "all size:"size/1024/1024"M"}'

我们将一行文本按分隔符分隔之后,第一个就是 第一个域,下面依次类推
举个栗子:

1
2
3
4
5
root     pts/1   192.168.1.100  Tue Feb 10 11:21   still logged in
root pts/1 192.168.1.100 Tue Feb 10 00:46 - 02:28 (01:41)
root pts/1 192.168.1.100 Mon Feb 9 11:41 - 18:30 (06:48)
dmtsai pts/1 192.168.1.100 Mon Feb 9 11:41 - 11:41 (00:00)
root tty1 Fri Sep 5 14:09 - 14:10 (00:01)

将上面的信息保存为test.txt文件,我们执行下面命令

1
awk '{print $1}' test.txt

我们没有指定分隔符,那么就是以 空格 进行分隔的,那么输出结果就会是

1
2
3
4
5
root
root
root
dmtsai
root

awk 是逐行进行分析,我们分析第一行就可以了

1
root     pts/1   192.168.1.100  Tue Feb 10 11:21   still logged in

按分隔符进行分隔,那么

1
2
域1       域2		    域3			域4				   域5
root pts/1 192.168.1.100 Tue Feb 10 11:21 still logged in

2.4 实例

查问某文件中的关键字

1
awk '/root/' /etc/passwd

其中 / / 之间可以使用正则表达式

1
awk '/^root/' /etc/passwd

查看本地所有运行的端口

1
netstat -na | grep LISTENING | awk '{print $2}' | awk -F: '{print $2}'

go语言实现服务器与微信公众平台对接

发表于 2019-08-11 | 分类于 go,微信小程序

微信对接文档:点击查看

在这不得不吐槽一下,写的什么玩意文档,给的例子真他么垃圾,文档说是返回echostr,例子给返回个true,false要不要这么误导人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type GetSignature struct {
Signature string `json:"signature"` //微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
Timestamp string `json:"timestamp"` //时间戳
Nonce string `json:"nonce"` //随机数
Echostr string `json:"echostr"` //随机字符串
}

func signature(r *ghttp.Request) {
getSignature := new(GetSignature)
r.GetToStruct(getSignature)
sign := getSignature.Signature
nonce := getSignature.Nonce
timestamp := getSignature.Timestamp
echostr := getSignature.Echostr
strs := []string{token, timestamp, nonce}
sort.Strings(strs)

tempStr := fmt.Sprintf("%s%s%s", strs[0], strs[1], strs[2])
h := sha1.New()
io.WriteString(h, tempStr)
result := fmt.Sprintf("%x", h.Sum(nil))
if result != sign {
// 等不等于都让它返回正确的结果,zz验证
r.Response.Write(echostr)
return
}
r.Response.Write(echostr)
}

完全搞不懂微信他们到底要搞什么骚操作,这是什么垃圾验证,中间做了一大堆算法,其实一点屁用都没有,直接用下面这个粗暴的版本也可以:

1
2
3
4
5
6
7
func signature(r *ghttp.Request) {
getSignature := new(GetSignature)
r.GetToStruct(getSignature)
echostr := getSignature.Echostr
// 直接返回就可以了
r.Response.Write(echostr)
}

GoLand文件格式化配置

发表于 2019-08-11 | 分类于 go

@[toc]

1. Go fmt配置


Arguments: -w $FilePath$

2. golangci-lint静态代码检查

2.1 安装

1
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint

2.2 配置


3. Proto格式化

3.1 下载clang-format

在这个网址进行下载:http://releases.llvm.org/download.html

文件有点大,一百多M,下载之后安装即可

3.2 配置clang-format

Arguments上面输入:

1
2
3
-style="{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0, AlignConsecutiveAssignments: true, AlignConsecutiveAssignments: true}"
-i
$FilePath$

注意:如果你是Windows,clang-format要选择clang-format.exe

1234…14
派大星

派大星

137 日志
64 分类
230 标签
GitHub CSDN
© 2019 派大星