0%

WebClient

Spring 有两个web客户端的实现,一个是RestTemplate另一个是spring5的响应代替WebClient。

WebClient是一个以Reactive方式处理HTTP请求的非阻塞客户端。

  • RestTemplate是阻塞客户端

    • 它基于thread-pre-requset模型。
    • 这意味着线程将阻塞,直到 Web 客户端收到响应。阻塞代码的问题是由于每个线程消耗了一些内存和 CPU 周期。当出现慢速请求的时候,等待结果的线程会堆积起来,将导致创建更多的线程、消耗更多的资源。频繁切换CPU资源也会降低性能。
  • WebClient是异步、非阻塞的方案。

    • WebClient将为每个事件创建类似于“任务”的东西。在幕后,Reactive 框架会将这些“任务”排队并仅在适当的响应可用时执行它们。

    • WebClient是Spring WebFlux库的一部分。因此,我们还可以使用具有反应类型(Mono和Flux的功能性、流畅的 API 作为声明性组合来编写客户端代码。

    • 底层支持的库

      • Reactor Netty - ReactorClientHttpConnector
      • Jetty ReactiveStream HttpClient - JettyHttpConnector
  • 关于IDEA开启 Reactive Streams DEBUG

演示代码

阅读全文 »

之前都是用jenkins+gitee 做的CI方案,发现朋友在使用Drone构建。运行资源占用比较小。适合个人做CI。

Drone

Drone是一款基于容器技术的持续集成工具,使用简单的YAML配置文件即可完成复杂的自动化构建、测试、部署任务,在Github上已经有22K+Star。

  • 相比于jenkins使用起来简介很多,大多时候没那么多需求。这个也足够满足基础需求。
  • 使用gitee + drone 演示
  • 官方文档
阅读全文 »

数据审计

部分业务需要记录用户对操作行为,如果给每张表都做一个记录表,感觉冗余数据太多了。所以采用审计表存储相关日志信息。

实现方案

基于Spring Aop 织入方式对关键方法进行拦截,也可以通过方法进行拦截。

  • 基于注解,在方法上织入。
  • 基于普通方法,在业务逻辑中进行调用。
  • 批量写入、延时写入数据库可能会出现短时间部分数据的丢失,但是频繁写入会导致Mysql资源占用过多。
    • 使用java.util.concurrent.ConcurrentLinkedQueue 作为消费列队。每次最多消费消费10条日志记录批量写入数据库。
  • 在审计信息中添加业务执行链路标识、执行用户标识。方便排查问题。
    • 基于MDC记录链路标识。

演示效果

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
/**
* 无参数
* @return
*/
@MonitorAnnotation(audit = true, type = "查看", title = "首页")
@RequestMapping(value = "")
public Object index() {
...
}

/**
* url占位符
* @param key
* @return
*/
@RequestMapping(value = "/add{key}")
public Object add(@PathVariable(required = false) String key) {
if (Objects.isNull(key)) {
key = Strings.EMPTY;
}
auditService.test(key);
...
}

/**
* url参数
* @param apiParam
* @return
*/
@MonitorAnnotation(audit = true, type = "查看", title = "api", descriptionExpression = "解析参数 #{[0].id}")
@RequestMapping(value = "/path")
public Object path(ApiParam apiParam) {
...
}

/**
* json参数
* @param apiParam
* @return
*/
@MonitorAnnotation(audit = true, type = "查看", title = "api", descriptionExpression = "解析json #{[0].id}")
@RequestMapping(value = "/api")
public Object api(@RequestBody ApiParam apiParam) {
...
}
  • Console
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
# 无参数
code-example git:(main) ✗ curl http://127.0.0.1:9041
{"code":"200"}%

[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 58] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] HTTP URL Method : http://127.0.0.1:9041/#GET
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 68] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] Class Method : cn.z201.audit.AppApplicationController#index
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 79] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] Args : []
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 64] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] Time-Consuming : 10 ms
[XNIO-1 task-1] [AuditRepository.java : 50] [fffffffffe71f0b8ffffffffaed06e501644984299461010] [] {"eventType":"查看","eventTitle":"首页","eventDescription":"","eventTime":1644984299487,"opTraceId":"fffffffffe71f0b8ffffffffaed06e501644984299461010","userId":1}
# url参数 方法调用
code-example git:(main) ✗ curl http://127.0.0.1:9041/add1
{"code":"200","data":"1"}%

[XNIO-1 task-1] [AuditRepository.java : 50] [000000001aeb91faffffffff9f9ad4b01644984411302010] [] {"eventType":"查看","eventTitle":"测试方法写入","eventDescription":"1","opTraceId":"000000001aeb91faffffffff9f9ad4b01644984411302010","userId":1}
#url参数
code-example git:(main) ✗ curl http://127.0.0.1:9041/path\?id\=1
{"code":"200","data":{"id":1}}%
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 58] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] HTTP URL Method : http://127.0.0.1:9041/path#GET
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 68] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] Class Method : cn.z201.audit.AppApplicationController#path
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 79] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] Args : [{"id":1}]
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 64] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] Time-Consuming : 3 ms
[XNIO-1 task-1] [AuditRepository.java : 50] [ffffffffbc9cb15500000000603b0e6a1644984489674100] [] {"eventType":"查看","eventTitle":"api","eventDescription":"解析参数 1","opTraceId":"ffffffffbc9cb15500000000603b0e6a1644984489674100","userId":1}

# json 参数
code-example git:(main) ✗ curl -X POST http://localhost:9041/api -H 'Content-Type: application/json' -d '{"id":"1"}'
{"code":"200","data":{"id":1}}%
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 58] [000000001fbbccdf000000006a218db41644985048604100] [] HTTP URL Method : http://localhost:9041/api#POST
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 68] [000000001fbbccdf000000006a218db41644985048604100] [] Class Method : cn.z201.audit.AppApplicationController#api
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 79] [000000001fbbccdf000000006a218db41644985048604100] [] Args : [{"id":1}]
[XNIO-1 task-1] [MonitorAnnotationAspectPlugImpl.java : 64] [000000001fbbccdf000000006a218db41644985048604100] [] Time-Consuming : 1 ms
[XNIO-1 task-1] [AuditRepository.java : 50] [000000001fbbccdf000000006a218db41644985048604100] [] {"eventType":"查看","eventTitle":"api","eventDescription":"解析json 1","opTraceId":"000000001fbbccdf000000006a218db41644985048604100","userId":1}

演示代码

阅读全文 »

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

代码演示

RedLock

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,就认为客户端成功地获得分布式锁了,否则加锁失败。即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

  • 操作流程
  1. 客户端获取当前时间

  2. 客户端按照N个Redis实例执行加锁操作

    • 加锁操作和在单实例上执行的加锁操作一样,使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。还有一个超时时间。如果在某一个Redis实例上加锁失败,会直接在下一个实例上执行。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。

  3. 客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

    • 当客户端加锁操作满足,以下条件才算成功。
      1. 客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;
      2. 客户端获取锁的总耗时没有超过锁的有效时间。
  4. 客户端释放锁

    • 释放掉锁执行lua脚本即可。

Redisson实现RedLock

  • Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

Redis主从版本

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
/**
* @author z201.coding@gmail.com
**/
@Configuration
public class RedisConfig {

@Bean
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redisson) {
return new RedissonConnectionFactory(redisson);
}

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// key序列化方式
redisTemplate.setKeySerializer(redisSerializer);
// value序列化
redisTemplate.setValueSerializer(redisSerializer);
// value hashmap序列化
redisTemplate.setHashValueSerializer(redisSerializer);
return redisTemplate;
}

@Bean
public RedissonClient redissonClient() throws IOException {
Config config = new Config();
// 主从
config.useMasterSlaveServers()
//可以用"rediss://"来启用SSL连接
.setMasterAddress("redis://127.0.0.1:6379").setPassword("redis_pwd")
.addSlaveAddress("redis://127.0.0.1:6380","redis://127.0.0.1:6381").setPassword("redis_pwd")
.setRetryInterval(5000)
.setTimeout(10000)
.setConnectTimeout(10000);//(连接超时,单位:毫秒 默认值:3000);
return Redisson.create(config);
}
}

  • 单元测试
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
@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = AppApplication.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AppApplicationTest {

@Autowired
private RedissonClient redissonClient;

private static final String LOCK_TITLE = "redisLock_";

@Test
@Disabled
void contextLoads() throws InterruptedException {
int count = 10;
CountDownLatch countDownLatch = new CountDownLatch(count);
ExecutorService executorService = Executors.newFixedThreadPool(count);
String key = UUID.randomUUID().toString();
String lockKey = LOCK_TITLE + key;
for (int i = 0; i < count; i++) {
executorService.execute(() -> {
try {
RLock lock = redissonClient.getLock(lockKey);
//加锁,并且设置锁过期时间,防止死锁的产生
Boolean result = lock.tryLock(2, TimeUnit.MINUTES);
log.info(" {} lock {}", result, lockKey);
Thread.sleep(1000L);
//执行具体业务逻辑
redissonClient.getBucket("value").set(String.valueOf(System.currentTimeMillis()));
String value = (String) redissonClient.getBucket("value").get();
log.info(" {} value {}", Thread.currentThread().getName(), value);
} catch (Exception e) {
log.error("{}", e.getMessage());
} finally {
//获取所对象
RLock lock = redissonClient.getLock(lockKey);
//释放锁(解锁)
lock.unlock();
log.info(" {} unlock {}", Thread.currentThread().getName(), lockKey);
}
countDownLatch.countDown();
});
}
countDownLatch.countDown();
executorService.shutdown();
;
log.info("run end~~~");
Thread.sleep(11000L);
}
}

END

Mybatis使用Snowflake生成主键

  • 在某些业务中为了安全已经扩展性需要弃用mysql自增id。采用Snowflake生成方式。
  • 全局唯一性,不能出现重复的id。
  • 趋势递增,MysqlInnoDB引擎使用的是是聚集索引,使用B-tree的数据结构来存储索引数据。尽量使用有序的主键保证写入性能。
  • 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、排序等特殊需求。
  • id是无序的,连续的id容易被社会工程。

开发思路

  • 创建BaseEntity,将基础字段存放此处。实体基类。
  • Snowflake工具类,生成SnowflakeId。
  • MybatisInterceptor,用于拦截sql执行,根据Insert、update语句拦截。并修改BaseEntity参数。

效果

  • http://localhost:9002/mybatis/ 每次刷新这个地址都会新增一条记录

源码地址

Java实现代码

  • 目录
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
├── Dockerfile
├── docker-compose.yml # docker-compose 文件 测试环境使用
├── docker-config
   ├── mysql
      ├── init
         └── 1_init.sql # 数据库初始化文件
      └── my.cnf
   └── pwd.txt
├── pom.xml
└── src
├── main
   ├── java
      └── cn
      └── z201
      └── mybatis
      ├── AccountToolService.java
      ├── AppApplication.java
      ├── AppApplicationController.java
      ├── dao
         └── AccountDao.java
      ├── entity
         └── Account.java
      └── mybatis
      ├── BaseEntity.java # 实体基类
      ├── MybatisConfig.java # mybatis配置
      ├── MybatisInterceptor.java # mybatis拦截器
      └── SnowflakeTool.java # 雪花算法生成器
   └── resources
   ├── application-dev.yml
   ├── application-test.yml
   ├── application.yml
   ├── logback.xml
   └── mapper
└── test
└── java
└── cn
└── z201
└── mybatis
├── AppApplicationTest.java
└── CodeGeneratorTest.java

pom.xml依赖

  • Spring boot 2.4.*
  • Mybatis plus 3.*
阅读全文 »

Netty是一款用于快速开发高性能的网络应用程序的Java框架,正是因为有 Netty 的存在,网络编程领域 Java 才得以与 C++ 并肩而立。

  • Netty 官网给出了有关 Netty 的整体功能模块结构

  • Core核心层

提供底层网络通信抽象和实现,其中包括了可扩展的时间模型、通信API、支持零拷贝的buf等。

  • Protocol Support 协议支持层

协议支持层基本上覆盖了主流协议的编解码实现,比如HTTP、SSL、WebSocket

  • Transport Service 传输服务层

传输服务层提供了网络传输能力的定义和实现方法,支持 Socket、HTTP 隧道等。Netty 对 TCP、UDP 等数据传输做了抽象和封装

Netty流程

从功能上理解顺序

  • 启动服务 -> 构建连接 -> 接受数据 -> 业务处理 -> 发送数据 -> 断开连接 -> 关闭服务

案例

  • 初始化线程池
  • 初始化channel
  • 绑定端口并启动
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

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg);
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}


/**
* Echoes back any received data from a client.
*/
public final class EchoServer {

static final boolean SSL = System.getProperty("ssl") != null;
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
} else {
sslCtx = null;
}

// Configure the server.
// 在官方的example中默认是Reactor主从多线程模式
// 1.配置线程池
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // 2.初始化channel类型
.option(ChannelOption.SO_BACKLOG, 100) // 2.1.设置channel参数
.handler(new LoggingHandler(LogLevel.INFO))
// 注册channelhandler,在netty中通过ChannelPipeline 去注册多个 ChannelHandler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});

// Start the server.
// 3.启动 通过bind() 方法会真正触发启动,sync() 方法则会阻塞
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

阅读全文 »

Scan

游标迭代器

大于 2.8.0 版本可用。

时间复杂度:每次调用 O(1)。O(N) 用于完整的迭代,包括足够的命令调用以使光标返回 0。N 是集合内的元素数。

  • 在 Redis 2.8 之前,我们只能使用 keys 命令来查询我们想要的数据,但这个命令存在两个缺点:

  • 此命令没有分页功能,我们只能一次性查询出所有符合条件的 key 值,如果查询结果非常巨大,那么得到的输出信息也会非常多。

  • keys 命令是遍历查询,因此它的查询时间复杂度是 o(n),所以数据量越大查询时间就越长。

  • Scan:用于检索当前数据库中所有数据。

  • HScan:用于检索哈希类型的数据。

  • SScan:用于检索集合类型中的数据。

  • ZScan:由于检索有序集合中的数据。

特性

  • 它可以完整返回开始到结束检索集合中出现的所有元素,也就是在整个查询过程中如果这些元素没有被删除,且符合检索条件,则一定会被查询出来;
  • Scan 可以实现 keys 的匹配功能;
  • Scan 是通过游标进行查询的不会导致 Redis 假死;
  • Scan 提供了 count 参数,可以规定遍历的数量,但是返回并不是按照规定来的;
  • Scan 会把游标返回给客户端,用户客户端继续遍历查询;
  • Scan 返回的结果可能会有重复数据,需要客户端去重;
  • 单次返回空值且游标不为 0,说明遍历还没结束;
  • Scan 可以保证在开始检索之前,被删除的元素一定不会被查询出来;
  • 在迭代过程中如果有元素被修改, Scan 不保证能查询出相关的元素。

演示代码

阅读全文 »

整理下Jenkins本地部署程序的过程。

  • 在开发阶段,许多编译工具会将我们的源码编译可使用的文件。例如 vue-cli 的项目会被 webpack 打包编译为浏览器的文件,Java 项目会被编译为 .class/jar 文件以供服务器使用。

    1. 开发人员将源代码,经过编译、压缩等一系列流程打包

    2. 上传到服务器。

    3. 在服务器将编译后的文件,手动可用的容器服务内(例如 Nginx,Tomcat,Apache 等服务)

单机部署

在jenkins所在的机器上部署程序

  1. 部署jenkins并配置环境
  2. 设置jenkins-credentials (后续用于从git仓库中获取代码)
  3. 创建任务
    1. 设置仓库地址
    2. 设置程序部署shell脚本
  • jenkins shell
1
2
3
4
mvn clean install -Dmaven.test.skip=true
cp x x x/target/xxx-1.0.0-SNAPSHOT.jar /opt/xxx/upgrade
cd /opt/xxx
BUILD_ID=dontKillMe sh xxx.sh
  • 编写程序运行shell脚本。
  • 部署java
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
#!/bin/bash
app=xxx
appName=${app}-1.0.0-SNAPSHOT
projPath=/opt/xxx
upgradePath=${projPath}/upgrade
backPath=${projPath}/backup
logFile=${projPath}/data/logs/${app}.log

# 检查升级文件是否存在
echo "checking upgrade file..."
if [ ! -f ${upgradePath}/${appName}.jar ]; then
echo "cann't found file ${upgradePath}/${appName}.jar!"
exit
fi

# 结束进程
pid=`ps aux | grep ${projPath}/${appName}.jar | grep -v grep | grep -v kill | awk '{print $2}'`
if [ ${pid} ]; then
echo "kill ${appName}!"
kill -9 $pid
fi

# 备份原升级文件,如果存在的话
if [ -f ${projPath}/${appName}.jar ]; then
timeStr=`date +%Y%m%d%H%M%S`
mv ${projPath}/${appName}.jar ${backPath}/${appName}.jar.bak_${timeStr}
echo "load ${appName} success!"
echo "backup ${appName} success!"
fi

# 拷贝升级文件
echo "copy ${appName}.jar..."
cp ${upgradePath}/${appName}.jar ${projPath}/${appName}.jar

# 启动进程
echo "start ${appName}..."
nohup java -Djava.security.egd=file:/dev/./urandom -Xms512m -Xmx512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -jar ${projPath}/${appName}.jar --spring.profiles.active=test >/dev/null 2>&1 &

# 查看日志
# 判断当天日志文件是否存在
if [ ! -f ${logFile} ]; then
# 文件不存在则创建文件,再执行tail命令
touch ${logFile}
fi
echo "tail -f ${logFile}"

  • 部署vue
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

# git revert
# git restore .

applicationPath=/opt/view/xxxx

echo 'delete old files...'
rm -rf ${applicationPath}/*

echo 'copy files after npm build!'
cp -rf dist/* ${applicationPath}

准备学习下neo4j记录在mac下安装neo4j的坑。

neo4j官网网站下载失败

国内只能从镜像地址下载

http://doc.we-yun.com:1008/

配置jdk11

可以查看我上面内容实用Jenv管理多个版本。

启动neo4j

下载完成后解压

1
2
3
4
# 启动
./neo4j start
# 关闭
./neo4j stop

neo4j界面

  • 在浏览器中输入localhost:7474
  • 初始用户名、密码均为neo4j, 第一次登录需要修改密码

安装jupyter

注意提前安装python环境

1
2
3
pip install jupyter notebook -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host=mirrors.aliyun.com
# 检查版本
jupyter --version

OpenJdk 11 or 17 环境下JavaFx开发环境设置

Openfx

  • 记录了jdk11和17版本下javafx的使用方式。

  • 下载openfx

方式一

pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>11</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>11</version>
</dependency>
</dependencies>
idea设置

下载javafx版本,然后引入依赖。项目lib中引入。启动的时候在vm中添加启动参数

1
2
3
4
5
6
7
8
9
10
11
12
--module-path
/Users/$USER/word/libhome/javafx-sdk-11.0.2/lib
--add-modules
javafx.controls
--add-modules
javafx.base
--add-modules
javafx.graphics
--add-modules
javafx.fxml
--add-modules
javafx.media

方式二

参考文献

简单记录下mac下多版本jdk管理

检查环境

查看当前安装的jdk版本

1
2
/usr/libexec/java_home -V
# 如果有安装过jdk版本这里会输出信息

使用brew 安装jenv

1
2
3
4
brew install jenv
#检查
jenv doctor
# 刚安装会提示很多信息

初始化jenv

1
2
3
4
5
brew install jenv
jenv init -
echo 'eval "$(jenv init -)"' >> ~/.bash_profile
echo 'eval "$(jenv init -)"' >> ~/.zprofile
jenv add <path-to-java8-Home-Dir> # 这里从/usr/libexec/java_home -V 获取javaHome

切换jdk

1
2
3
4
5
6
7
➜  ~ jenv versions
* system (set by /Users/zengqingfeng/.jenv/version)
1.8
1.8.0.275
openjdk64-1.8.0.275

➜ ~ jenv local system # 切换版本

可能出现的问题

  • 使用 jenv 过程可能会发现,当切换 JDK 版本之后, ${JAVA_HOME} 环境变量没有改变,还是上一个 JDK 版本配置。
1
这时可以运行 exec $SHELL -l , ${JAVA_HOME} 将会变成当前版本。

之前有介绍过SpringBoot-Delayed-Queue基于jdk实现DelayedQueue。在实际开发中使用redis zset来实现的也比较常见。

  • Redis 延迟队列实现的思路,利用 zrangebyscore 查询符合条件的所有待处理任务,循环执行队列任务。或者每次查询最早的一条消息,判断这条信息的执行时间是否小于等于此刻的时间,如果是则执行此任务,否则继续循环检测。

Redis-Sorted-Sets

每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。

  • zrangebyscore 返回排序集合中的所有元素,key其分数在min and之间max(包括分数等于minor的元素max)。元素被认为是从低到高排序的。
  • zrem 从排序集中删除的成员数,不包括不存在的成员。
  • zadd 将具有指定分数的所有指定成员添加到存储在的排序集中key。可以指定多个分数/成员对。如果指定的成员已经是排序集的成员,则更新分数并将元素重新插入到正确的位置以确保正确的排序。
阅读全文 »

日常在开发过程中方便排查文件,需要在日志中输出一些关键内容。

MDC

在茫茫日志中定位需要的信息很费时,MDC 全称是 Mapped Diagnostic Context.一个线程安全的存放诊断日志的容器。

演示代码

HTTP处理演示

代码

实际上代码不需要这么麻烦,只需要调用MDC.put 就能将数据写入,但是实际开发过程中。通过手动编码的方式添加太低效了,这里在spring mvc的场景下使用用拦截器来处理数据注入操作。

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
/**
* @author z201.coding@gmail.com
**/
@RestController
@Slf4j
public class AppApplicationController {

@RequestMapping(value = "")
public Object index() {
log.info("index");
Map<String, Object> data = new HashMap<>();
data.put("code", "200");
return data;
}

}


**
* @author z201.coding@gamil.com
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 8)
@ConditionalOnClass(WebMvcConfigurer.class)
@Slf4j
@Component
public class MdcTraceContextFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String appTraceId = request.getHeader(MdcApiConstant.HTTP_HEADER_TRACE_ID);
/**
* 没有设置就设置下,设置了就直接返回。注意这里必须提前在拦截器中设置好,不然会失效。
*/
if (StrUtil.isEmpty(appTraceId)) {
appTraceId = MDC.get(MdcApiConstant.HTTP_HEADER_TRACE_ID);
if (Strings.isEmpty(appTraceId)) {
appTraceId = MdcTool.getInstance().currentTraceId();
}
request.setAttribute(MdcApiConstant.HTTP_HEADER_TRACE_ID, appTraceId);
}
MDC.put(MdcApiConstant.HTTP_HEADER_TRACE_ID, appTraceId);
filterChain.doFilter(request, response);
}
}


/**
* @author z201.coding@gmail.com
**/
public class MdcTool {

private static class SingletonHolder {
private static final MdcTool INSTANCE = new MdcTool();
}
private MdcTool (){}

public static final MdcTool getInstance() {
return SingletonHolder.INSTANCE;
}

public synchronized String currentTraceId() {
ThreadLocalRandom random = ThreadLocalRandom.current();
UUID uuid = new UUID(random.nextInt(), random.nextInt());
StringBuilder st = new StringBuilder(uuid.toString().replace("-", "").toLowerCase());
st.append(Instant.now().toEpochMilli());
int i = 0;
while (i < 3) {
i++;
st.append(ThreadLocalRandom.current().nextInt(2));
}
return st.toString();
}
}

log配置文件

Log4j 或者 logback 在pattern表中修改表达式%X{AppTraceId} 这就是拦截去注入进去的数据key。

1
<pattern>[%t][%level][%file:%line][%X{AppTraceId}] %msg%n</pattern>
  • Console
1
2
culr http://localhost:9007/mdc/
[AppApplicationController.java:19][00000000299baa76ffffffffbd27659b1641811416723011] index
阅读全文 »

Scheduling-Tasks

Spring Scheduler里有两个概念:任务(Task)和运行任务的框架(TaskExecutor/TaskScheduler)。TaskExecutor顾名思义,是任务的执行器,允许我们异步执行多个任务。TaskScheduler是任务调度器,来运行未来的定时任务。触发器Trigger可以决定定时任务是否该运行了,最常用的触发器是CronTrigger,具体用法会在下面章节中详细介绍。Spring内置了多种类型的TaskExecutor和TaskScheduler,方便用户根据不同业务场景选择。

  • 相同的方案还有Quartz,但是需要持久化数据库。
  • Java.util中的Timer,功能相对少了点。

@Scheduled

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
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

/**
* cron 表达式
*/
String cron() default "";

/**
* 将解析 cron 表达式的时区。默认情况下,此属性为空字符串(即将使用服务器的本地时区
*/
String zone() default "";

/**
* Execute the annotated method with a fixed period in milliseconds between the
*/
long fixedDelay() default -1;

/**
* 和fixedDelay一个意思,只是类型是字符串。
*/
String fixedDelayString() default "";

/**
* 表示按照一定频率时间间隔执行,类型long单位ms。
*/
long fixedRate() default -1;

/**
* 和fixedRate一个意思,只是类型是字符串。
*/
String fixedRateString() default "";

/**
* 表示延时多久再执行第一次任务,类型long单位ms。
*/
long initialDelay() default -1;

/**
* 和initialDelay一个意思,只是类型是字符串。
*/
String initialDelayString() default "";

}

Cron表示式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 ┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *

每一个域可出现的字符如下:
Seconds:可出现", - * /"四个字符,有效范围为0-59的整数
Minutes:可出现", - * /"四个字符,有效范围为0-59的整数
Hours:可出现", - * /"四个字符,有效范围为0-23的整数
DayofMonth:可出现", - * / ? L W C"八个字符,有效范围为0-31的整数
Month:可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEc
DayofWeek:可出现", - * / ? L C #"四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推
Year:可出现", - * /"四个字符,有效范围为1970-2099年

举几个例子:
0 0 2 1 * ? * 表示在每月的1日的凌晨2点调度任务
0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
0 15 10 ? 6L 2020-2021 表示2020-2021年的每个月的最后一个星期五上午10:15执行作

演示代码

阅读全文 »

GoAccess

GoAccess是一个基于终端的快速日志分析器。其核心思想是实时快速分析和查看Web服务器统计信息,而无需使用您的浏览器(如果您希望通过SSH快速分析访问日志,或者只是喜欢在终端中工作),终端输出是默认输出,但它能够生成完整的,独立的实时 HTML报告(非常适合分析,监控和数据可视化),以及a JSON和CSV报告。

  • 数据持久性强,GoAccess能够通过磁盘上的B + Tree数据库逐步处理日志。
  • GoAccess是用C语言编写的,要运行它,你只需要将ncurses作为依赖项,它甚至还具有自己的RFC6455兼容Web Socket服务器。
  • 跟踪提供请求所需的时间。如果您想跟踪减慢网站速度的网页,则非常有用。
  • GoAccess允许任何自定义日志格式字符串。预定义选项包括Apache,Nginx,Amazon S3,Elastic Load Balancing,CloudFront等。
  • 所有面板和指标都定时在终端输出上每200毫秒更新一次,在HTML输出上每秒更新一次。

阅读全文 »

日常开发中,需要对sql进行。为了提高效率,曾经使用mybatis扩展输出查询计划。

慢SQL

从编码角度来优化数据层的话,我首先会去查一下项目中运行的sql语句,定位到瓶颈是否出现在这里,首先去优化sql语句,而慢sql就是其中的主要优化对象,对于慢sql,顾名思义就是花费较多执行时间的语句,它带来的影响也比较恶劣,首先是执行时间过长影响数据的返回速度,其次,慢sql的长时间执行也会消耗和占用mysql的系统资源,影响其他的sql语句执行,过多的慢sql极其影响性能,如果系统流量或者并发量较大的情况下,过多的执行慢sql很有可能造成mysql的死锁以致于mysql服务无法正常使用。

Explain

explain关键字一般放在SELECT查询语句的前面,用于描述MySQL如何执行查询操作、以及MySQL成功返回结果集需要执行的行数。explain 可以帮助我们分析 select 语句,让我们知道查询效率低下的原因,从而改进我们查询,让查询优化器能够更好的工作。

演示效果

1
2
3
4
➜  blog curl http://127.0.0.1:9023/mybatis/   # 新增测试数据
{"code":"200","data":{"id":3,"isEnable":true,"createTime":1643953933094,"updateTime":1643953933094,"phoneNumber":"13611707472","email":"chu@gmail.com","saltPassword":"Xe8mbdpS","salt":null,"usrName":"析丹楚"}}%
➜ blog curl http://127.0.0.1:9023/mybatis/list # 查询测试数据
{"code":"200","data":[{"id":1,"isEnable":true,"createTime":1643953924990,"updateTime":1643953924990,"phoneNumber":"13876877231","email":"wu@gmail.com","saltPassword":"rd7NHZlt","salt":"","usrName":"国富吴"},{"id":2,"isEnable":true,"createTime":1643953925710,"updateTime":1643953925710,"phoneNumber":"13971918631","email":"guohui.yang@qq.com","saltPassword":"ritHRn5L","salt":"","usrName":"国辉杨"},{"id":3,"isEnable":true,"createTime":1643953933094,"updateTime":1643953933094,"phoneNumber":"13611707472","email":"chu@gmail.com","saltPassword":"Xe8mbdpS","salt":"","usrName":"析丹楚"}]}%
  • Console
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
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] ==>  Preparing: SELECT id,is_enable,create_time,update_time,phone_number,email,salt_password,salt,usr_name FROM account
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] ==> Parameters:
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] <== Total: 2
[XNIO-1 task-1] [MybatisInterceptor.java : 80] [{"id":"1","selectType":"SIMPLE","table":"account","type":"ALL","rows":"2","filtered":"100.0"}]
[XNIO-1 task-1] [MybatisInterceptor.java : 82] SQL RunTime 15 ms
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] ==> Preparing: INSERT INTO account ( is_enable, create_time, update_time, phone_number, email, salt_password, usr_name ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] ==> Parameters: true(Boolean), 1643953933094(Long), 1643953933094(Long), 13611707472(String), chu@gmail.com(String), Xe8mbdpS(String), 析丹楚(String)
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] <== Updates: 1
[XNIO-1 task-1] [MybatisInterceptor.java : 54] EXPLAIN
SELECT
id,
is_enable,
create_time,
update_time,
phone_number,
email,
salt_password,
salt,
usr_name
FROM
account
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] ==> Preparing: SELECT id,is_enable,create_time,update_time,phone_number,email,salt_password,salt,usr_name FROM account
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] ==> Parameters:
[XNIO-1 task-1] [BaseJdbcLogger.java : 137] <== Total: 3
[XNIO-1 task-1] [MybatisInterceptor.java : 80] [{"id":"1","selectType":"SIMPLE","table":"account","type":"ALL","rows":"3","filtered":"100.0"}]
[XNIO-1 task-1] [MybatisInterceptor.java : 82] SQL RunTime 8 ms
阅读全文 »

Lucene

在Java领域常用的搜索框架鼻祖Lucene,现在常用的SolrElasticSearch底层都是基于Lucene

  • 倒排索引算法

  • 索引和搜索,索引是现代搜索引擎的核心,建立索引的过程就是把源数据处理成非常方便查询的索引文件的过程。

luke

  • 可以分词文件内容,这里需要注意版本。

ik-analyzer

分词器,Lucene对中文支持不友好,这里使用开源的分词器。

  • 由于在maven仓库中找不到,所以需要手动编译。这里需要注意版本分支,比如我使用的是8.4所以要切换到8.4的版本。和lucene库版本统一。
  • mvn clean install -Dmaven.test.skip=true 构建版本,如果要部署可以将jar部署到自己的私有仓库,或者直接已lib方式引入项目。

源码了解

  • Document 文档Lucene 内部的数据结构,索引文档时,会按照一定规则去创建索引,生成倒排索引文件。

  • Field 直接为文档创建字段,它是搜索和索引的单位,也是字段的集合。

  • Term 这是搜索的单位。它由两个元素组成 key、value。

  • StandardAnalyzer 分词的目的是为了索引,索引的目的是为了搜索,就像查字典一样。ik-analyzer就是中文分词器。

阅读全文 »

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。是为了解决分布式系统中,不同的系统或是同一个系统的不同主机共享同一个资源的问题,它通常会采用互斥来保证程序的一致性。

锁是一种常用的并发控制机制,用于保证一项资源在任何时候只能被一个线程使用,如果其他线程也要使用同样的资源,必须排队等待上一个线程使用完。

分布式锁

上面说的锁指的是程序级别的锁,例如 Java 语言中的 synchronized 和 ReentrantLock 在单应用中使用不会有任何问题,但如果放到分布式环境下就要使用分布式锁。

实现分布式锁方案

  1. 基于 MySQL 的悲观锁来实现分布式锁,性能不太好。容易写bug造成Mysql死锁问题。
    • 基于数据库实现分布式锁比较简单,绝招在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。
    • 单点故障问题。一旦数据库不可用,会导致整个系统崩溃
    • 死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。倘若已获得共享资源访问权限的进程突然挂掉、或者解锁操作失败,使得锁记录一直存在数据库中,无法被删除,而其他进程也无法获得锁,从而产生死锁现象。
  2. 基于 Redis 实现分布式锁,目前广泛使用的方案。
    • 当多个进程频繁去访问 Redis 时,Redis 可能成为瓶颈。关键Redis并不是和做分布式锁(比较极端的场景下)
    • 反复尝试会增加通信成本和性能开销,需要指定重试的次数。如果每次都是众多进程进行竞争的话,有可能会导致有些进程永远获取不到锁。
    • 可以集群部署,可以避免单点故障。
  3. 基于 ZooKeeper 实现分布式锁,利用 ZooKeeper 顺序临时节点来实现。
    • ZooKeeper 基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。
    • 持久节点(PERSISTENT)。这是默认的节点类型,一直存在于 ZooKeeper 中。
    • 持久顺序节点(PERSISTENT_SEQUENTIAL)。在创建节点时,ZooKeeper 根据节点创建的时间顺序对节点进行编号命名。
    • 临时节点(EPHEMERAL)。当客户端与 Zookeeper 连接时临时创建的节点。与持久节点不同,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除。
    • 临时顺序节点(EPHEMERAL_SEQUENTIAL)。就是按时间顺序编号的临时节点。
    • zookeeper在分布式环境下能保证互斥,具备锁失效机制。防止死锁即便出现持有锁崩溃或者锁失败的情况也能被动解锁。保证后续的线程可以获得锁。并且可以多次访问临界资源。有高可用获得锁和释放锁的功能,性能并不是很差。
    • 羊群效应,就是在整个 ZooKeeper 分布式锁的竞争过程中,大量的进程都想要获得锁去使用共享资源。每个进程都有自己的“Watcher”来通知节点消息,都会获取整个子节点列表,使得信息冗余,资源浪费。当共享资源被解锁后,Zookeeper 会通知所有监听的进程,这些进程都会尝试争取锁,但最终只有一个进程获得锁,使得其他进程产生了大量的不必要的请求,造成了巨大的通信开销,很有可能导致网络阻塞、系统性能下降。
      • 在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。
      • 每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。
      • 若本进程对应的临时节点编号不是最小的,则注册 Watcher,监听自己的上一个临时顺序节点,当监听到该节点释放锁后,获取锁。
  4. 基于etcd分布式锁实现
  • 分别对这三种实现方式进行性能压测,可以发现在同样的服务器配置下,Redis 的性能是最好的,Zookeeper 次之,数据库最差。从实现方式和可靠性来说,Zookeeper 的实现方式简单,且基于分布式集群,可以避免单点问题,具有比较高的可靠性。因此,在对业务性能要求不是特别高的场景中,建议使用 Zookeeper 实现的分布式锁。

部署方案

  • 单机
    • 简单,但是不能保证高可用,一旦出现单点故障就gg了。
  • 集群
    • 在Redis场景下有大名鼎鼎的红锁(RedLock)
      • 红锁的核心逻辑是,部署集群的情况下,比如5个master。加锁的时候挨个加锁。当满足(5/2+1)=3 的时候就表示加锁成功,也就是半数成功则算成功。释放锁也是类似的操作。但是也带来了通信成本。仍需要二次检查锁的完整性。
      • 单点故障时,我们第一时间想到的就是搞几个 Slave 从节点做备份,Redis 里很好地支持了哨兵(Sentinel)模式,自动主从切换。锁写到Master后,还没同步到Slave呢,Master挂了Slave选举成了Master,但是Slave里没有锁,其他线程再次能上锁了。不安全。
      • 如果非要用Redis方案来做锁,在保证高可用的情况下可以通过二次检查的逻辑防止锁故障。
    • Redis集群只是做了 slot 分片,锁还是只写到一个 Master 上,所以它和哨兵(Sentinel)模式会面临同样的问题。
    • Zookeeper提供了协调分布式应用的基本服务,它向外部应用暴露一组通用服务——分布式同步(Distributed Synchronization)
      • Zookeeper本身就是一个分布式程序(只要有半数以上节点存活,zk就能正常服务)
      • 如果是钱的业务,建议使用zookeeper。
      • ZooKeeper实现分布式锁的核心原理是临时节点,更确切的说法是临时顺序节点。
      • ZooKeeper的节点是通过session心跳来续期的,比如客户端1创建了一个节点, 那么客户端1会和ZooKeeper服务器创建一个Session,通过这个Session的心跳来维持连接。如果ZooKeeper服务器长时间没收到这个Session的心跳,就认为这个Session过期了,也会把对应的节点删除。临时节点类型的最大特性是:当客户端宕机后,临时节点会随之消亡。

分布式锁的条件

  • 可以提供分布式部署应用集群中,同一个方法只能在某一个应用的线程执行。

  • 该锁需要包含一下特性:分布式互斥、重入锁、锁续期、阻塞锁、公平锁、良好的加锁释放锁性能。

分布式使用场景

  • 保证接口的幂等性,防止冲突提交数据。
  • 防止重复消费,比如推送消息或者发送邮件。保证合理的执行次数。
  • 防止分布式场景缓存击穿,比如秒杀活动的超卖情况。
阅读全文 »

爱迪生说:“天才不过是一个经常能完成自己工作的聪明人而已。”我们不必做天才,优秀的人不过是一个经常能完成自己工作的普通人而已。

减少学习排斥心理

  1. 在学习过程中,可以穿插安排喜欢且擅长的环节。而不是死磕一个艰难的任务。艰难和轻松的任务穿插完成,避免过度负面情绪,让自己在学习和工作中保持愉悦和高效的方法。
  2. 定制学习计划
    1. 定制学习OKR,不要指定完不成的任务。将任务具像化,人的大脑是善于遗忘的。让自己每天都能从学习上得到正面都反馈,拥有足够都收获感。
    2. 人的大脑存在奖励系统,它更加愿意选择立刻就能得到的好处。避免每天同类型的学习任务,任务类型尽可能少安排。
    3. 尝试让学习变得自发自觉,不要追求某种解决,而是专注与学习的本身。
    4. 我们的大脑中存在场所神经元环境的改变可以激发它的活性
  3. 学习过程中,遭遇困难是很正常的事情。它是成长过程中的一部分。

伪勤奋

  1. 学习是了解、记忆、应用、输出的过程,只有去完成感到需要思考、需要克服困难的任务,才能算真勤奋
  2. 学的东西,要立刻检验,检验自己有没有记住。要有输出和思考。
  3. 观察、分析、试错、反思、调整、应用、总结。可以让学习到的知识能被掌握并应用,要知道自己是如何做到的。
  4. 避免形式主义,很多事情动起来就能解决**80%**。
阅读全文 »

在对老系统进行saas改造的时候,在项目初期使用了动态数据源的方式处理业务。

  • 本文内容仅针对数据存储方案。

动态数据源

SaaS是Software-as-a-service(软件即服务)它是一种通过Internet提供软件的模式,厂商将应用软件统一部署在自己的服务器

①独立性:每个租户的系统相互独立。

②平台性:所有租户归平台统一管理。

③隔离性:每个租户的数据相互隔离。

需求

  • 老项目系统进行saas改造工作。

方案

独立数据库、共享数据库、共享架构、OLTP、OLAP

  • 根据客户需求可以选择隔离数据库。
  • 也可以选择公用数据库。
  • 采用OLTP方案进行数据同步。
  • 采用OLAP方案进行冷数据同步。

技术方案

  • spring boot 2.4.5
  • mybatis plus
AbstractRoutingDataSource

Spring 官网提供的切换数据源的抽象方法,基于查找键将getConnection()调用路由到各种目标 DataSource 之一的抽象DataSource实现。后者通常(但不一定)通过一些线程绑定的事务上下文来确定。

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

/**
* determineTargetDataSource() 方法进行切换。
**/
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}

/**
* 确定当前查找键。这通常会被实现来检查线程绑定的事务上下文。允许任意键。返回的键需要匹配存储的查找键类型,由resolveSpecifiedLookupKey方法解析。
**/
@Nullable
protected abstract Object determineCurrentLookupKey();
/**
* 检索当前目标数据源。确定current lookup key ,在targetDataSources映射中执行查找,必要时回退到指定 的default target DataSource
**/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}

演示代码

阅读全文 »

在Java的技术栈中可以使用maven插件来构建docker镜像,用起来没有docker-compose好用。

实现目标

  1. spring boot 、mysql 、redis 定义正常运行。
  2. 解决docker compose 启动顺序问题。
  3. 解决配置文件读取其它容器ip问题。

源码地址

演示效果

docker运行情况

spring boot运行情况

1
2
➜  docker-run curl http://127.0.0.1:9000/docker/
{"code":"200","data":"[information_schema, docker_app_1, mysql, performance_schema, sys]","cache":{"redis_version": "5.0.5"}}

构建项目

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
.
├── Dockerfile # Dockerfile
├── docker-compose.yml # docker-compose文件
├── docker-config # docekr配置文件
│   ├── mysql
│   │   ├── init
│   │   │   └── 1_init.sql # mysql数据库初始化文件
│   │   └── my.cnf # mysql 配置文件
│   ├── pwd.txt
│   └── redis
│   └── redis.conf # redis 配置文件
├── pom.xml # java maven 项目依赖
├── src
│   ├── main
│   │   ├── java
│   │   │   └── cn
│   │   │   └── z201
│   │   │   └── docker
│   │   │   ├── AppApplication.java # spring boot 启动类
│   │   │   └── AppApplicationController.java # demo接口类
│   │   └── resources
│   │   ├── application-dev.yml # dev环境下配置文件
│   │   ├── application-test.yml # test环境下配置文件
│   │   ├── application.yml # spring boot 配置文件
│   │   └── logback.xml # 日志输出文件
│   └── test
│   └── java
└── target
├── Docker-Compose-SpringBoot-Mysql-Redis-1.0.0-SNAPSHOT.jar # mvn install 构建多产物

spring boot 项目

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
package cn.z201.docker;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
* @author z201.coding@gmail.com
**/
@RestController
public class AppApplicationController {

@Autowired
private JdbcTemplate jdbcTemplate;

@Autowired
private RedisTemplate redisTemplate;

@RequestMapping(value = "")
public Object index() {
List<String> dataBasesList = jdbcTemplate.queryForList("SHOW DATABASES", String.class);
Properties info = redisTemplate.getConnectionFactory().getConnection().info();
Map<String, Object> data = new HashMap<>();
data.put("code", "200");
data.put("db", dataBasesList.toString());
data.put("cache", info);
return data;
}
}


package cn.z201.docker;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

/**
* @author z201.coding@gmail.com
*/
@SpringBootApplication
public class AppApplication {

public static void main(String[] args) {
ApplicationContext applicationContext = SpringApplication.run(AppApplication.class, args);
}
}

  • 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: spring-boot-mysql-redis
profiles:
active: dev
mvc:
throw-exception-if-no-handler-found: true # 处理404
web:
resources:
add-mappings: false # 关闭资源映射
server:
port: 9000
servlet:
context-path: /docker

logging:
config: classpath:logback.xml


  • test
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
spring:
datasource:
url: jdbc:mysql://mysql:3306/docker_app_1?useSSL=false&useUnicode=true&autoReconnect=true&failOverReadOnly=false&characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari: # https://github.com/brettwooldridge/HikariCP (uses milliseconds for all time values)
maximumPoolSize: 20 # 连接池最大连接数,默认是10
minimumIdle: 5 # 最小空闲连接数量
idleTimeout: 600000 # 空闲连接存活最大时间,默认600000(10分钟)
connectionTimeout: 30000 # 数据库连接超时时间,默认30秒,即30000
maxLifetime: 1800000 # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
redis:
host: redis
port: 6379
password: root
database: 0
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: 2000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
max-idle: 8 # 连接池中的最大空闲连接
shutdown-timeout: 100 # 关闭超时时间
timeout: 60s


  • 这里需要注意在mysql和redis连接上并没有写ip而是docker容器互联的功能,在同一个网桥中使用links互通多个容器的网络。

  • dev的环境都是本地环境,可以自己搭建mysql、redis环境。

docket-compose配置文件

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
version : '3'

networks:
network-docker-app:
driver: bridge

services:
web:
container_name: cn.z201.docker-app
build:
context: .
dockerfile: .
image: cn.z201.docker-app-3
networks:
- network-docker-app
expose:
- '9000'
ports:
- '9000:9000'
depends_on: # 等待其它服务启动完成
- mysql
- redis
links:
- mysql
- redis
mysql:
image: mysql:5.7
container_name: mysql5.7-dev-3
networks:
- network-docker-app
expose:
- '3306'
ports:
- '3306:3306'
volumes:
- ./docker-config/mysql/my.cnf:/etc/mysql/my.cnf # 映射数据库配置文件
- ./docker-config/mysql/init:/docker-entrypoint-initdb.d # 初始化数据库
command: [
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_unicode_ci',
'--lower_case_table_names=1',
'--default-time-zone=+8:00']
environment:
- MYSQL_ROOT_PASSWORD=root # 设置root密码
healthcheck:
test: "/bin/netstat -anpt|grep 3306"
interval: 30s
timeout: 3s
retries: 1
redis:
image: redis:5.0.5
container_name: redis5.0.6-dev-3
networks:
- network-docker-app
expose:
- '6379'
ports:
- '6379:6379'
volumes:
- ./docker-config/redis/redis.conf:/etc/redis.conf # 映射数据库配置文件
command: redis-server /etc/redis.conf # 启动redis命令
healthcheck:
test: "/bin/netstat -anpt|grep 6379"
interval: 30s
timeout: 3s
retries: 1

END

之前在订单场景使用过DelayedQueue延时列队,实际上还有阻塞列队的用法。刚工作那会写爬虫的时候有使用过。这里总结下使用。

BlockingQueue

  • 阻塞列队,BlockingQueue是java.util.concurrent包下的实现类。提供了线程安全的列队访问方式,当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。

  • 一般使用场景,生产者消费者。
  • 列队类型有两种,无限列队、有限列队。
    • 无限队列 (unbounded queue ) - 几乎可以无限增长。
      • BlockingQueue<String> blockingQueue = new LinkedBlockingDeque<>();
        • 默认构造函数将容量设置成 Integer.MAX_VALUE
    • 有限队列 ( bounded queue ) - 定义了最大容量。
      • BlockingQueue<String> blockingQueue = new LinkedBlockingDeque<>(10);
      • BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
        • ArrayBlockingQueue初始化的时候必须指定容器大小。
        • 同时可以指定是否公平锁。
  • LinkedBlockingDequeArrayBlockingQueue 都是 FIFO ,而PriorityBlockingQueue元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限,前面2种都是有界队列。
  • DelayQueue基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
阅读全文 »

上次编写了Docker-Compose-Redis-Master-Slave的文档,完成了简单的主从配置。这次在上次的基础上增加哨兵监控。

源码地址

编写docker-compose配置文件

  • 这里和上次的使用同一个网桥
  • Docker-compose 文件和 sentinel配置文件放在同一个目录下。
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
version : '3'

networks:
network-redis:
driver: bridge

services:
sentinel1:
image: redis:5.0.5
container_name: redis5.0.6-sentinel-1
networks:
- network-redis
ports:
- '26379:26379'
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- ./sentinel.conf:/usr/local/etc/redis/sentinel.conf
sentinel2:
image: redis:5.0.5
container_name: redis5.0.6-sentinel-2
networks:
- network-redis
ports:
- '26380:26379'
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- ./sentinel1.conf:/usr/local/etc/redis/sentinel.conf
sentinel3:
image: redis:5.0.5
container_name: redis5.0.6-sentinel-3
networks:
- network-redis
ports:
- '26381:26379'
command: redis-sentinel /usr/local/etc/redis/sentinel.conf
volumes:
- ./sentinel2.conf:/usr/local/etc/redis/sentinel.conf
  • 编写sentinel配置文件,
1
2
3
4
port 26379
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 172.21.0.2 6379 2
sentinel auth-pass mymaster redis_pwd
  • port 这里使用了三个哨兵,端口号需要单独设置 26379 26380 26381 并且复制出三份。按照要求修改端口号信息。

  • sentinel monitor mymaster 172.21.0.2 6379 1

    • master-name 表示给监视的主节点起一个名称;
    • ip 表示主节点的 IP;这个ip填写正确这里docker演示使用docker inspect [id] 查看master ip
    • port 表示主节点的端口;
    • quorum 表示确认主节点下线的 Sentinel 数量,如果 quorum 设置为 1 表示只要有一台 Sentinel 判断它下线了,就可以确认它真的下线了。
  • sentinel auth-pass mymaster redis_pwd

    • 所以如果 Redis 有密码,也需要设置。
1
2
cp sentinel.conf sentinel1.conf
cp sentinel.conf sentinel2.conf

启动检查

  • 启动docker-compose文件
1
docker-compose -f docker-sentinel-compose.yml up -d
  • Console

  • 进入sentinel容器检查
1
docker -it [id]
  • 进入sentinel
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
redis-cli -p 26379
127.0.0.1:26379> sentinel master mymaster
1) "name"
2) "mymaster"
3) "ip"
4) "172.21.0.2" # 主库的ip
5) "port"
6) "6379"
7) "runid"
8) "c0a0e5fa85c6fc61cf0670d374eac9c35f1b440c"
9) "flags"
10) "master"
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "508"
19) "last-ping-reply"
20) "508"
21) "down-after-milliseconds"
22) "30000"
23) "info-refresh"
24) "6070"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "106612"
29) "config-epoch"
30) "3"
31) "num-slaves" # 两个从库
32) "2"
33) "num-other-sentinels" # //还有两个哨兵
34) "2"
35) "quorum"
36) "2"
37) "failover-timeout"
38) "180000"
39) "parallel-syncs"
40) "1"
127.0.0.1:26379>

高可用检查

停止 master容器,等待10s,进入任意sentinel容器,使用sentinel master mymaster命令观察主节点发生变化,观察外挂的Sentinel*.conf 主节点IP发生变化

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
127.0.0.1:26379> sentinel master mymaster
1) "name"
2) "mymaster"
3) "ip"
4) "172.21.0.4" # 这里发现已经发生变化了
5) "port"
6) "6379"
7) "runid"
8) "5114e2ca2ea3eb15a865f2cd5d7c5101e2bf0d34"
9) "flags"
10) "master"
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "520"
19) "last-ping-reply"
20) "520"
21) "down-after-milliseconds"
22) "30000"
23) "info-refresh"
24) "1529"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "1604"
29) "config-epoch"
30) "4"
31) "num-slaves"
32) "2"
33) "num-other-sentinels"
34) "2"
35) "quorum"
36) "2"
37) "failover-timeout"
38) "180000"
39) "parallel-syncs"
40) "1"

  • 还可以进入Redis容器检查Redis运行角色
1
2
3
4
5
6
7
8
9
10
11
reids-cli
127.0.0.1:6379> auth redis_pwd
OK
127.0.0.1:6379> role
1) "slave"
2) "172.21.0.4"
3) (integer) 6379
4) "connected"
5) (integer) 227725
127.0.0.1:6379>

END

使用docker-compose搭建Redis主从测试环境。

源码地址

编写docker-compose配置文件

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
version : '3'

networks:
network-redis:
driver: bridge

services:
master:
image: redis:5.0.5
container_name: redis5.0.6-master
networks:
- network-redis
ports:
- '6379:6379'
command: redis-server --requirepass redis_pwd --masterauth redis_pwd # 启动redis命令
slaves1:
image: redis:5.0.5
container_name: redis5.0.6-slaves1
networks:
- network-redis
ports:
- '6380:6379'
command: redis-server --slaveof redis5.0.6-master 6379 --requirepass redis_pwd --masterauth redis_pwd # 启动redis命令
slaves2:
image: redis:5.0.5
container_name: redis5.0.6-slaves2
networks:
- network-redis
ports:
- '6381:6379'
command: redis-server --slaveof redis5.0.6-master 6379 --requirepass redis_pwd --masterauth redis_pwd # 启动redis命令
  • 指定密码redis_pwd 作为测试密码

启动测试

1
docker-compse -f docker-compose up -d
  • Console
1
2
3
Starting redis5.0.6-master ... done
Starting redis5.0.6-slaves2 ... done
Starting redis5.0.6-slaves1 ... done
  • 检查docker运行
1
docker ps 

  • 使用redis-cli检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
redis-cli
127.0.0.1:6379> auth redis_pwd
OK
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=172.21.0.3,port=6379,state=online,offset=126,lag=0
slave1:ip=172.21.0.4,port=6379,state=online,offset=126,lag=0
master_replid:76d204429eefdd1df062c54ce8eae3b8ba9268b6
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:126
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:126
127.0.0.1:6379>
  • 这里可以有两个slave。

  • 可以使用客户端工具往master写入数据,看slave是否存在数据。

END

本章是整理知识内容,为强化知识长期更新。

网络协议

网络协议是一组确定的规则,这些规则确定如何在同一网络中的不同设备之间传输数据。本质上,它允许连接的设备彼此通信,而不管其内部过程,结构或设计是否有差异。(两个端点都需要了解协议才能进行交流)。

OSI网络模型

OSI英文全称叫做(Open System Interconnection Model). 中文全称叫做开放式系统互联模型. 也叫做网络7层模型,从下到上依次为,物理层,数据链路层,网络层,传输层,会话层,表示层,应用层。

  • OIS概念模型实际上并没有真正实现过,但是我们需要参考这个分层来理解网络协议一般来说把5-7层叫做上层,1-4层叫做下层。

  • 7-应用层 -> 网络流程应用(表示的是用户界面,例如Telnet,HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP)
  • 6-表示层 -> 数据表示 (数据如何呈现,特殊处理->例如加密,比如JPEG、ASCLL、EBCDIC、加密格式等)
  • 5-会话层 -> 主机间的通信(将不同应用程序的数据分开。建立,管理和终止应用之间的会话)
  • 4-传输层 -> 端到端连接(可靠或不可靠的传递,例如TCP,UDP)
  • 3-网络层 -> 地址和最佳路径(提供路由器用于路径的逻辑寻址,比如ICMP IGMP IP(IPV4 IPV6))
  • 2-数据链路层 -> 媒体访问(将位组合成字节,将字节组合成帧,使用MAC地址访问,错误检测-比如HDLC)
  • 1-物理层 -> 二进制传输(在设备之间移动bits。例如V.35)

TCP/IP网络模型

TCP/IP 模型和OSI相比会简单一点,只有四层,分别为数据链路层,网络层,传输层和应用层。

OSI模型与TCP/IP模型

  • 4-应用层 -> 对应于OSI的5-7层
  • 3-传输层 -> 这个是和OSI的第四层想同的
  • 2-网络层 -> 这个是和OSI的第3网络层对应的
  • 1-网络访问层 -> 这个是和OSI的第1-2层所对应的
阅读全文 »