开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Docker部署Golang Http服务

发表于 2021-01-27

前言

最近在工作中有这么一个需求:由于某个服务只在生产环境下部署,测试环境下没有相关服务,但是本地无法访问生产环境的服务,所以我需要mock一个返回特定json的http服务。

服务代码

相关代码已经上传至GitHub:https://github.com/bodhiye/http-fake

main.go代码

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
go复制代码package main

import (
"encoding/json"
"fmt"
"net/http"
)

type fakeReq struct {
URI string `json:"uri"`
}

// 处理application/json类型的POST请求
func fake(w http.ResponseWriter, r *http.Request) {
// 根据请求body创建一个json解析器实例
decoder := json.NewDecoder(r.Body)
// 用于存放参数数据
var req fakeReq
// 解析参数 存入map
decoder.Decode(&req)
// 打印日志
fmt.Printf("url=%s\n", req.URI)

// 返回你需要的json,这里返回了{"code": 200}
fmt.Fprintf(w, `{"code": 200}`)
}

func main() {
http.HandleFunc("/fake", fake)
http.ListenAndServe(":2333", nil)
}

Dockerfile代码

1
2
3
4
5
6
7
dockerfile复制代码FROM golang:latest
MAINTAINER "yeqiongzhou@whu.edu.cn"
WORKDIR /go/src/http-fake
ADD . /go/src/http-fake
RUN go build .
EXPOSE 2333
ENTRYPOINT ["./http-fake"]

部署流程

  1. 编译Dockerfile文件:docker build -t bodhiye/http-fake .编译完成后在终端输入docker images可以查看刚才编译好的http-fake镜像。
  2. 你得注册一个Docker Hub账号,注册成功后登录Docker Hub:docker login -u bodhiye,之后输入密码即可登录成功。
  3. 把镜像上传到Docker Hub:docker push bodhiye/http-fake上传成功后可以在Docker Hub网站上看到刚刚上传的http-fake镜像。
  4. 在你需要部署的服务器或者本地环境下拉取镜像:docker pull bodhiye/http-fake:latest代码中latest表示拉取最新的镜像版本。
  5. 启动http-fake服务:docker run --name test-http -d -p 2048:2333 bodhiye/http-fake将容器服务内部的2333端口映射到本机的2048端口上,并给该容器服务起了一个my-test-http名称。
  6. 测试http-fake服务:curl -X POST -d '{"uri":"https://img.yeqiongzhou.com/test.jpg"}' 127.0.0.1:2048/fake通过curl的方式来访问本地的http-fake服务,可以看到该服务返回了预期的json字段。如果要在本地访问服务器上的该服务,则127.0.0.1替换成服务器的IP地址。
  7. 查看日志:docker logs test-http可以打印出服务相关日志url=https://img.yeqiongzhou.com/test.jpg

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字符缓冲流BufferedReader和BufferedWr

发表于 2021-01-27

问题: 如何使用字符缓冲流,对键盘输入的每一行数据写入到一个新文件中,
缓冲流有哪些

1.字节缓冲流

我们知道,字节缓冲流BufferedInputStream和BufferedOutputStream他们在逐个字节读取的时候效率远高于使用字节流InputStream和OutputStream,我们可以测试如下:

一般字节流读取mp3文件:

我们大概计时了一下,大概需要20秒左右才能复制玩一个8Mb音乐文件,一般字节流通过一个一个字节读取并打印到新文件,这样效率贼低。

下面是使用字符流读取mp3文件:

我计时大概不到两秒钟就复制完写入新文件了,速度比一般字节流起码快10倍以上,效率特别高。

缓冲流默认的缓冲区大小为8kb,当缓冲区大小没被读写的数据填满时,不会主动 将数据写到目标文件中,我们可以通过调用其flush()或close()方法来强制将缓冲区内容写到文件中。
实际上,close()方法中调用的就是flush()方法,缓冲流其实还是将数据写到字节数组中。

2.字符缓冲流

字符缓冲流有BufferedReader和BufferedWriter,他们是以行为单位读取和输出的,效率也比普通字符流高。

字符缓冲流BufferedReader使用:

字符缓冲流进阶使用:

如何将键盘输入的数据输出到文件中:
该目标实现过程中如何将键盘输入的数据转换为字符缓冲流是一个难点,首先需要定义缓冲流输入对象,该对象需定义为字节输入流类型的;

然后将字节流转为字符流对象,再把字符流转为字符缓冲流,这样就可以通过BufferedReader中的readLine()方法将数据读入字符串里。

最后就通过定义字符缓冲输出流,然后写入已获得的字符串数据即可。

总结
缓冲流的效率比一般字节流和字符流速度快多了,我们在项目中可以多使用缓冲流来提高读写数据的效率。

希望大家觉得有用的点亮我的小星星,感谢大伙儿!!!给您笔芯。。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

dubbo服务订阅_Reference注解

发表于 2021-01-27

参考资料:Dubbo系列之(四)服务订阅(1)

介绍

dubbo的服务订阅有两种方式,第一种是通过xml文件的标签<dubbo:reference />,第二种是通过注解@Reference。两者在使用上没有什么区别,标签上的属性都可以在注解上找到对应的配置。在源码实现上,两者存在一定的区别和共同点。

共同点:两者最终都是调用com.alibaba.dubbo.config.ReferenceConfig#get方法来产生代理对象和订阅服务

区别:

  • 标签<dubbo:reference />的实现方式是通过spring自定义标签实现的。dubbo使用ReferenceBean解析标签内容,ReferenceBean实现了FactoryBean接口,因此可通过getObject方法创建具体的实例对象,该方法里面就是调用父类ReferenceConfig#get方法。
+ 
1
2
3
4
5
6
7
8
java复制代码public class DubboNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
// 省略部分代码...
registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
}
}
+ ![image-20201221145847852](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/b78aa47400d40716d2ee760ef4a13495426a1e41d59388aea1152df6bc13b222) + ![image-20201221145924255](https://gitee.com/songjianzaina/juejin_p12/raw/master/img/a5165efa7da40530039163cb4c796f8467a6c51b618f5a29131be0bc3cdd801f)
  • @Reference注解是通过后置处理器实现的,与@Autowired、@Qualifier等注解类似,都是往Spring Bean对象注入指定属性。dubbo框架自己编写类似AutowiredAnnotationBeanPostProcessor的后置处理器,叫做ReferenceAnnotationBeanPostProcessor,通过这个后置处理器来解析被@Reference注解表标注的变量和方法。
+ `com.alibaba.dubbo.config.annotation.Reference`
+ 
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface Reference {
Class<?> interfaceClass() default void.class;
String interfaceName() default "";
String url() default "";
boolean check() default true;
int retries() default 2;
int timeout() default 0;
// 省略部分信息
}

总结

主线:postProcessPropertyValues -> findInjectionMetadata -> inject

步骤:

  1. 利用spring提供的扩展点,InstantiationAwareBeanPostProcessor的postProcessPropertyValues方法,该方法在spring bean对象属性赋值时触发;
  2. dubbo编写ReferenceAnnotationBeanPostProcessor后置处理器,实现InstantiationAwareBeanPostProcessor的postProcessPropertyValues方法;
  3. 先获取目标类上被**@Reference**标注的成员变量和方法的元信息,把元信息集合封装到InjectionMetadata对象;
  4. 遍历被标注的成员变量和成员方法,两者都是通过反射完成属性值的注入。如果是成员变量,调用field.set(),如果是成员方法,调用method.invoke()
  5. 属性值的获取过程主要分为三步:
    1. 创建ReferenceBean对象,并放入缓存
    2. 创建ReferenceBeanInvocationHandler对象,接着执行handler#init方法,里面是调用ReferenceBean#get方法,进入真正的服务订阅
    3. 最后,通过jdk动态代理,返回一个代理对象

ReferenceAnnotationBeanPostProcessor解析

spring相关:属性依赖注入的扩展点

参考资料:

  • InstantiationAwareBeanPostProcessor接口介绍
  • @Autowired自动注入原理

在解析远程服务引入前,我们先了解一个特殊的后置处理器,InstantiationAwareBeanPostProcessor,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {

/**
* 在bean实例化前执行的回调方法,先于postProcessBeforeInitialization执行
*/
Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException;

/**
* 在bean实例化后,属性显式填充和自动注入前回调
*/
boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException;

/**
* 关键!
* 给属性赋值
*/
PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException;

}
单词 含义
Instantiation 表示实例化,对象还未生成
Initialization 表示初始化,对象已经生成

dubbo就是对上面的postProcessPropertyValues方法进行扩展,给对象注入依赖时,创建代理对象,订阅远程服务。

先来看下ReferenceAnnotationBeanPostProcessor的继承关系,然后重点关注postProcessPropertyValues方法。

image-20201221153451683

由于ReferenceAnnotationBeanPostProcessor没有实现postProcessPropertyValues方法,所以我们看到的是其父类AnnotationInjectedBeanPostProcessor

AnnotationInjectedBeanPostProcessor#postProcessPropertyValues

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Override
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {

// 查找当前Bean对象需要注入的成员的元信息,包括成员变量和方法
// 即被@Reference标注的成员,把这些元信息集合封转成一个InjectionMetadata对象
InjectionMetadata metadata = findInjectionMetadata(beanName, bean.getClass(), pvs);
try {
// 注入属性值
metadata.inject(bean, beanName, pvs);
} catch (BeanCreationException ex) {
throw ex;
} catch (Throwable ex) {
throw new BeanCreationException("xxx");
}
return pvs;
}

该方法先通过findInjectionMetadata()得到被注解标注的成员变量和方法的元信息,把这些信息集合封装成InjectionMetadata对象,最后调用inject方法,内部使用for循环将属性值逐个注入。

org.springframework.beans.factory.annotation.InjectionMetadata#inject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable {
Collection<InjectedElement> elementsToIterate =
(this.checkedElements != null ? this.checkedElements : this.injectedElements);

if (!elementsToIterate.isEmpty()) {
boolean debug = logger.isDebugEnabled();
for (InjectedElement element : elementsToIterate) {
if (debug) {
logger.debug("Processing injected element of bean '" + beanName + "': " + element);
}
// 调用具体实现类的inject方法,接下来会分析这块
element.inject(target, beanName, pvs);
}
}
}

spring相关:如何找到@Reference标注的成员

通过上面小节,我们知道当某个Spring Bean对象需要引入dubbo服务时,是通过@Reference注入服务提供者的实例对象,原理是通过postProcessPropertyValues方法注入。接下来,我们需要了解怎么找到@Reference标注的成员,包括成员变量和成员方法。

AnnotationInjectedBeanPostProcessor#findInjectionMetadata

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
java复制代码/**
* 找到需要注入的成员元信息,封装成InjectionMetadata
*
* @param beanName 当前需要被注入的对象名
* @param clazz 当前需要被注入的类对象
* @param pvs 当前需要被注入对象的参数
* @return 返回需要注入的成员元信息
*/
public InjectionMetadata findInjectionMetadata(String beanName, Class<?> clazz, PropertyValues pvs) {
// 获取缓存key,默认是beanName,否则是claaName
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());

// 先从缓存获取,获取不到,或者注入属性的元数据所在的目标类与当前被注入的类不一致,需要重新获取
AnnotationInjectedBeanPostProcessor.AnnotatedInjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
// 双层判断
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
// 与原来的目标类不一致,先清楚参数属性,但是排除需要的参数
if (metadata != null) {
metadata.clear(pvs);
}
try {
// 找到需要注入的属性信息,并放入缓存
metadata = buildAnnotatedMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
} catch (NoClassDefFoundError err) {
throw new IllegalStateException("Failed to introspect object class [" + clazz.getName() +
"] for annotation metadata: could not find class that it depends on", err);
}
}
}
}
return metadata;
}

上面一大段方法,要表达的内容是:先从缓存获取属性的元信息,如果没有,调用buildAnnotatedMetadata方法获取。

AnnotationInjectedBeanPostProcessor#buildAnnotatedMetadata

1
2
3
4
5
6
7
8
9
10
java复制代码private AnnotationInjectedBeanPostProcessor.AnnotatedInjectionMetadata buildAnnotatedMetadata(final Class<?> beanClass) {
// 查找被标注的成员变量的元信息
Collection<AnnotationInjectedBeanPostProcessor.AnnotatedFieldElement> fieldElements =
findFieldAnnotationMetadata(beanClass);
// 查找被标注的成员方法的元信息
Collection<AnnotationInjectedBeanPostProcessor.AnnotatedMethodElement> methodElements =
findAnnotatedMethodMetadata(beanClass);

return new AnnotationInjectedBeanPostProcessor.AnnotatedInjectionMetadata(beanClass, fieldElements, methodElements);
}

AnnotationInjectedBeanPostProcessor#findFieldAnnotationMetadata

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
java复制代码/**
* Finds {@link InjectionMetadata.InjectedElement} Metadata from annotated {@link A} fields
*
* @param beanClass The {@link Class} of Bean
* @return non-null {@link List}
*/
private List<AnnotationInjectedBeanPostProcessor.AnnotatedFieldElement> findFieldAnnotationMetadata(final Class<?> beanClass) {

final List<AnnotationInjectedBeanPostProcessor.AnnotatedFieldElement> elements =
new LinkedList<AnnotationInjectedBeanPostProcessor.AnnotatedFieldElement>();

ReflectionUtils.doWithFields(beanClass, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
// 遍历每个成员变量,并通过getAnnotationType()指定注解类型(@Reference),最后得到结果后存入集合
A annotation = getAnnotation(field, getAnnotationType());
if (annotation != null) {
if (Modifier.isStatic(field.getModifiers())) {
if (logger.isWarnEnabled()) {
logger.warn("@" + getAnnotationType().getName() + " is not supported on static fields: " + field);
}
return;
}
elements.add(new AnnotationInjectedBeanPostProcessor.AnnotatedFieldElement(field, annotation));
}
}
});
return elements;
}

AnnotationInjectedBeanPostProcessor#findAnnotatedMethodMetadata

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
java复制代码/**
* Finds {@link InjectionMetadata.InjectedElement} Metadata from annotated {@link A @A} methods
*
* @param beanClass The {@link Class} of Bean
* @return non-null {@link List}
*/
private List<AnnotationInjectedBeanPostProcessor.AnnotatedMethodElement> findAnnotatedMethodMetadata(final Class<?> beanClass) {

final List<AnnotationInjectedBeanPostProcessor.AnnotatedMethodElement> elements =
new LinkedList<AnnotationInjectedBeanPostProcessor.AnnotatedMethodElement>();

ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Method bridgedMethod = findBridgedMethod(method);
if (!isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
// 遍历每个方法,获取被@Reference标注的方法
A annotation = findAnnotation(bridgedMethod, getAnnotationType());
if (annotation != null && method.equals(ClassUtils.getMostSpecificMethod(method, beanClass))) {
// 省略部分异常检查
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, beanClass);
elements.add(new AnnotationInjectedBeanPostProcessor.AnnotatedMethodElement(method, pd, annotation));
}
}
});
return elements;
}

总的来说,dubbo都是通过AnnotationUtils#findAnnotation方法获取到被注解@Reference标注成员变量和成员方法。

如何实现依赖注入

获取到需要被@Reference标注的成员元信息后,接着就是属性值的注入。dubbo提供了两个注入类,分别是AnnotatedFieldElement和AnnotatedMethodElement,显然,一个用于处理成员变量,一个是用于处理成员方法。

AnnotatedFieldElement#inject

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
// 获取属性的类对象
Class<?> injectedType = field.getType();
// 获取注入的实例对象
injectedBean = getInjectedObject(annotation, bean, beanName, injectedType, this);
// 反射,允许访问
ReflectionUtils.makeAccessible(field);
// 属性赋值
field.set(bean, injectedBean);
}

AnnotatedMethodElement#inject

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
// 获取参数的类对象
Class<?> injectedType = pd.getPropertyType();

injectedBean = getInjectedObject(annotation, bean, beanName, injectedType, this);

ReflectionUtils.makeAccessible(method);
// 调用方法
method.invoke(bean, injectedBean);
}

获取属性值

上面两个方法都需要通过getInjectedObject()获取注入的实例对象,该方法先从缓存获取结果,如果没有,则调用doGetInjectBean()创建对象

ReferenceAnnotationBeanPostProcessor#doGetInjectedBean

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
java复制代码/**
* 该方法是一个模板方法,用来得到一个指定注入类型的对象
*
* @param reference
* @param bean Current bean that will be injected
* @param beanName Current bean name that will be injected
* @param injectedType the type of injected-object
* @param injectedElement {@link InjectionMetadata.InjectedElement}
* @return
* @throws Exception
*/
@Override
protected Object doGetInjectedBean(Reference reference, Object bean, String beanName, Class<?> injectedType,
InjectionMetadata.InjectedElement injectedElement) throws Exception {
// 构建ReferenceBean名称
String referencedBeanName = buildReferencedBeanName(reference, injectedType);
// 构建ReferenceBean对象
ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referencedBeanName, reference, injectedType, getClassLoader());
// 放入缓存
cacheInjectedReferenceBean(referenceBean, injectedElement);
// 创建代理对象
Object proxy = buildProxy(referencedBeanName, referenceBean, injectedType);

return proxy;
}

doGetInjectedBean方法先是构建ReferenceBean对象,然后通过jdk动态代理返回一个代理对象。

在构建代理对象前,需要先创建一个ReferenceBeanInvocationHandler,接着handler会调用ReferenceBean#get方法,这个方法是服务订阅的核心方法,下篇文章会讲解。得到InvocationHandler对象后,就通过jdk动态代理返回代理对象。

源码如下:

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
java复制代码// ReferenceAnnotationBeanPostProcessor#buildProxy
private Object buildProxy(String referencedBeanName, ReferenceBean referenceBean, Class<?> injectedType) {
InvocationHandler handler = buildInvocationHandler(referencedBeanName, referenceBean);
Object proxy = Proxy.newProxyInstance(getClassLoader(), new Class[]{injectedType}, handler);
return proxy;
}

// ReferenceAnnotationBeanPostProcessor#buildInvocationHandler
private InvocationHandler buildInvocationHandler(String referencedBeanName, ReferenceBean referenceBean) {
// 先从缓存获取
ReferenceBeanInvocationHandler handler = localReferenceBeanInvocationHandlerCache.get(referencedBeanName);
if (handler == null) {
handler = new ReferenceBeanInvocationHandler(referenceBean);
}
if (applicationContext.containsBean(referencedBeanName)) { // Is local @Service Bean or not ?
// ReferenceBeanInvocationHandler's initialization has to wait for current local @Service Bean has been exported.
// 本地服务先缓存,等服务暴露再初始化
localReferenceBeanInvocationHandlerCache.put(referencedBeanName, handler);
} else {
// Remote Reference Bean should initialize immediately
// 远程服务需要马上初始化,调用ReferenceBeanInvocationHandler的init方法
handler.init();
}
return handler;
}

ReferenceBeanInvocationHandler源码如下

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
java复制代码private static class ReferenceBeanInvocationHandler implements InvocationHandler {

private final ReferenceBean referenceBean;

private Object bean;

private ReferenceBeanInvocationHandler(ReferenceBean referenceBean) {
this.referenceBean = referenceBean;
}

// 代理对象执行目标方法时,会被invoke拦截
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
if (bean == null) { // If the bean is not initialized, invoke init()
// issue: https://github.com/apache/incubator-dubbo/issues/3429
init();
}
result = method.invoke(bean, args);
} catch (InvocationTargetException e) {
// re-throws the actual Exception.
throw e.getTargetException();
}
return result;
}

private void init() {
// 调用ReferenceBean的get方法,进入真正的服务订阅过程
this.bean = referenceBean.get();
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

因为一次 Kafka 宕机,我明白了 Kafka 高可用原理

发表于 2021-01-27

Kafka宕机引发的高可用问题

问题要从一次Kafka的宕机开始说起。

笔者所在的是一家金融科技公司,但公司内部并没有采用在金融支付领域更为流行的RabbitMQ,而是采用了设计之初就为日志处理而生的Kafka,所以我一直很好奇Kafka的高可用实现和保障。从Kafka部署后,系统内部使用的Kafka一直运行稳定,没有出现不可用的情况。

但最近系统测试人员常反馈偶有Kafka消费者收不到消息的情况,登陆管理界面发现三个节点中有一个节点宕机挂掉了。但是按照高可用的理念,三个节点还有两个节点可用怎么就引起了整个集群的消费者都接收不到消息呢?

要解决这个问题,就要从Kafka的高可用实现开始讲起。

Kafka的多副本冗余设计

不管是传统的基于关系型数据库设计的系统,还是分布式的如zookeeper、redis、Kafka、HDFS等等,实现高可用的办法通常是采用冗余设计,通过冗余来解决节点宕机不可用问题。

首先简单了解Kafka的几个概念:

  • 物理模型

  • 逻辑模型

  • Broker(节点):Kafka服务节点,简单来说一个Broker就是一台Kafka服务器,一个物理节点。
  • Topic(主题):在Kafka中消息以主题为单位进行归类,每个主题都有一个Topic Name,生产者根据Topic Name将消息发送到特定的Topic,消费者则同样根据Topic Name从对应的Topic进行消费。
  • Partition(分区):Topic(主题)是消息归类的一个单位,但每一个主题还能再细分为一个或多个Partition(分区),一个分区只能属于一个主题。主题和分区都是逻辑上的概念,举个例子,消息1和消息2都发送到主题1,它们可能进入同一个分区也可能进入不同的分区(所以同一个主题下的不同分区包含的消息是不同的),之后便会发送到分区对应的Broker节点上。
  • Offset(偏移量):分区可以看作是一个只进不出的队列(Kafka只保证一个分区内的消息是有序的),消息会往这个队列的尾部追加,每个消息进入分区后都会有一个偏移量,标识该消息在该分区中的位置,消费者要消费该消息就是通过偏移量来识别。

其实,根据上述的几个概念,是不是也多少猜到了Kafka的多副本冗余设计实现了?别急,咱继续往下看。

在Kafka 0.8版本以前,是没有多副本冗余机制的,一旦一个节点挂掉,那么这个节点上的所有Partition的数据就无法再被消费。这就等于发送到Topic的有一部分数据丢失了。

在0.8版本后引入副本记者则很好地解决宕机后数据丢失的问题。副本是以Topic中每个Partition的数据为单位,每个Partition的数据会同步到其他物理节点上,形成多个副本。

每个Partition的副本都包括一个Leader副本和多个Follower副本,Leader由所有的副本共同选举得出,其他副本则都为Follower副本。在生产者写或者消费者读的时候,都只会与Leader打交道,在写入数据后Follower就会来拉取数据进行数据同步。

就这么简单?是的,基于上面这张多副本架构图就实现了Kafka的高可用。当某个Broker挂掉了,甭担心,这个Broker上的Partition在其他Broker节点上还有副本。你说如果挂掉的是Leader怎么办?那就在Follower中在选举出一个Leader即可,生产者和消费者又可以和新的Leader愉快地玩耍了,这就是高可用。

你可能还有疑问,那要多少个副本才算够用?Follower和Leader之间没有完全同步怎么办?一个节点宕机后Leader的选举规则是什么?

直接抛结论:

多少个副本才算够用? 副本肯定越多越能保证Kafka的高可用,但越多的副本意味着网络、磁盘资源的消耗更多,性能会有所下降,通常来说副本数为3即可保证高可用,极端情况下将replication-factor参数调大即可。

Follower和Lead之间没有完全同步怎么办? Follower和Leader之间并不是完全同步,但也不是完全异步,而是采用一种ISR机制(In-Sync Replica)。每个Leader会动态维护一个ISR列表,该列表里存储的是和Leader基本同步的Follower。如果有Follower由于网络、GC等原因而没有向Leader发起拉取数据请求,此时Follower相对于Leader是不同步的,则会被踢出ISR列表。所以说,ISR列表中的Follower都是跟得上Leader的副本。

一个节点宕机后Leader的选举规则是什么? 分布式相关的选举规则有很多,像Zookeeper的Zab、Raft、Viewstamped Replication、微软的PacificA等。而Kafka的Leader选举思路很简单,基于我们上述提到的ISR列表,当宕机后会从所有副本中顺序查找,如果查找到的副本在ISR列表中,则当选为Leader。另外还要保证前任Leader已经是退位状态了,否则会出现脑裂情况(有两个Leader)。怎么保证?Kafka通过设置了一个controller来保证只有一个Leader。

Ack参数决定了可靠程度

另外,这里补充一个面试考Kafka高可用必备知识点:request.required.asks参数。

Asks这个参数是生产者客户端的重要配置,发送消息的时候就可设置这个参数。该参数有三个值可配置:0、1、All。

第一种是设为0,意思是生产者把消息发送出去之后,之后这消息是死是活咱就不管了,有那么点发后即忘的意思,说出去的话就不负责了。不负责自然这消息就有可能丢失,那就把可用性也丢失了。

第二种是设为1,意思是生产者把消息发送出去之后,这消息只要顺利传达给了Leader,其他Follower有没有同步就无所谓了。存在一种情况,Leader刚收到了消息,Follower还没来得及同步Broker就宕机了,但生产者已经认为消息发送成功了,那么此时消息就丢失了。注意,设为1是Kafka的默认配置!!!可见Kafka的默认配置也不是那么高可用,而是对高可用和高吞吐量做了权衡折中。

第三种是设为All(或者-1),意思是生产者把消息发送出去之后,不仅Leader要接收到,ISR列表中的Follower也要同步到,生产者才会任务消息发送成功。

进一步思考,Asks=All就不会出现丢失消息的情况吗?答案是否。当ISR列表只剩Leader的情况下,Asks=All相当于Asks=1,这种情况下如果节点宕机了,还能保证数据不丢失吗?因此只有在Asks=All并且有ISR中有两个副本的情况下才能保证数据不丢失。

解决问题

绕了一大圈,了解了Kafka的高可用机制,终于回到我们一开始的问题本身,Kafka的一个节点宕机后为什么不可用?

我在开发测试环境配置的Broker节点数是3,Topic是副本数为3,Partition数为6,Asks参数为1。

当三个节点中某个节点宕机后,集群首先会怎么做?没错,正如我们上面所说的,集群发现有Partition的Leader失效了,这个时候就要从ISR列表中重新选举Leader。如果ISR列表为空是不是就不可用了?并不会,而是从Partition存活的副本中选择一个作为Leader,不过这就有潜在的数据丢失的隐患了。

所以,只要将Topic副本个数设置为和Broker个数一样,Kafka的多副本冗余设计是可以保证高可用的,不会出现一宕机就不可用的情况(不过需要注意的是Kafka有一个保护策略,当一半以上的节点不可用时Kafka就会停止)。那仔细一想,Kafka上是不是有副本个数为1的Topic?

问题出在了__consumer_offset上,__consumer_offset是一个Kafka自动创建的Topic,用来存储消费者消费的offset(偏移量)信息,默认Partition数为50。而就是这个Topic,它的默认副本数为1。如果所有的Partition都存在于同一台机器上,那就是很明显的单点故障了!当将存储__consumer_offset的Partition的Broker给Kill后,会发现所有的消费者都停止消费了。

这个问题怎么解决?

第一点,需要将__consumer_offset删除,注意这个Topic时Kafka内置的Topic,无法用命令删除,我是通过将logs删了来实现删除。

第二点,需要通过设置offsets.topic.replication.factor为3来将__consumer_offset的副本数改为3。

通过将__consumer_offset也做副本冗余后来解决某个节点宕机后消费者的消费问题。

最后,关于为什么__consumer_offset的Partition会出现只存储在一个Broker上而不是分布在各个Broker上感到困惑,如果有朋友了解的烦请指教~

总结了一些2020年的面试题,这份面试题的包含的模块分为19个模块,分别是: Java基础、容器、多线程、反射、对象拷贝、JavaWeb异常、网络、设计模式、Spring/SpringMVC、SpringBoot/SpringCloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM。

**获取以下资料:关注公众号:【有故事的程序员】,获取学习资料。

记得点个关注+评论哦~**

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

13K点赞都基于 Vue+Spring 前后端分离管理系统E

发表于 2021-01-27

其实项目网上有很多了,但是教程比较详细的没多少,今天分享的项目从安装部署到代码具体功能都有很详细都说明

图片

eladmin 是一款基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。欢迎关注Java项目分享

这个开源项目基本稳定,并且后续作者还会继续优化。完全开源!这个真的要为原作者点个赞,如果大家觉得这个项目有用的话,建议可以稍微捐赠一下原作者支持一下。后端整理代码质量、表设计等各个方面来说都是很不错的。前后端分离,前端使用的是国内常用的 vue 框架,也比较容易上手。欢迎关注Java项目分享

系统功能

  • 用户管理:提供用户的相关配置,新增用户后,默认密码为123456
  • 角色管理:对权限与菜单进行分配,可根据部门设置角色的数据权限
  • 菜单管理:已实现菜单动态路由,后端可配置化,支持多级菜单
  • 部门管理:可配置系统组织架构,树形表格展示
  • 岗位管理:配置各个部门的职位
  • 字典管理:可维护常用一些固定的数据,如:状态,性别等
  • 系统日志:记录用户操作日志与异常日志,方便开发人员定位拍错
  • SQL监控:采用druid 监控数据库访问性能,默认用户名admin,密码123456
  • 定时任务:整合Quartz做定时任务,加入任务日志,任务运行情况一目了然
  • 代码生成:高灵活度生成前后端代码,减少大量重复的工作任务
  • 邮件工具:配合富文本,发送html格式的邮件
  • 七牛云存储:可同步七牛云存储的数据到系统,无需登录七牛云直接操作云数据
  • 支付宝支付:整合了支付宝支付并且提供了测试账号,可自行测试
  • 服务监控:监控服务器的负载情况
  • 运维管理:一键部署你的应用

项目结构

项目采用按功能分模块的开发方式,结构如下

  • eladmin-common 为系统的公共模块,各种工具类,公共配置存在该模块
  • eladmin-system 为系统核心模块也是项目入口模块,也是最终需要打包部署的模块
  • eladmin-logging 为系统的日志模块,其他模块如果需要记录日志需要引入该模块
  • eladmin-tools 为第三方工具模块,包含:图床、邮件、云存储、本地存储、支付宝
  • eladmin-generator 为系统的代码生成模块,代码生成的模板在 system 模块中

详细结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
markdown复制代码- eladmin-common 公共模块
- annotation 为系统自定义注解
- aspect 自定义注解的切面
- base 提供了Entity、DTO基类和mapstruct的通用mapper
- config 自定义权限实现、redis配置、swagger配置、Rsa配置等
- exception 项目统一异常的处理
- utils 系统通用工具类
- eladmin-system 系统核心模块(系统启动入口)
- config 配置跨域与静态资源,与数据权限
- thread 线程池相关
- modules 系统相关模块(登录授权、系统监控、定时任务、运维管理等)
- eladmin-logging 系统日志模块
- eladmin-tools 系统第三方工具模块
- eladmin-generator 系统代码生成模块

程序汪发现的亮点

  • 统一异常处理设计
  • 注解权限的设计
  • 接口级别的限流设计
  • 比较完善的工具,如支付宝,邮件,定时任务,各种监控的实现
  • 缓存redis

图片

图片

图片

图片

后台首页

特性

  • 技术栈:使用 SpringBoot/Jpa/Security、Redis、Vue、ElementUI 等技术开发;
  • 模块化:后端采用按功能分模块开发方式,提升开发,测试效率;
  • 高效率:项目简单可配,内置代码生成器,配置好表信息就能一键生成前后端代码;
  • 分离式:前后端完全分离,前端基于 Vue,后端基于 Spring boot;
  • 响应式:支持电脑、平板、手机等所有主流设备访问;
  • 易用性:几乎可用于所有Web项目的开发,如 OA、Cms,网址后台管理等;欢迎关注Java项目分享

另外,作者最近还提供了一份详细的文档帮助小伙伴们学习这个项目。

文档从环境搭建到后端每一块的详细设计都有涵盖,非常适合拿来学习!

图片

欢迎关注公众号 【码农开花】一起学习成长
我会一直分享Java干货,也会分享免费的学习资料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 Github 上的高仿 app 项目,B站 微博 微信

发表于 2021-01-27

学技术的,多多少少都仿过出名的产品。

一来,可以练练手,二来对知识点能查漏补缺。欢迎关注Java项目分享

更重要的一点是能给你带来及时的正反馈,让学习的过程不那么枯燥。

今天给大家介绍 GIthub 上几个仿造大厂的 app 项目。我是程序汪

\高仿微信**

▼

iOS 版:

1
arduino复制代码Github 地址:https://github.com/nacker/LZEasemob3

界面截图:

图片

Android 版:

1
arduino复制代码Github 地址:https://github.com/GitLqr/LQRWeChat

界面截图:

图片

高仿 youtube

▼

iOS 版:

1
arduino复制代码Github 地址:https://github.com/aslanyanhaik/youtube-iOS

界面截图:

图片

Android:

1
arduino复制代码Github 地址:https://github.com/TeamNewPipe/NewPipe

界面截图:

图片

\高仿网易云音乐**

▼

iOS 版:

1
arduino复制代码Github 地址:https://github.com/QuintGao/GKAudioPlayerDemo

界面截图:

img

Android:

1
bash复制代码Github 地址:https://github.com/aa112901/remusic

界面截图:

图片

\高仿Bilibili**

▼

iOS 版:

1
arduino复制代码Github 地址:https://github.com/MichaelHuyp/Bilibili_Wuxianda

界面截图:

图片

Android:

1
bash复制代码Github 地址:https://github.com/HotBitmapGG/bilibili-android-client

界面截图:

图片

\高仿微博**

▼

iOS 版:

1
arduino复制代码Github 地址:https://github.com/sam408130/DSLolita

Android:

1
arduino复制代码Github 地址:https://github.com/wenmingvs/WeiBo

界面截图:

图片

来源:作者:水哥;来源:GitHubClub
欢迎关注公众号 【码农开花】一起学习成长
我会一直分享Java干货,也会分享免费的学习资料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux mysqldump命令的用法

发表于 2021-01-27

mysqldump 属于数据库逻辑备份程序,通常使用它来对一个或多个 MySQL 数据库进行备份或还原,另外还可以将数据库传输给其他的 MySQL 服务器。下面良许小编就将Linux mysqldump命令的用法进行详述,希望对大家有所帮助。

Linux命令

在使用 mysldump 来备份数据库表时,必须要求该账户拥有 SELECT 权限,SHOW VIEW 权限用于备份视图,TRIGGER 权限用于备份触发器。

注意,其他的命令选项可能还需要拥有更多的权限才能完成。

由于 mysqldump 需要通过重建 SQL 语句来实现备份功能,对于数据量比较大的数据库备份与还原操作,速度都比较慢,因此 mysqldump 不适用于大数据的备份。当打开 mysqldump 备份文件时,备份文件的内容就是数据库的 SQL 语言重现。对于大数据的备份与还原,通常会选择物理备份,即直接复制数据文件,就可以实现快速的数据还原工作。

使用 mysqldump 可以备份数据库中的数据表,也可以备份整个数据库,还可以备份 MySQL 系统中的所有数据库。对于使用 mysqldump 工具备份的数据库文件,可以使用 mysql 命令工具还原数据。

注意,在备份整个数据库时,不能在数据库后使用数据表的名称。

mysqldump 命令的语法格式如下:

[root@liangxu ~]# mysqldump [选项] db_name [table_name]
[root@liangxu ~]# mysqldump [选项] –databases db_name …
[root@liangxu ~]# mysqldump [选项] –all-databases

mysqldump 中的常用选项可以通过 [mysqldump] 和 [client] 写入配置文件。mysqldump 命令的常用选项及说明如表 1 所示。

表 1 mysqldump命令的常用选项及说明

选 项 说 明
–add-drop-database 在备份文件中添加、删除相同数据库的 SQL 语句
–add-drop-table 在备份文件中添加、删除相同数据表的 SQL 语句
–add-drop-trigger 在备份文件中添加、删除相同触发器的 SQL 语句
–add-locks 在备份数据表前后添加表锁定与解锁 SQL 语句
–all-databases 备份所有数据库中的数据表
–apply-slave-statements 在 CHANGE MASTER 前添加 STOP SLAVE 语句
–bind-address=ip_address 使用指定的网络接口连接 MySQL 服务器
–comments 添加备份文件的注释
–create-options 在 CREATE TABLE 语句中包含所有的 MySQL 特性
–databases 备份指定的数据库
–debug 创建 debugging 日志
–default-character-set=charsename 设置默认字符集
–host,-h 设置需要连接的主机
–ignore-table 设置不需要备份的数据表,该选项可以使用多次
–lock-all-tables 设置全局锁,锁定所有的数据表以保证备份数据的完整性
–no-create-db,-n 只导出数据而不创建数据库
–no-create-info 只导出数据而不创建数据表
–no-date 不备份数据内容,用于备份表结构
–password,-p 还用密码连接服务器
–port=port_num 使用指定端口号连接服务器
–replace 使用 REPLACE 语句代替 INSERT 语句

mysqldump工具的使用方法如下:

  1. 备份所有的数据库,如下所示:

[root@liangxu ~]# mysqldump -u root -p –all-databases > all database sql
Enter password:

  1. 备份 mysql 数据库下的 user 数据包,如下所示:

[root@liangxu ~]# mysqldump -u root -p myaql user > user_table
Enter password:

  1. 使用 all_database_sql 数据库备份文件还原数据库,如下所示:

[root@liangxu ~]# mysql -u root -p myaql < all-database_sql
Enter password:

  1. 使用 user_table 数据库备份文件还原数据表,如下所示:

[root@liangxu ~]# mysql -u root -p myaql < user_table
Enter password:

注意,所有的备份和还原操作都必须在输入命令后,输入密码。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

05-自动登录

发表于 2021-01-27

自动登录

自动登录的原理是将用户的登录信息保存在用户浏览器的cookie中,当用户下次访问时实现自动校验并建立登录状态的一种机制。

Spring Security 提供了两种令牌机制

  • 用散列算法加密用户必要的登录信息并生成令牌
  • 数据库等持久化存储机制持久化令牌

散列算法

将用户名(username)、过期时间(expirationTime)、密码(password)和散列盐值(key)进行md5运算得到令牌。再下次登录时会使用base64简单解码得到用户名、过期时间和加密散列值,然后通过用户名得到密码,接着重新计算与旧的散列值进行对比确认是否有效。

1
2
java复制代码hashInfo = md5Hex(username+":"+expirationTime+":"+password+":"+key);
remeberCookie = base64(username+":"+expirationTime+":"+hashInfo)

在Spring Security中实现该功能非常简单,只需要在javaconfig进行配置即可。这里在learn-05上进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    @Override
protected void configure(HttpSecurity http) throws Exception {
http
// 在验证用户名和密码之前验证验证码
.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class).
..... // 省略其他代码
.and()
.rememberMe()
// 定义过期时间 一周
.tokenValiditySeconds(7 * 24 * 60 * 60)
// 定义记住我 在前端的name
.rememberMeParameter("remember-me")
// 在cookie中存放显示的名称
.rememberMeCookieName("remember-cookies");

}

在前端登录页面添加记住我的功能。

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
html复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div>
用户名<input name="username" id="username"><br>
密码 <input type="password" id="password"><br>
验证码<input type="text" id="kaptcha">
<img src="/kaptcha" alt="验证码" id="img_cha"><br>
<br>
<label for="rememberMe">一周之内免登陆</label><input type="checkbox" id="rememberMe"/>
<button id="btn">登录</button>
<br>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script>

$("#img_cha").click(() => {
console.log("hehhe");
$('#img_cha').attr("src", "/kaptcha");
});


$("#btn").click(() => {
let username = $("#username").val();
let password = $("#password").val();
let checked = $("#rememberMe").is(":checked");
let captcha = $("#kaptcha").val();
let form = {
"username": username,
"password": password,
"captcha": captcha,
"remember-me": checked,
};
$.ajax({
url: "/login",
method: "post",
data: form,
success: (res) => {
if (res.code === 200) {
location.href = "/index"
} else {
console.log(res);
alert(res.data);
$("#username").val("");
$("#password").val("")
}
}
})
})
</script>

</body>
</html>

存在的问题

在每次服务重启之后,key的值都会进行更新导致重启之前的cookie失效而且如果是多实例部署的情况下,由于实例之间的key值不同,所以当用户访问另一个实例的时候自动登录状态就会失效。合理的用法是指定key,这里测试即使是指定了key在重启之后仍然需要进行登录。

1
2
java复制代码   .rememberMeCookieName("remember-cookies")
.userDetailsService(userDetailsService()).key("fba675ec-9a6b-489a-bb8a-607f1558c18f");

持久化令牌

持久化在交互逻辑上和散列加密是一致的,都是用户在勾选了remember-me后将生成的令牌发送给浏览器,用户在下次访问系统时读取该令牌进行认证。不同的是持久化令牌采取了更加严格的验证。

在持久化令牌方案中最核心的是series和token两个值,都是采用MD5散列过的随机字符串,不同的是series在用户使用密码进行重新登录后进行更新,而token则是每次会话都会进行更新。自动登录并不会将series进行更改,当令牌在未使用时就被盗用时,系统会在非法验证用户通过后刷新token。此时的合法用户的token也会失效,系统因此可以对合法用户进行提醒 用户账号可能被盗用。

创建持久化令牌的数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for persistent_logins
-- ----------------------------
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`series` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`last_used` timestamp(0) NULL DEFAULT NULL,
PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

引入新的依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码   <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>

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

配置数据库连接

1
2
3
4
5
6
yml复制代码spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai

实现repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    @Resource
private DataSource dataSource;

/**
* save remember-me to database
*
* @return {@link org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl}
*/
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}

实现自定义退出

实现自定义退出

1
2
3
4
5
6
7
8
java复制代码@Component
public class WebLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 自定义的业务逻辑 如果是前后端分离可以不用跳转 返回json 由前端进行控制页面的跳转
response.sendRedirect("/login.html");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码 @Resource
private WebLogoutSuccessHandler logoutSuccessHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {

http
// 在验证用户名和密码之前验证验证码
.addFilterBefore(captchaCodeFilter, UsernamePassw
.... // 省略代码
.and().logout()
// 退出成功的规则 退出成功会清除session信息
// .logoutSuccessUrl("/login.html")
// 自定义退出功能
.logoutSuccessHandler(logoutSuccessHandler)
// 删除浏览器的cookie
.deleteCookies("JSESSIONID")
;

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊一聊:Service层你觉得有用吗?

发表于 2021-01-27

前段日子在社群(点击加入)里看到有人讨论关于Service层接口的问题,DD也经常碰到周围的新人有问过一些类似的问题:一定要写个Service层的接口吗?Service层的接口到底用做什么用的呢?好像都没什么用啊?

说说我的看法:

Service层在业务逻辑不复杂的时候,似乎是没有什么用,但是随着应用迭代,业务逻辑变得复杂了之后,这一层是非常有用的。

主要表现在这几个方面:

1、更适合用来处理复杂的业务逻辑,可能会涉及多张表的操作,甚至还混杂着消息投递、接口调用等一系列的复杂综合性事务,这也是我们常说的事务管理所处的层次。

2、对表现层的复用支持,往往我们一个业务逻辑处理,不会单单只应用在一个API接口或页面上,如果直接把这部分内容写到Controller中,那当出现重复操作的时候就会产生复制黏贴,以后再要维护这段逻辑就麻烦了

3、对单元测试的支持,通过单独的一层service实现业务逻辑,那么对于业务逻辑的单元测试会更容易编写,只需要对service来编写就可以了;而web层的单元测试就不需要关注业务本身,只需要关注反馈格式就行了;不然web层就既要考虑业务逻辑的计算,还要考虑web反馈的格式验证,太过复杂。

4、业务逻辑的组装支持,因为Controller中依赖的是Service接口的定义,而具体实现可以有很多种,随着不同的需要可以注入不同的实现,可以比较好的实现多种业务逻辑版本共存。而如果直接把业务逻辑写了Controller,再要替换的时候,就比较麻烦了。

所以,Service层的设计是非常有必要的,这在单体应用的可维护性和可测试性上都占据了非常重要的地位。

换你思考了,你觉得Service层接口是否有必要呢?

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

自从上了K8S,项目更新都不带停机的!

发表于 2021-01-27

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

如果你看了《Kubernetes太火了!花10分钟玩转它不香么?》一文的话,基本上已经可以玩转K8S了。其实K8S中还有一些高级特性也很值得学习,比如弹性扩缩应用、滚动更新、配置管理、存储卷、网关路由等。今天我们就来了解下这些高级特性,希望对大家有所帮助!

核心概念

首先我们先来了解一些核心概念,了解这些核心概念对使用K8S的高级特性很有帮助。

ReplicaSet

ReplicaSet确保任何时间都有指定数量的Pod副本在运行。通常用来保证给定数量的、完全相同的Pod的可用性。建议使用Deployment来管理ReplicaSet,而不是直接使用ReplicaSet。

ConfigMap

ConfigMap是一种API对象,用来将非机密性的数据保存到键值对中。使用时,Pod可以将其用作环境变量、命令行参数或者存储卷中的配置文件。使用ConfigMap可以将你的配置数据和应用程序代码分开。

Volume

Volume指的是存储卷,包含可被Pod中容器访问的数据目录。容器中的文件在磁盘上是临时存放的,当容器崩溃时文件会丢失,同时无法在多个Pod中共享文件,通过使用存储卷可以解决这两个问题。

常用的存储卷有如下几种:

  • configMap:configMap卷提供了向Pod注入配置数据的方法。ConfigMap对象中存储的数据可以被configMap类型的卷引用,然后被Pod中运行的容器化应用使用。
  • emptyDir:emptyDir卷可用于存储缓存数据。当Pod分派到某个Node上时,emptyDir卷会被创建,并且Pod在该节点上运行期间,卷一直存在。当Pod被从节点上删除时emptyDir卷中的数据也会被永久删除。
  • hostPath:hostPath卷能将主机节点文件系统上的文件或目录挂载到你的Pod中。在Minikube中的主机指的是Minikube所在虚拟机。
  • local:local卷所代表的是某个被挂载的本地存储设备,例如磁盘、分区或者目录。local卷只能用作静态创建的持久卷,尚不支持动态配置。
  • nfs:nfs卷能将NFS(网络文件系统)挂载到你的Pod中。
  • persistentVolumeClaim:persistentVolumeClaim卷用来将持久卷(PersistentVolume)挂载到Pod中。持久卷(PV)是集群中的一块存储,可以由管理员事先供应,或者使用存储类(Storage Class)来动态供应,持久卷是集群资源类似于节点。

Ingress

Ingress类似于K8S中的网关服务,是对集群中服务的外部访问进行管理的API对象,典型的访问方式是HTTP。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟托管。

高级特性

扩缩应用

当流量增加时,我们需要扩容应用程序满足用户需求。当流量减少时,需要缩放应用以减少服务器开销。在K8S中扩缩是通过改变Deployment中的副本数量来实现的。

  • 获取所有Deployment可使用如下命令:
1
bash复制代码kubectl get deployments
1
2
bash复制代码NAME               READY   UP-TO-DATE   AVAILABLE   AGE
kubernetes-nginx 1/1 1 1 43h
  • 获取所有ReplicaSet可使用如下命令:
1
bash复制代码kubectl get rs
1
2
bash复制代码NAME                          DESIRED   CURRENT   READY   AGE
kubernetes-nginx-78bcc44665 1 1 1 43h
  • 对应用进行扩容操作,扩容到4个实例,再查看所有:
1
bash复制代码kubectl scale deployments/kubernetes-nginx --replicas=4
1
2
3
bash复制代码[macro@linux-local root]$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
kubernetes-nginx 4/4 4 4 43h
  • 查看所有Pod,发现已经有4个运行在不同的IP地址上了;
1
bash复制代码kubectl get pods -o wide
1
2
3
4
5
bash复制代码NAME                                READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 2 43h 172.17.0.3 minikube <none> <none>
kubernetes-nginx-78bcc44665-dvq4t 1/1 Running 0 84s 172.17.0.8 minikube <none> <none>
kubernetes-nginx-78bcc44665-thzg9 1/1 Running 0 84s 172.17.0.7 minikube <none> <none>
kubernetes-nginx-78bcc44665-w7xqd 1/1 Running 0 84s 172.17.0.6 minikube <none> <none>
  • 对应用进行缩放操作,缩放到2个实例;
1
bash复制代码kubectl scale deployments/kubernetes-nginx --replicas=2
1
2
3
bash复制代码NAME                                READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 2 44h 172.17.0.3 minikube <none> <none>
kubernetes-nginx-78bcc44665-w7xqd 1/1 Running 0 11m 172.17.0.6 minikube <none> <none>

滚动更新

滚动更新允许通过使用新的实例逐步更新Pod实例,零停机进行Deployment更新。K8S不仅可以实现滚动更新,还可以支持回滚操作。

  • 目前运行了4个Nginx1.10版本的实例:
1
2
3
4
5
6
bash复制代码[macro@linux-local root]$ kubectl get pods
NAME READY STATUS RESTARTS AGE
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 2 44h
kubernetes-nginx-78bcc44665-jpw2g 1/1 Running 0 5s
kubernetes-nginx-78bcc44665-w7xqd 1/1 Running 0 59m
kubernetes-nginx-78bcc44665-xx8s5 1/1 Running 0 5s
  • 可以通过kubectl describe命令来查看镜像版本号:
1
2
3
bash复制代码[macro@linux-local root]$ kubectl describe pods |grep Image
Image: nginx:1.10
Image ID: docker-pullable://nginx@sha256:6202beb06ea61f44179e02ca965e8e13b961d12640101fca213efbfd145d7575
  • 通过kubectl set image命令来更新Nginx镜像的版本号为1.19,此时K8S会执行滚动更新,逐步停止1.10版本的实例并启动1.19版本的实例;
1
2
bash复制代码# 命令格式 kubectl set image Deployment的名称 容器名称=容器镜像:镜像版本号
kubectl set image deployments/kubernetes-nginx nginx=nginx:1.19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码# 停止1个旧实例并创建2个新实例
NAME READY STATUS RESTARTS AGE
kubernetes-nginx-66f67cd758-rbcz5 0/1 ContainerCreating 0 11s
kubernetes-nginx-66f67cd758-s9ck8 0/1 ContainerCreating 0 11s
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 2 45h
kubernetes-nginx-78bcc44665-jpw2g 0/1 Terminating 0 15m
kubernetes-nginx-78bcc44665-w7xqd 1/1 Running 0 75m
kubernetes-nginx-78bcc44665-xx8s5 1/1 Running 0 15m
# 1个实例已被停止2个新实例仍创建中
NAME READY STATUS RESTARTS AGE
kubernetes-nginx-66f67cd758-rbcz5 0/1 ContainerCreating 0 30s
kubernetes-nginx-66f67cd758-s9ck8 0/1 ContainerCreating 0 30s
kubernetes-nginx-78bcc44665-8fnnn 1/1 Running 2 45h
kubernetes-nginx-78bcc44665-w7xqd 1/1 Running 0 75m
kubernetes-nginx-78bcc44665-xx8s5 1/1 Running 0 15m
# 4个新实例均已创建完成
NAME READY STATUS RESTARTS AGE
kubernetes-nginx-66f67cd758-jn926 1/1 Running 0 48s
kubernetes-nginx-66f67cd758-rbcz5 1/1 Running 0 3m12s
kubernetes-nginx-66f67cd758-s9ck8 1/1 Running 0 3m12s
kubernetes-nginx-66f67cd758-smr7n 1/1 Running 0 44s
  • 此时再使用kubectl describe命令来查看镜像版本号,发现Nginx已经更新至1.19版本:
1
2
3
bash复制代码[macro@linux-local root]$ kubectl describe pods |grep Image
Image: nginx:1.19
Image ID: docker-pullable://nginx@sha256:4cf620a5c81390ee209398ecc18e5fb9dd0f5155cd82adcbae532fec94006fb9
  • 如果想回滚到原来的版本的话,直接使用kubectl rollout undo命令即可。
1
bash复制代码kubectl rollout undo deployments/kubernetes-nginx

配置管理

ConfigMap允许你将配置文件与镜像文件分离,以使容器化的应用程序具有可移植性。接下来我们演示下如何将ConfigMap的的属性注入到Pod的环境变量中去。

  • 添加配置文件nginx-config.yaml用于创建ConfigMap,ConfigMap名称为nginx-config,配置信息存放在data节点下:
1
2
3
4
5
6
7
yaml复制代码apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: default
data:
nginx-env: test
  • 应用nginx-config.yaml文件创建ConfigMap:
1
bash复制代码kubectl create -f nginx-config.yaml
  • 获取所有ConfigMap:
1
bash复制代码kubectl get configmap
1
2
3
bash复制代码NAME               DATA   AGE
kube-root-ca.crt 1 2d22h
nginx-config 1 13s
  • 通过yaml格式查看ConfigMap中的内容:
1
bash复制代码kubectl get configmaps nginx-config -o yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yaml复制代码apiVersion: v1
data:
nginx-env: test
kind: ConfigMap
metadata:
creationTimestamp: "2021-01-08T01:49:44Z"
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:nginx-env: {}
manager: kubectl-create
operation: Update
time: "2021-01-08T01:49:44Z"
name: nginx-config
namespace: default
resourceVersion: "61322"
uid: a477567f-2aff-4a04-9a49-f19220baf0d3
  • 添加配置文件nginx-deployment.yaml用于创建Deployment,部署一个Nginx服务,在Nginx的环境变量中引用ConfigMap中的属性:
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
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.10
ports:
- containerPort: 80
env:
- name: NGINX_ENV # 在Nginx中设置环境变量
valueFrom:
configMapKeyRef:
name: nginx-config # 设置ConfigMap的名称
key: nginx-env # 需要取值的键
  • 应用配置文件文件创建Deployment:
1
bash复制代码kubectl apply -f nginx-deployment.yaml
  • 创建成功后查看Pod中的环境变量,发现NGINX_ENV变量已经被注入了;
1
bash复制代码kubectl exec deployments/nginx-deployment -- env
1
2
3
bash复制代码PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=nginx-deployment-66fcf997c-xxdsb
NGINX_ENV=test

存储卷使用

通过存储卷,我们可以把外部数据挂载到容器中去,供容器中的应用访问,这样就算容器崩溃了,数据依然可以存在。

  • 记得之前我们使用Docker部署Nginx的时候,将Nginx的html、logs、conf目录从外部挂载到了容器中;
1
2
3
4
5
bash复制代码docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
  • Minikube可以认为是一台虚拟机,我们可以用Minikube的ssh命令来访问它;
1
bash复制代码minikube ssh
  • Minikube中默认有一个docker用户,我们先重置下它的密码;
1
bash复制代码sudo passwd docker
  • 在Minikube中创建mydata目录;
1
bash复制代码midir /home/docker/mydata
  • 我们需要把Nginx的数据目录复制到Minikube中去,才能实现目录的挂载,注意docker用户只能修改/home/docker目录中的文件,我们通过scp命令来复制文件;
1
bash复制代码scp -r /home/macro/mydata/nginx docker@192.168.49.2:/home/docker/mydata/nginx
  • 添加配置文件nginx-volume-deployment.yaml用于创建Deployment:
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
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-volume-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.10
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html-volume
- mountPath: /var/log/nginx
name: logs-volume
- mountPath: /etc/nginx
name: conf-volume
volumes:
- name: html-volume
hostPath:
path: /home/docker/mydata/nginx/html
type: Directory
- name: logs-volume
hostPath:
path: /home/docker/mydata/nginx/logs
type: Directory
- name: conf-volume
hostPath:
path: /home/docker/mydata/nginx/conf
type: Directory
  • 应用配置文件创建Deployment;
1
bash复制代码kubectl apply -f nginx-volume-deployment.yaml
  • 添加配置文件nginx-service.yaml用于创建Service;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
  • 应用配置文件创建Service;
1
bash复制代码kubectl apply -f nginx-service.yaml
  • 查看下Service服务访问端口;
1
2
3
4
5
bash复制代码[macro@linux-local nginx]$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6d23h
kubernetes-nginx NodePort 10.106.227.54 <none> 80:30158/TCP 5d22h
nginx-service NodePort 10.103.72.111 <none> 80:30080/TCP 7s
  • 通过CURL命令可以访问Nginx首页信息。
1
bash复制代码curl $(minikube ip):30080

网关路由

Ingress可以作为K8S的网关来使用,能提供服务路由和负载均衡等功能。

  • Minikube默认没有启用Ingress插件,需要手动开启;
1
bash复制代码minikube addons enable ingress
  • 开启Ingress过程中遇到了一个坑,会在验证的时候卡主,其实是Minikube内部无法下载Ingress镜像导致的:
1
2
bash复制代码[macro@linux-local ~]$ minikube addons enable ingress
* Verifying ingress addon...
  • 解决该问题需要手动下载第三方镜像,并标记为需要的镜像,并重新启用Ingress插件;
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码# 查找启动有问题的Pod
kubectl get pods -n kube-system
# 查看启动失败原因
kubectl describe ingress-nginx-controller-xxx -n kube-system
# 连接到Minikube
minikube ssh
# 原来需要下载的镜像(已经无法下载)
docker pull us.gcr.io/k8s-artifacts-prod/ingress-nginx/controller:v0.40.2
# 下载第三方替代镜像(直接去DockerHub官网搜索即可)
docker pull pollyduan/ingress-nginx-controller:v0.40.2
# 修改镜像名称
docker tag pollyduan/ingress-nginx-controller:v0.40.2 us.gcr.io/k8s-artifacts-prod/ingress-nginx/controller:v0.40.2
  • 重启插件后检查下Ingress是否在运行;
1
bash复制代码kubectl get pods -n kube-system
1
2
3
4
bash复制代码NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx-admission-create-krpgk 0/1 Completed 0 46h
ingress-nginx-admission-patch-wnxlk 0/1 Completed 3 46h
ingress-nginx-controller-558664778f-wwgws 1/1 Running 2 46h
  • 添加配置文件nginx-ingress.yaml用于创建Ingress;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
rules:
- host: nginx-volume.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-service
port:
number: 80
  • 应用配置文件创建Ingress;
1
bash复制代码kubectl apply -f nginx-ingress.yaml
  • 查看所有Ingress,此时我们已经可以通过nginx-volume.com来访问Pod中运行的Nginx服务了;
1
bash复制代码kubectl get ingress
1
2
bash复制代码NAME            CLASS    HOSTS              ADDRESS        PORTS   AGE
nginx-ingress <none> nginx-volume.com 192.168.49.2 80 6s
  • 需要修改下host文件,注意切换到root账号后修改:
1
2
3
4
5
6
bash复制代码# 切换到root用户
su -
# 修改host文件
vi /etc/hosts
# 添加如下记录
192.168.49.2 nginx-volume.com
  • 最后通过CURL命令可以访问Nginx首页信息。
1
bash复制代码curl nginx-volume.com

总结

通过K8S扩展和管理容器化应用确实十分方便,通过几个命令我们就可以实现零停机更新,出了故障也不怕,一个命令实现回滚。但是大量的命令行操作总显得枯燥无味,要是有个可视化工具可以直接管理K8S就更好了。

参考资料

官方文档:kubernetes.io/zh/docs/hom…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…728729730…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%