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

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


  • 首页

  • 归档

  • 搜索

面试:如何面对HR的提问?

发表于 2019-11-24

面试:如何面对HR的提问?

今天就来介绍下HR面试过程中需要注意的地方,大家往往技术面过后,却倒在了HR面上!
毕竟有的公司HR往往有一票否决权,所以大家还是需要认真面对!!!

HR的想法

找工作难,招人也好难。HR想要招什么样的人?
稳定。如果你跳槽频繁,HR可能会担心你干了没一年就跑路了,她又得重新招人。
高性价比。最好是能干活,然后又不贵的。如果你特别想加入一家公司,可以降低一下期望。

履历

Q:请你自我介绍一下自己好吗?
一般人回答这个问题过于平常,只说姓名、年龄、爱好、工作经验,这些在简历上都有。其实,企业最希望知道的是求职者能否胜任工作,包括:最强的技能、最深入研究的知识领域、个性中最积极的部分、做过的最成功的事,主要的成就等,这些都可以和学习无关,也可以和学习有关,但要突出积极的个性和做事的能力,说得合情合理企业才会相信。企业很重视一个人的礼貌,求职者要尊重考官,在回答每个问题之后都说一句“谢谢”,企业喜欢有礼貌的求职者。

Q:你为什么离职?
想加入平台大一点的公司。
工作地点太远。
公司因为业务需要,换城市,非常远。
公司整体上都很稳定,也没有什么发展空间,我希望有一份比较有挑战的工作,毕竟还年轻……就辞职了
公司转型,之前的职位定位和我的发展初衷不一致……就换了新工作。
公司架构调整,我的职位有变化…..过去的经验没有发展空间……我就跳槽了。
我目前在公司已经到顶,短期不会有什么发展空间,公司本身就不大,我希望到更大的平台发展……所以当时选择跳槽。
在互联网时代,我希望能够转入目前发展最快,最有挑战的行业和公司,这样也有利于我的个人快速成长……所以辞职了。
工作没有挑战,我想接触更多新项目,所以很快就跳槽了。
家里有事情,没办法兼顾当时的工作,我只能辞职……
公司倒闭

Q:你上一家公司有多少人?技术团队有多少人?几个后端、前端、产品、测试?
Q:你上一家公司薪酬怎样?经历的这几家公司,薪水都是怎么样的?有没有调薪?
Q:你工作过的这几家公司,是因为什么原因加入?又因为什么原因离职?
一般是因为想要更好的待遇、或者技术已经遇到了瓶颈、想加入平台更大的公司。

Q:你为什么选择这个时间段离职?
已经拿到年终奖了,选择金三银四,这个时间段好找工作。

Q:这几家公司中,你在哪家获得的成长最大?
可以从技术广度、技术深度回答。

Q:你为什么不在毕业时就加入大公司?
当时认为小公司能全面地提升能力,因为小公司都是要通才,一个人干几种活。
而大公司要专才,最好是精通某个领域的技术栈。

技术水平

虽然HR不懂技术,但是会旁敲侧击地推测你的水平。
如果你想忽悠的话,可以多说些技术词汇。。

Q:你在过往技术团队中属于什么水平?
没必要谦虚,肯定要说属于水平比较靠前的。

Q:你做得最不好的地方是什么?

Q:你在项目中做得最不好的功能是什么?
我只能及时完成任务,但是对公司未来的发展战略不太了解。

Q:你现在手上有几个offer?
现在手上有两个offer,但都是创业公司,个人还是想加入比较大的公司。

沟通能力

程序员的沟通能力还是很重要的。经常要和产品、需求、测试讨论。
Q:如果你的上司做事很霸道,你会怎么处理?
以前遇到过一个很老派的上司,他要求用一种几十年前的老技术,然后我就查阅了很多资料,并跟他反复权衡利弊,说明可能带来的弊端。

Q:你一般是怎么和产品、需求沟通的?遇到过哪些问题?
日常周一开例会。线上线下的沟通都有,而且要有文档。
以前遇到过一次赶进度,没有记录文档,然后项目联调的时候,互相甩锅,最后还是我背锅,想想都觉得痛。

Q:你希望与什么样的上级共事?
通过应聘者对上级的“希望”可以判断出应聘者对自我要求的意识,这既上一个陷阱,又是一次机会。
1.最好回避对上级具体的希望,多谈对自己的要求。
2.如“做为刚步入社会的新人,我应该多要求自己尽快熟悉环境、适应环境,而不应该对环境提出什么要求,只要能发挥我的专长就可以了。

Q:你最认同的处世方式/态度是什么?
不卑不亢

Q:你通常如何对待别人的批评?
1.沉默是金,不必说什么,否则情况更糟,不过我会接受建设性的批评。
2.我会等大家冷静下来再讨论。

抗压能力

Q:你愿意接受加班吗?
我愿意。(其实不是很愿意=.=)
如果工作需要我会义不容辞加班,我现在单身,没有任何家庭负担,可以全身心的投入工作。但同时我也会提高工作效率,减少不必要的加班。

Q:如果上司经常对你施压,你会怎么做?
我长期抗压。只要不是侮辱性质的,都没事。

你还有什么要问我的?(技术面最后一问)
在技术面中,面试官面试完,一般都会问一句”你还有什么要问我的?”
虽然是技术面,由于属于开放的问题,所以也放在此处。

Q:如果通过这次面试我们录用了你,但工作一段时间却发现你根本不适合这个职位,你怎么办?
一段时间发现工作不适合我,有两种情况:
1.如果你确实热爱这个职业,那你就要不断学习,虚心向领导和同事学习业务知识和处事经验,了解这个职业的精神内涵和职业要求,力争减少差距;
2.你觉得这个职业可有可无,那还是趁早换个职业,去发现适合你的,你热爱的职业,那样你的发展前途也会大点,对单位和个人都有好处。

Q:公司有多少人?技术团队有多少?后端有几个?
Q:公司项目有没有用到分布式或者微服务?有没有落地
Q:公司的开发流程是怎样的?有没有单元测试,覆盖率如何?是怎么做CI/CD(持续集成/持续部署)的?
Q:公司的作息时间是怎样的?
Q:做开发肯定是要加班的,但我想了解一下加班的强度。

薪酬

Q:你期望薪酬是多少?
可以反问“你们这边可以提供多少呢?”
如果HR说根据能力而定,那就看自己的情况了。
一般情况,可以涨薪30%左右。

其他

Q:谈谈你对跳槽的看法?
1.正常的“跳槽”能促进人才合理流动,应该支持。
2.频繁的跳槽对单位和个人双方都不利,应该反对。

Q:你欣赏哪种性格的人?
诚实、不死板而且容易相处的人、有“实际行动”的人。

Q:你觉得你个性上最大的优点是什么?
沉着冷静、条理清楚、立场坚定、顽强向上、乐于助人和关心他人、适应能力和幽默感、乐观和友爱。我在北大青鸟经过一到两年的培训及项目实战,加上实习工作,使我适合这份工作。

Q:说说你最大的缺点?
这个问题企业问的概率很大,通常不希望听到直接回答的缺点是什么等,如果求职者说自己小心眼、爱忌妒人、非常懒、脾气大、工作效率低,企业肯定不会录用你。绝对不要自作聪明地回答“我最大的缺点是过于追求完美”,有的人以为这样回答会显得自己比较出色,但事实上,他已经岌岌可危了。企业喜欢求职者从自己的优点说起,中间加一些小缺点,最后再把问题转回到优点上,突出优点的部分

Q:今后的职业规划?
Q:对我们公司有什么看法?

推荐

ProcessOn是一个在线作图工具的聚合平台~

文末

欢迎关注个人微信公众号:Coder编程
欢迎关注Coder编程公众号,主要分享数据结构与算法、Java相关知识体系、框架知识及原理、Spring全家桶、微服务项目实战、DevOps实践之路、每日一篇互联网大厂面试或笔试题以及PMP项目管理知识等。更多精彩内容正在路上~

文章收录至
Github: github.com/CoderMerlin…
Gitee: gitee.com/573059382/c…
欢迎关注并star~

微信公众号

本文转载自: 掘金

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

Dubbo源码解析(二十二)远程调用——Protocol 远

发表于 2019-11-24

远程调用——Protocol

目标:介绍远程调用中协议的设计和实现,介绍dubbo-rpc-api中的各种protocol包的源码,是重点内容。

前言

在远程调用中协议是非常重要的一层,看下面这张图:

dubbo-framework

该层是在信息交换层之上,分为了并且夹杂在服务暴露和服务引用中间,为了有一个约定的方式进行调用。

dubbo支持不同协议的扩展,比如http、thrift等等,具体的可以参照官方文档。本文讲解的源码大部分是对于公共方法的实现,而具体的服务暴露和服务引用会在各个协议实现中讲到。

下面是该包下面的类图:

protocol包类图

源码分析

(一)AbstractProtocol

该类是协议的抽象类,实现了Protocol接口,其中实现了一些公共的方法,抽象方法在它的子类AbstractProxyProtocol中定义。

1.属性

1
2
3
4
5
6
7
8
9
10
复制代码/**
* 服务暴露者集合
*/
protected final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<String, Exporter<?>>();

/**
* 服务引用者集合
*/
//TODO SOFEREFENCE
protected final Set<Invoker<?>> invokers = new ConcurrentHashSet<Invoker<?>>();

2.serviceKey

1
2
3
4
5
6
7
8
9
10
复制代码protected static String serviceKey(URL url) {
// 获得绑定的端口号
int port = url.getParameter(Constants.BIND_PORT_KEY, url.getPort());
return serviceKey(port, url.getPath(), url.getParameter(Constants.VERSION_KEY),
url.getParameter(Constants.GROUP_KEY));
}

protected static String serviceKey(int port, String serviceName, String serviceVersion, String serviceGroup) {
return ProtocolUtils.serviceKey(port, serviceName, serviceVersion, serviceGroup);
}

该方法是为了得到服务key group+”/“+serviceName+”:”+serviceVersion+”:”+port

3.destroy

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
复制代码@Override
public void destroy() {
// 遍历服务引用实体
for (Invoker<?> invoker : invokers) {
if (invoker != null) {
// 从集合中移除
invokers.remove(invoker);
try {
if (logger.isInfoEnabled()) {
logger.info("Destroy reference: " + invoker.getUrl());
}
// 销毁
invoker.destroy();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
// 遍历服务暴露者
for (String key : new ArrayList<String>(exporterMap.keySet())) {
// 从集合中移除
Exporter<?> exporter = exporterMap.remove(key);
if (exporter != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Unexport service: " + exporter.getInvoker().getUrl());
}
// 取消暴露
exporter.unexport();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
}

该方法是对invoker和exporter的销毁。

(二)AbstractProxyProtocol

该类继承了AbstractProtocol类,其中利用了代理工厂对AbstractProtocol中的两个集合进行了填充,并且对异常做了处理。

1.属性

1
2
3
4
5
6
7
8
9
复制代码/**
* rpc的异常类集合
*/
private final List<Class<?>> rpcExceptions = new CopyOnWriteArrayList<Class<?>>();

/**
* 代理工厂
*/
private ProxyFactory proxyFactory;

2.export

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
复制代码@Override
@SuppressWarnings("unchecked")
public <T> Exporter<T> export(final Invoker<T> invoker) throws RpcException {
// 获得uri
final String uri = serviceKey(invoker.getUrl());
// 获得服务暴露者
Exporter<T> exporter = (Exporter<T>) exporterMap.get(uri);
if (exporter != null) {
return exporter;
}
// 新建一个线程
final Runnable runnable = doExport(proxyFactory.getProxy(invoker, true), invoker.getInterface(), invoker.getUrl());
exporter = new AbstractExporter<T>(invoker) {
/**
* 取消暴露
*/
@Override
public void unexport() {
super.unexport();
// 移除该key对应的服务暴露者
exporterMap.remove(uri);
if (runnable != null) {
try {
// 启动线程
runnable.run();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
};
// 加入集合
exporterMap.put(uri, exporter);
return exporter;
}

其中分为两个步骤,创建一个exporter,放入到集合汇中。在创建exporter时对unexport方法进行了重写。

3.refer

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
复制代码@Override
public <T> Invoker<T> refer(final Class<T> type, final URL url) throws RpcException {
// 通过代理获得实体域
final Invoker<T> target = proxyFactory.getInvoker(doRefer(type, url), type, url);
Invoker<T> invoker = new AbstractInvoker<T>(type, url) {
@Override
protected Result doInvoke(Invocation invocation) throws Throwable {
try {
// 获得调用结果
Result result = target.invoke(invocation);
Throwable e = result.getException();
// 如果抛出异常,则抛出相应异常
if (e != null) {
for (Class<?> rpcException : rpcExceptions) {
if (rpcException.isAssignableFrom(e.getClass())) {
throw getRpcException(type, url, invocation, e);
}
}
}
return result;
} catch (RpcException e) {
// 抛出未知异常
if (e.getCode() == RpcException.UNKNOWN_EXCEPTION) {
e.setCode(getErrorCode(e.getCause()));
}
throw e;
} catch (Throwable e) {
throw getRpcException(type, url, invocation, e);
}
}
};
// 加入集合
invokers.add(invoker);
return invoker;
}

该方法是服务引用,先从代理工厂中获得Invoker对象target,然后创建了真实的invoker在重写方法中调用代理的方法,最后加入到集合。

1
2
3
复制代码protected abstract <T> Runnable doExport(T impl, Class<T> type, URL url) throws RpcException;

protected abstract <T> T doRefer(Class<T> type, URL url) throws RpcException;

可以看到其中抽象了服务引用和暴露的方法,让各类协议各自实现。

(三)AbstractInvoker

该类是invoker的抽象方法,因为协议被夹在服务引用和服务暴露中间,无论什么协议都有一些通用的Invoker和exporter的方法实现,而该类就是实现了Invoker的公共方法,而把doInvoke抽象出来,让子类只关注这个方法。

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
复制代码/**
* 服务类型
*/
private final Class<T> type;

/**
* url对象
*/
private final URL url;

/**
* 附加值
*/
private final Map<String, String> attachment;

/**
* 是否可用
*/
private volatile boolean available = true;

/**
* 是否销毁
*/
private AtomicBoolean destroyed = new AtomicBoolean(false);

2.convertAttachment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码private static Map<String, String> convertAttachment(URL url, String[] keys) {
if (keys == null || keys.length == 0) {
return null;
}
Map<String, String> attachment = new HashMap<String, String>();
// 遍历key,把值放入附加值集合中
for (String key : keys) {
String value = url.getParameter(key);
if (value != null && value.length() > 0) {
attachment.put(key, value);
}
}
return attachment;
}

该方法是转化为附加值,把url中的值转化为服务调用invoker的附加值。

3.invoke

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
复制代码@Override
public Result invoke(Invocation inv) throws RpcException {
// if invoker is destroyed due to address refresh from registry, let's allow the current invoke to proceed
// 如果服务引用销毁,则打印告警日志,但是通过
if (destroyed.get()) {
logger.warn("Invoker for service " + this + " on consumer " + NetUtils.getLocalHost() + " is destroyed, "
+ ", dubbo version is " + Version.getVersion() + ", this invoker should not be used any longer");
}

RpcInvocation invocation = (RpcInvocation) inv;
// 会话域中加入该调用链
invocation.setInvoker(this);
// 把附加值放入会话域
if (attachment != null && attachment.size() > 0) {
invocation.addAttachmentsIfAbsent(attachment);
}
// 把上下文的附加值放入会话域
Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
/**
* invocation.addAttachmentsIfAbsent(context){@link RpcInvocation#addAttachmentsIfAbsent(Map)}should not be used here,
* because the {@link RpcContext#setAttachment(String, String)} is passed in the Filter when the call is triggered
* by the built-in retry mechanism of the Dubbo. The attachment to update RpcContext will no longer work, which is
* a mistake in most cases (for example, through Filter to RpcContext output traceId and spanId and other information).
*/
invocation.addAttachments(contextAttachments);
}
// 如果开启的是异步调用,则把该设置也放入附加值
if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)) {
invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
}
// 加入编号
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);


try {
// 执行调用链
return doInvoke(invocation);
} catch (InvocationTargetException e) { // biz exception
Throwable te = e.getTargetException();
if (te == null) {
return new RpcResult(e);
} else {
if (te instanceof RpcException) {
((RpcException) te).setCode(RpcException.BIZ_EXCEPTION);
}
return new RpcResult(te);
}
} catch (RpcException e) {
if (e.isBiz()) {
return new RpcResult(e);
} else {
throw e;
}
} catch (Throwable e) {
return new RpcResult(e);
}
}

该方法做了一些公共的操作,比如服务引用销毁的检测,加入附加值,加入调用链实体域到会话域中等。然后执行了doInvoke抽象方法。各协议自己去实现。

(四)AbstractExporter

该类和AbstractInvoker类似,也是在服务暴露中实现了一些公共方法。

1.属性

1
2
3
4
5
6
7
8
9
复制代码/**
* 实体域
*/
private final Invoker<T> invoker;

/**
* 是否取消暴露服务
*/
private volatile boolean unexported = false;

2.unexport

1
2
3
4
5
6
7
8
9
10
11
复制代码@Override
public void unexport() {
// 如果已经消取消暴露,则之间返回
if (unexported) {
return;
}
// 设置为true
unexported = true;
// 销毁该实体域
getInvoker().destroy();
}

(五)InvokerWrapper

该类是Invoker的包装类,其中用到类装饰模式,不过并没有实现实际的功能增强。

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
复制代码public class InvokerWrapper<T> implements Invoker<T> {

/**
* invoker对象
*/
private final Invoker<T> invoker;

private final URL url;

public InvokerWrapper(Invoker<T> invoker, URL url) {
this.invoker = invoker;
this.url = url;
}

@Override
public Class<T> getInterface() {
return invoker.getInterface();
}

@Override
public URL getUrl() {
return url;
}

@Override
public boolean isAvailable() {
return invoker.isAvailable();
}

@Override
public Result invoke(Invocation invocation) throws RpcException {
return invoker.invoke(invocation);
}

@Override
public void destroy() {
invoker.destroy();
}

}

(六)ProtocolFilterWrapper

该类实现了Protocol接口,其中也用到了装饰模式,是对Protocol的装饰,是在服务引用和暴露的方法上加上了过滤器功能。

1.buildInvokerChain

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
复制代码private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
Invoker<T> last = invoker;
// 获得过滤器的所有扩展实现类实例集合
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (!filters.isEmpty()) {
// 从最后一个过滤器开始循环,创建一个带有过滤器链的invoker对象
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
// 记录last的invoker
final Invoker<T> next = last;
// 新建last
last = new Invoker<T>() {

@Override
public Class<T> getInterface() {
return invoker.getInterface();
}

@Override
public URL getUrl() {
return invoker.getUrl();
}

@Override
public boolean isAvailable() {
return invoker.isAvailable();
}

/**
* 关键在这里,调用下一个filter代表的invoker,把每一个过滤器串起来
* @param invocation
* @return
* @throws RpcException
*/
@Override
public Result invoke(Invocation invocation) throws RpcException {
return filter.invoke(next, invocation);
}

@Override
public void destroy() {
invoker.destroy();
}

@Override
public String toString() {
return invoker.toString();
}
};
}
}
return last;
}

该方法就是创建带 Filter 链的 Invoker 对象。倒序的把每一个过滤器串连起来,形成一个invoker。

2.export

1
2
3
4
5
6
7
8
9
复制代码@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
// 如果是注册中心,则直接暴露服务
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
// 服务提供侧暴露服务
return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY, Constants.PROVIDER));
}

该方法是在服务暴露上做了过滤器链的增强,也就是加上了过滤器。

3.refer

1
2
3
4
5
6
7
8
9
复制代码@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 如果是注册中心,则直接引用
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
return protocol.refer(type, url);
}
// 消费者侧引用服务
return buildInvokerChain(protocol.refer(type, url), Constants.REFERENCE_FILTER_KEY, Constants.CONSUMER);
}

该方法是在服务引用上做了过滤器链的增强,也就是加上了过滤器。

(七)ProtocolListenerWrapper

该类也实现了Protocol,也是装饰了Protocol接口,但是它是在服务引用和暴露过程中加上了监听器的功能。

1.export

1
2
3
4
5
6
7
8
9
10
11
复制代码@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
// 如果是注册中心,则暴露该invoker
if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
return protocol.export(invoker);
}
// 创建一个暴露者监听器包装类对象
return new ListenerExporterWrapper<T>(protocol.export(invoker),
Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(ExporterListener.class)
.getActivateExtension(invoker.getUrl(), Constants.EXPORTER_LISTENER_KEY)));
}

该方法是在服务暴露上做了监听器功能的增强,也就是加上了监听器。

2.refer

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 如果是注册中心。则直接引用服务
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
return protocol.refer(type, url);
}
// 创建引用服务监听器包装类对象
return new ListenerInvokerWrapper<T>(protocol.refer(type, url),
Collections.unmodifiableList(
ExtensionLoader.getExtensionLoader(InvokerListener.class)
.getActivateExtension(url, Constants.INVOKER_LISTENER_KEY)));
}

该方法是在服务引用上做了监听器功能的增强,也就是加上了监听器。

后记

该部分相关的源码解析地址:github.com/CrazyHZM/in…

该文章讲解了远程调用中关于协议的部分,其实就是讲了一些公共的方法,并且把关键方法抽象出来让子类实现,具体的方法实现都在各个协议中自己实现。接下来我将开始对rpc模块的代理进行讲解。

本文转载自: 掘金

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

原创 面试官:Java对象一定分配在堆上吗?

发表于 2019-11-23

原创|面试官:Java对象一定分配在堆上吗?

最近在看 Java 虚拟机方面的资料,以备工作中的不时之需。首先我先抛出一个我自己想的面试题,然后再引出后面要介绍的知识点如逃逸分析、标量替换、栈上分配等知识点

面试题

Java 对象一定分配在堆上吗?

自己先思考下,再往下阅读效果更佳哦!

分析

我们都知道 Java 对象一般分配在堆上,而堆空间又是所有线程共享的。了解 NIO 库的朋友应该知道还有一种是堆外内存也叫直接内存。直接内存是直接向操作系统申请的内存区域,访问直接内存的速度一般会优于堆内存。直接内存的大小不直接受 Xmx 设定的值限制,但是在使用的时候也要注意,毕竟系统内存有限,堆内存和直接内存的总和依然还是会受操作系统的内存限制的。

通过上面的分析,大家也知道了,Java 对象除了可以分配在堆上,还可以直接分配在堆外内存中。但这点不是我今天想讨论的,我想和大家聊聊栈上分配,说到栈上分配就不得不先说下逃逸分析

逃逸分析

逃逸分析是是一种动态确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。

换句话说,逃逸分析的目的是判断对象的作用域是否有可能逃出方法体

判断依据有两个

  1. 对象是否被存入堆中(静态字段或堆中对象的实例字段)
  2. 对象是否被传入未知代码中(方法的调用者和参数)

我们来分析下这两个依据

对于第一点对象是否被存入堆中,我们知道堆内存是线程共享的,一旦对象被分配在堆中,那所有线程都可以访问到该对象,这样即时编译器就追踪不到所有使用到该对象的地方了,这样的对象就属于逃逸对象,如下所示

1
2
3
4
5
6
复制代码public class Escape {
private static User u;
public static void alloc() {
u = new User(1, "baiya");
}
}

User 对象属于类 Escape 的成员变量,该对象是可能被所有线程访问的,所以会发生逃逸

第二点是对象是否被传入未知代码中,Java 的即时编译器是以方法为单位进行编译,即时编译器会把方法中未被内联的方法当成未知代码,所以无法判断这个未知方法的方法调用会不会将调用者或参数放到堆中,所以认为方法的调用者和参数是逃逸的,如下所示

1
2
3
4
5
6
复制代码public class Escape {
private static User u;
public static void alloc(User user) {
u = user;
}
}

方法 alloc 的参数 user 被赋值给类 Escape 的成员变量 u,所以也会被所有线程访问,也是会发生逃逸的。

栈上分配

栈上分配是 Java 虚拟机提供的一种优化技术,该技术的基本思想是可以将线程私有的对象打散,分配到栈上,而非堆上。那分配到栈上有什么好处呢?
我们知道栈中的变量会在方法调用结束后自动销毁,所以省掉了 jvm 进行垃圾回收,进而可以提高系统的性能

栈上分配是要基于逃逸分析和标量替换实现的

我们通过一个具体的例子来验证下非逃逸分析的对象确实是分配到了栈上

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class OnStack {
public static void alloc() {
User user = new User(1, "baiya");
}
public static void main(String[] args) {
long start = Instant.now().toEpochMilli();
for (int i = 0; i < 100_000_000; i++) {
alloc();
}
long end = Instant.now().toEpochMilli();
System.out.println("耗时:" + (end - start));
}
}

上面的代码是循环 1 亿次执行 alloc 方法创建 User 对象,每个 User 对象占用约 16 bytes(怎么计算的下面会说) 空间,创建 1 亿次,所以如果 User 都是在堆上分配的话则需要 1.5G 的内存空间。如果我们设置堆空间小于这个数,应该会发生 gc,如果设置的特别小,应该会发生大量的 gc。

我们用下面的参数执行上述代码

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations

其中 -server 是开启 server 模式,逃逸分析需要 server 模式的支持

-Xmx10 -Xms10m,设置堆内存是 10m,远小于 1.5G

-XX:+DoEscapeAnalysis 开启逃逸分析

-XX:+PrintGCDetails 如果发生 gc,打印 gc 日志

-XX:+EliminateAllocations 开启标量替换,允许把对象打散分配在栈上,比如 User 对象,它有两个属性 id 和 name,可以把他们看成独立的局部变量分别进行分配

配置好 jvm 参数后,执行代码,查看结果可知执行了 3 次 gc,耗时 10 毫秒,可以推断出 User 对象并未全部分配到堆上,而是把绝大多数分配到了栈上,分配在栈上的好处是方法结束后自动释放对应的内存,是一种优化手段。

栈上分配

我们上面说了栈上分配依赖逃逸分析和标量替换,那么我们可以破坏其中任意一个条件,去掉逃逸分析就可以通过 -XX:-DoEscapteAnalysis 或者关闭标量替换 -XX:-EliminateAllocations 再去执行上述代码,观察执行情况,发现发生了大量的 gc,并且耗时 3182 毫秒,执行时间远远高于上面的 10 毫秒,所以可以推测出并未执行栈上分配的优化手段

堆上分配

计算 User 对象占用空间大小

对象由四部分构成

  1. 对象头:记录一个对象的实例名字、ID和实例状态。

普通对象占用 8 bytes,数组占用 12 bytes (8 bytes 的普通对象头 + 4 bytes 的数组长度)
2. 基本类型

boolean,byte 占用 1 byte

char,short 占用 2 bytes

int,float 占用 4 bytes

long,double 占用 8 bytes
3. 引用类型:每个引用类型占用 4 bytes
4. 填充物:以 8 的倍数计算,不足 8 的倍数会自动补齐

我们上面的 User 对象有两个属性,一个 int 类型的 id 占用 4 bytes,一个引用类型的 name 占用 4bytes,在加上 8 bytes 的对象头,正好是 16 bytes

总结

关于虚拟机的知识点还有很多而且也比较重要,如果懂对写优质代码、优化性能、排查问题等都是锦上添花,比如逃逸分析,即时编译器会根据逃逸分析的结果进行优化,如所消除以及标量替换。感兴趣的朋友可以自己查查资料学习下。通过这个栈上分配的例子,以后我们写代码时,把可以不逃逸的对象写进方法体中,这样就会被编译器优化,提升性能。而且也知道了上面面试题的答案,就是 Java 中的对象并一定分配在堆上,也可能分配在栈上

参考资料

  1. 《实战Java虚拟机》
  2. 《深入理解Java虚拟机》
  3. zh.wikipedia.org/wiki/逃逸分析

欢迎关注公众号 【每天晒白牙】,获取最新文章,我们一起交流,共同进步!

本文转载自: 掘金

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

学会这几道链表算法题,面试再也不怕手写链表了 学会这几道链表

发表于 2019-11-23

学会这几道链表算法题,面试再也不怕手写链表了

笔者文笔功力尚浅,如有不妥,请慷慨指出,必定感激不尽

在面试的时候经常被问到让手写关于链表的代码,下面几个都是我在面试中被问到过的问题。当然我写的不一定是最优解,如果有更好的解决办法欢迎大家指出。

便于大家观看,我先将题目列出

  • 删除链表中倒数第N个节点
  • 链表反转
  • 合并两个有序链表
  • 求链表的中间节点

大家一定要自己在电脑手敲一遍,最好是在纸上自己手写一遍

删除链表中倒数第N个节点

题目:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

1
2
3
复制代码1给定一个链表: 1->2->3->4->5, 和 n = 2.  
2
3当删除了倒数第二个节点后,链表变为 1->2->3->5.

在链表的题目中,有时候一个指针解决不了的问题那么我们就再加一个指针。一般来说两个指针就能解决大部分的问题

上面是我们定义的链表,例如我们想要删除倒数第N个节点,那么就定义两个指针,一个快指针,一个慢指针。快指针比慢指针快N步,然后快慢指针一起向前移动,那么正好快指针走到Null的时候慢指针所指向的就是我们要删除的节点。

举个例子例如我们想要删除倒数第二个节点,那么初始化的指针指针指向如下。

遍历完以后的指针如下,我们就可以看到左边的指针指向的就是我们想要删除的节点

部分代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码 1public void deleteNodeForN(Node head, int n){  
2
3    Node left = head;
4    Node right = head;
5    int i = 0;
6    while (right!=null && i < n){
7        right = right.getNext();
8        i++;
9    }
10
11    while (right!=null){
12        left = left.getNext();
13        right = right.getNext();
14    }
15    // 遍历完后删除左节点
16    deleteNode(left);
17
18}

链表反转

题目:反转一个单链表。

1
2
复制代码1输入: 0->1->2->3->4->5->NULL  
2输出: 5->4->3->2->1->0->NULL

链表中没有什么问题是通过加指针解决不了的,如果有,那么就再加一个指针。

解法一:加指针

在上面链表删除第N个节点中我们加了两个指针解决了问题,那么接下来如果要反转一个链表该怎么做呢?两个指针已经不够用了,我们需要三个指针用来定义当前节点、当前节点的前节点、当前节点的后节点。当然这种方式是既不占用空间,时间也快的一种解法。

还是我们定义的一个链表,那么我们想要的指针效果是什么样呢?接下来我们用图示一步一步演示怎么用三个指针将链表翻转过来,大家不用看我最后给出的解法答案,可以自己试着看着我的图自己写一遍代码,看能不能写出来。

部分代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码 1public Node reversalNodeThree(Node head) {  
2    if (head == null || head.getNext() == null){
3        return head;
4    }
5
6    Node preNode = null;
7    Node nextNode = null;
8    while (head != null){
9        nextNode = head.getNext();
10        head.setNext(preNode);
11        preNode = head;
12        head = nextNode;
13    }
14    return preNode;
15}

解法二:递归

递归代码的关键是如何将大问题分解为小问题的规律,并且基于此写出递归公式,然后再推敲终止条件。

在写递归代码的时候我们的思路千万不要一步一步往里套,套着套着自己就会容易蒙了。其实递归的本质就是分解小任务执行,而我们正确的思维方式屏蔽掉递归的细节,假设后面的已经我们想要的结果,然后只想第一步即可。

我们就以反转链表为例子,怎么用递归的思想来思考,又怎样把我们的思考变成代码。

这里0号节点的下一节点不是5号节点,而是我们灰色背景下的大节点

还是上面的链表为例,我们要反转,假设第一个节点随后的所有节点已经反转成功了,那么接下来我们怎么做呢?相信这里大家都会了吧,相当于两个节点的转换。

1
2
复制代码1Node(0).getNext().setNext(Node(0));  
2Node(0).setNext(null);
  • 终止条件:当所传Node是null或者所传的Node.next是null。表明传进来的是空节点或者就一个节点,无需反转

我们利用上面的终止条件以及分析出来的代码就可以写出如下的递归反转一个链条的代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码 1public Node reversalNodeTwo(Node head){  
2
3    if (head == null || head.getNext() == null){
4        return head;
5    }
6
7    Node reHead = reversalNodeTwo(head.getNext());
8
9    head.getNext().setNext(head);
10
11    head.setNext(null);
12
13    return reHead;
14
15}

合并两个有序链表

题目:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

1
2
复制代码1输入:1->2->4, 1->3->4  
2输出:1->1->2->3->4->4

迭代

迭代的方式将两个链表合并是通过指针的方式来解决问题的。加入我们现在有下面两个链表。我们会定义两个指针分别指向两个链表的表头,来开始进行一一比较,较小的一方将节点移出来,并将指针向后移动。直至为null。接下来我们用图片分解每一步。大家可以根据图片的提示自己先编码练习一下,后面附有答案。

部分代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码 1public Node mergeTwoListTwo(Node nodeOne, Node nodeTwo){  
2
3    AboutLinkedList mergeTwoList = new AboutLinkedList();
4    Node headNodeOne = nodeOne;
5    Node headNodeTwo = nodeTwo;
6
7    while (headNodeOne!=null || headNodeTwo!=null){
8
9        if (headNodeOne == null || headNodeOne.getNum() > headNodeTwo.getNum()){
10            mergeTwoList.addNode(headNodeTwo);
11            Node pre = headNodeTwo;
12            headNodeTwo = headNodeTwo.getNext();
13            pre.setNext(null);
14        }else {
15            mergeTwoList.addNode(headNodeOne);
16            Node pre = headNodeOne;
17            headNodeOne = headNodeOne.getNext();
18            pre.setNext(null);
19        }
20    }
21    return mergeTwoList.head.getNext();
22}

求链表的中间节点

题目:求链表的中间节点

1
2
复制代码1输入:0->1->2->3->4  
2输出:2

指针

一般来说链表的题我们可以用指针的话无论是时间还是空间都是最优的解决办法,其实这里有个小技巧,就是定义两个指针(快慢指针),快指针每次走两个节点,慢指针每次走一个节点。这样当快指针走到最后的时候慢指针正好走到了中间位置。接下来我们用图更直观的感受一下指针是如何走的。大家可以按照图中的演示自己写一下代码,然后再看我最后给出的代码。这样会记忆更深刻

部分代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码 1public Node getNodeForCenter(Node head){  
2    if (head == null){
3        return null;
4    }else if (head.getNext() == null){
5        return head;
6    }
7    Node slow = head;
8    Node fast = head;
9    while (fast!=null && fast.getNext()!=null){
10        slow = slow.getNext();
11        fast = fast.getNext().getNext();
12    }
13    return slow;
14}

最后提醒,大家一定要自己在电脑手敲一遍,最好是在纸上自己手写一遍

代码地址

本文转载自: 掘金

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

记录go-python微服务实践,希望能帮助需要的人

发表于 2019-11-22

介绍

本文讲述如何使用 grpc,由 go 作为客户端,python 作为服务端进行通信。
(题外:一直迷惑于怎样让他们两个连起来,后来才发现只要对同一个proto文件进行编译就好了。。。😓)

实现功能

python 实现方法 f(name) ,返回 “hello “+name,由 go 调用得到返回值

安装配置

Go

  • 个人配置是 go 1.12 ,使用 go mod 项目管理
  • 因为有些包会被墙,所有要配置GOPROXY,我配置的是阿里的GOPROXY="https://mirrors.aliyun.com/goproxy/"

安装 grpc,protobuf编译器和对应的 go 插件

1
2
3
复制代码go get google.golang.org/grpc
go get github.com/golang/protobuf/proto
go get github.com/golang/protobuf/proto-gen-go

注:如果在 goland 编译器里使用命令行也需要配置代理

python3

同样也是安装 grpc,protobuf等

1
2
3
复制代码 pip3 install grpcio
pip3 install protobuf
pip3 install grpcio-tools

开始

我使用的是 goland 编译器,然后引入了 python 解释器

在红框内选择自己解释器就好

项目结构

本人初尝,可能不规范,敬请指正

1
2
3
4
5
6
7
复制代码-project
-go
-main.go
-micro
-hello.proto
-python
-server.py

这是所需要自己创建的目录和文件,go 包内即 go代码,micro是微服务配置文件,python包是python代码

micro包

首先创建proto文件–hello.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码syntax = "proto3"; //选择版本

package micro; // 包

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string msg = 1;
}

注:这里package 要与当前路径一致

并没有很详细解释 proto 语法,大家又需要可以自行查看

编译 proto文件
首先命令行移动到 micro 包下,然后分别执行 go 和 python 的编译语句

go:hello.proto即为需要编译的文件,编译后会在当前包下生成 hello.pb.go文件

1
复制代码protoc --go_out=plugins=grpc:. hello.proto

python:编译后会生成hello_pb2.py和hello_pb2_grpc.py两个文件

1
复制代码python3 -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. hello.proto

文件都生成成功的话就可以编写客户端和服务端代码了

python服务端

在 python 包下 server.py 文件内编写如下代码

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
复制代码from concurrent import futures
import time
import grpc
from micro import hello_pb2
from micro import hello_pb2_grpc


class Greeter(hello_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
return hello_pb2.HelloReply(msg = "hello "+request.name)

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers = 10))
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(),server)
server.add_insecure_port('[::]:50051')
print("服务启动")
server.start()
try:
while True:
time.sleep(60*60*24)
except KeyboardInterrupt:
server.stop(0)

if __name__=='__main__':
serve()

创建了端口号为50051的服务
可以尝试启动一下服务

go 客户端

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

import (
"context"
pb "cymdemo/micro"
"fmt"
"google.golang.org/grpc"
)

const address = "localhost:50051"

func main() {
conn,err := grpc.Dial(address,grpc.WithInsecure())
if err != nil {
fmt.Println(err)
}
defer conn.Close()

c := pb.NewGreeterClient(conn)
name := "world"
res,err := c.SayHello(context.Background(),&pb.HelloRequest{Name:name})

if err != nil{
fmt.Println(err)
}
fmt.Println(res.Msg)
}

先启动 python 服务端,然后启动 go 客户端就会拿到调用结果

1
复制代码hello world

本文转载自: 掘金

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

Spring Boot2 系列教程(二十四)Spring B

发表于 2019-11-21

Spring Boot 中的数据持久化方案前面给大伙介绍了两种了,一个是 JdbcTemplate,还有一个 MyBatis,JdbcTemplate 配置简单,使用也简单,但是功能也非常有限,MyBatis 则比较灵活,功能也很强大,据我所知,公司采用 MyBatis 做数据持久化的相当多,但是 MyBatis 并不是唯一的解决方案,除了 MyBatis 之外,还有另外一个东西,那就是 Jpa,松哥也有一些朋友在公司里使用 Jpa 来做数据持久化,本文就和大伙来说说 Jpa 如何实现数据持久化。

Jpa 介绍

首先需要向大伙介绍一下 Jpa,Jpa(Java Persistence API)Java 持久化 API,它是一套 ORM 规范,而不是具体的实现,Jpa 的江湖地位类似于 JDBC,只提供规范,所有的数据库厂商提供实现(即具体的数据库驱动),Java 领域,小伙伴们熟知的 ORM 框架可能主要是 Hibernate,实际上,除了 Hibernate 之外,还有很多其他的 ORM 框架,例如:

  • Batoo JPA
  • DataNucleus (formerly JPOX)
  • EclipseLink (formerly Oracle TopLink)
  • IBM, for WebSphere Application Server
  • JBoss with Hibernate
  • Kundera
  • ObjectDB
  • OpenJPA
  • OrientDB from Orient Technologies
  • Versant Corporation JPA (not relational, object database)

Hibernate 只是 ORM 框架的一种,上面列出来的 ORM 框架都是支持 JPA2.0 规范的 ORM 框架。既然它是一个规范,不是具体的实现,那么必然就不能直接使用(类似于 JDBC 不能直接使用,必须要加了驱动才能用),我们使用的是具体的实现,在这里我们采用的实现实际上还是 Hibernate。

Spring Boot 中使用的 Jpa 实际上是 Spring Data Jpa,Spring Data 是 Spring 家族的一个子项目,用于简化 SQL、NoSQL 的访问,在 Spring Data 中,只要你的方法名称符合规范,它就知道你想干嘛,不需要自己再去写 SQL。

关于 Spring Data Jpa 的具体情况,大家可以参考一文读懂 Spring Data Jpa

工程创建

创建 Spring Boot 工程,添加 Web、Jpa 以及 MySQL 驱动依赖,如下:

工程创建好之后,添加 Druid 依赖,完整的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.28</version>
<scope>runtime</scope>
</dependency>

如此,工程就算创建成功了。

基本配置

工程创建完成后,只需要在 application.properties 中进行数据库基本信息配置以及 Jpa 基本配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码# 数据库的基本配置
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=UTF-8
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# JPA配置
spring.jpa.database=mysql
# 在控制台打印SQL
spring.jpa.show-sql=true
# 数据库平台
spring.jpa.database-platform=mysql
# 每次启动项目时,数据库初始化策略
spring.jpa.hibernate.ddl-auto=update
# 指定默认的存储引擎为InnoDB
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect

注意这里和 JdbcTemplate 以及 MyBatis 比起来,多了 Jpa 配置,Jpa 配置含义我都注释在代码中了,这里不再赘述,需要强调的是,最后一行配置,默认情况下,自动创建表的时候会使用 MyISAM 做表的引擎,如果配置了数据库方言为 MySQL57Dialect,则使用 InnoDB 做表的引擎。

好了,配置完成后,我们的 Jpa 差不多就可以开始用了。

基本用法

ORM(Object Relational Mapping) 框架表示对象关系映射,使用 ORM 框架我们不必再去创建表,框架会自动根据当前项目中的实体类创建相应的数据表。因此,我这里首先创建一个 User 对象,如下:

1
2
3
4
5
6
7
8
9
10
复制代码@Entity(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String username;
private String address;
//省略getter/setter
}

首先 @Entity 注解表示这是一个实体类,那么在项目启动时会自动针对该类生成一张表,默认的表名为类名,@Entity 注解的 name 属性表示自定义生成的表名。@Id 注解表示这个字段是一个 id,@GeneratedValue 注解表示主键的自增长策略,对于类中的其他属性,默认都会根据属性名在表中生成相应的字段,字段名和属性名相同,如果开发者想要对字段进行定制,可以使用 @Column 注解,去配置字段的名称,长度,是否为空等等。

做完这一切之后,启动 Spring Boot 项目,就会发现数据库中多了一个名为 t_user 的表了。

针对该表的操作,则需要我们提供一个 Repository,如下:

1
2
3
4
5
复制代码public interface UserDao extends JpaRepository<User,Integer> {
List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);
@Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
User maxIdUser();
}

这里,自定义 UserDao 接口继承自 JpaRepository,JpaRepository 提供了一些基本的数据操作方法,例如保存,更新,删除,分页查询等,开发者也可以在接口中自己声明相关的方法,只需要方法名称符合规范即可,在 Spring Data 中,只要按照既定的规范命名方法,Spring Data Jpa 就知道你想干嘛,这样就不用写 SQL 了,那么规范是什么呢?参考下图:

当然,这种方法命名主要是针对查询,但是一些特殊需求,可能并不能通过这种方式解决,例如想要查询 id 最大的用户,这时就需要开发者自定义查询 SQL 了。

如上代码所示,自定义查询 SQL,使用 @Query 注解,在注解中写自己的 SQL,默认使用的查询语言不是 SQL,而是 JPQL,这是一种数据库平台无关的面向对象的查询语言,有点定位类似于 Hibernate 中的 HQL,在 @Query 注解中设置 nativeQuery 属性为 true 则表示使用原生查询,即大伙所熟悉的 SQL。上面代码中的只是一个很简单的例子,还有其他一些点,例如如果这个方法中的 SQL 涉及到数据操作,则需要使用 @Modifying 注解。

好了,定义完 Dao 之后,接下来就可以将 UserDao 注入到 Controller 中进行测试了(这里为了省事,就没有提供 Service 了,直接将 UserDao 注入到 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
复制代码@RestController
public class UserController {
@Autowired
UserDao userDao;
@PostMapping("/")
public void addUser() {
User user = new User();
user.setId(1);
user.setUsername("张三");
user.setAddress("深圳");
userDao.save(user);
}
@DeleteMapping("/")
public void deleteById() {
userDao.deleteById(1);
}
@PutMapping("/")
public void updateUser() {
User user = userDao.getOne(1);
user.setUsername("李四");
userDao.flush();
}
@GetMapping("/test1")
public void test1() {
List<User> all = userDao.findAll();
System.out.println(all);
}
@GetMapping("/test2")
public void test2() {
List<User> list = userDao.getUserByAddressEqualsAndIdLessThanEqual("广州", 2);
System.out.println(list);
}
@GetMapping("/test3")
public void test3() {
User user = userDao.maxIdUser();
System.out.println(user);
}
}

如此之后,即可查询到需要的数据。

好了,本文的重点是 Spring Boot 和 Jpa 的整合,这个话题就先说到这里。

多说两句

在和 Spring 框架整合时,如果用到 ORM 框架,大部分人可能都是首选 Hibernate,实际上,在和 Spring+SpringMVC 整合时,也可以选择 Spring Data Jpa 做数据持久化方案,用法和本文所述基本是一样的,Spring Boot 只是将 Spring Data Jpa 的配置简化了,因此,很多初学者对 Spring Data Jpa 觉得很神奇,但是又觉得无从下手,其实,此时可以回到 Spring 框架,先去学习 Jpa,再去学习 Spring Data Jpa,这是给初学者的一点建议。

相关案例已经上传到 GitHub,欢迎小伙伴们们下载:github.com/lenve/javab…

扫码关注松哥,公众号后台回复 2TB,获取松哥独家 超2TB 学习资源

本文转载自: 掘金

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

4 彤哥说netty系列之Java NIO实现群聊(自己跟

发表于 2019-11-20

nio

你好,我是彤哥,本篇是netty系列的第四篇。

欢迎来我的公从号彤哥读源码系统地学习源码&架构的知识。

简介

上一章我们一起学习了Java中的BIO/NIO/AIO的故事,本章将带着大家一起使用纯纯的NIO实现一个越聊越上瘾的“群聊系统”。

业务逻辑分析

首先,我们先来分析一下群聊的功能点:

(1)加入群聊,并通知其他人;

(2)发言,并通知其他人;

(3)退出群聊,并通知其他人;

一个简单的群聊系统差不多这三个功能足够了,为了方便记录用户信息,当用户加入群聊的时候自动给他分配一个用户ID。

业务实现

上代码:

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
复制代码// 这是一个内部类
private static class ChatHolder {
// 我们只用了一个线程,用普通的HashMap也可以
static final Map<SocketChannel, String> USER_MAP = new ConcurrentHashMap<>();

/**
* 加入群聊
* @param socketChannel
*/
static void join(SocketChannel socketChannel) {
// 有人加入就给他分配一个id,本文来源于公从号“彤哥读源码”
String userId = "用户"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
send(socketChannel, "您的id为:" + userId + "\n\r");

for (SocketChannel channel : USER_MAP.keySet()) {
send(channel, userId + " 加入了群聊" + "\n\r");
}

// 将当前用户加入到map中
USER_MAP.put(socketChannel, userId);
}

/**
* 退出群聊
* @param socketChannel
*/
static void quit(SocketChannel socketChannel) {
String userId = USER_MAP.get(socketChannel);
send(socketChannel, "您退出了群聊" + "\n\r");
USER_MAP.remove(socketChannel);

for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + " 退出了群聊" + "\n\r");
}
}
}

/**
* 扩散说话的内容
* @param socketChannel
* @param content
*/
public static void propagate(SocketChannel socketChannel, String content) {
String userId = USER_MAP.get(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + ": " + content + "\n\r");
}
}
}

/**
* 发送消息
* @param socketChannel
* @param msg
*/
static void send(SocketChannel socketChannel, String msg) {
try {
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put(msg.getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}

服务端代码

服务端代码直接使用上一章NIO的实现,只不过这里要把上面实现的业务逻辑适时地插入到相应的事件中。

(1)accept事件,即连接建立的时候,说明加入了群聊;

(2)read事件,即读取数据的时候,说明有人说话了;

(3)连接断开的时候,说明退出了群聊;

OK,直接上代码,为了与上一章的代码作区分,彤哥特意加入了一些标记:

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
复制代码public class ChatServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 将accept事件绑定到selector上
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
// 阻塞在select上
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历selectKeys
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 如果是accept事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = ssc.accept();
System.out.println("accept new conn: " + socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
// 加入群聊,本文来源于公从号“彤哥读源码”
ChatHolder.join(socketChannel);
} else if (selectionKey.isReadable()) {
// 如果是读取事件
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据读入到buffer中
int length = socketChannel.read(buffer);
if (length > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
// 将数据读入到byte数组中
buffer.get(bytes);

// 换行符会跟着消息一起传过来
String content = new String(bytes, "UTF-8").replace("\r\n", "");
if (content.equalsIgnoreCase("quit")) {
// 退出群聊,本文来源于公从号“彤哥读源码”
ChatHolder.quit(socketChannel);
selectionKey.cancel();
socketChannel.close();
} else {
// 扩散,本文来源于公从号“彤哥读源码”
ChatHolder.propagate(socketChannel, content);
}
}
}
iterator.remove();
}
}
}
}

测试

打开四个XSHELL客户端,分别连接telnet 127.0.0.1 8080,然后就可以开始群聊了。

nio

彤哥发现,自己跟自己聊天也是会上瘾的,完全停不下来,不行了,我再去自聊一会儿^^

总结

本文彤哥跟着大家一起实现了“群聊系统”,去掉注释也就100行左右的代码,是不是非常简单?这就是NIO网络编程的魅力,我发现写网络编程也上瘾了^^

问题

这两章我们都没有用NIO实现客户端,你知道怎么实现吗?

提示:服务端需要监听accept事件,所以需要有一个ServerSocketChannel,而客户端是直接去连服务器了,所以直接用SocketChannel就可以了,一个SocketChannel就相当于一个Connection。

最后,也欢迎来我的公从号彤哥读源码系统地学习源码&架构的知识。

code

本文转载自: 掘金

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

MySQL 在 Windows 下安装教程、避坑指南

发表于 2019-11-19

MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,2008 年被 SUN 公司收购,后 SUN 公司又被 Oracle 公司收购。

一、下载

MySQL 官网 www.mysql.com/

点击 DOWNLOADS 进入下载地址,会看到几个不同的版本:

  • MySQL Enterprise Edition:企业版(收费)
  • MySQL Cluster CGE:高级集群版(收费)
  • MySQL Community Edition:社区版(开源免费,但官方不提供技术支持)

通常我们用的都是社区版。点击进入社区版,看到一大堆东西,有点愣住了,不用急,其实点第一个 MySQL Community Server 的下载就可以了。

所以真正的下载地址其实是:dev.mysql.com/downloads/m…

拉到下面,选择 Windows 系统。

这里提供安装版和解压版,安装版是 32 位的(当然 64 位系统下也能安装),解压版是 64 位的。

点击 Download 后会跳转到如下页面,这是叫你注册/登录的,不理它,点击左下角的 No thanks, just start my download. 开始下载。

安装版是 32 位的,而现在的机器多半是 64 位机,虽然 32 位的程序也可以安装,但是并不建议。安装版的安装也比较容易,所以这里只讲解压版的安装。

二、解压版配置

1、配置环境变量

将安装包解压到你要安装的目录,将 bin 目录添加至环境变量。

添加环境变量

2、配置 my.ini

在根目录下新建一个 my.ini 文件。

my.ini

在 my.ini 中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码[mysqld]
; 设置3306端口
port=3306
; 设置mysql的安装目录
basedir=C:\\gl\\SQL\\mysql-8.0.18-winx64
; 设置mysql数据库的数据的存放目录
datadir=C:\\gl\\SQL\\mysql-data
; 允许最大连接数
max_connections=200
; 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统
max_connect_errors=10
; 服务端使用的字符集默认为UTF8
character-set-server=utf8
; 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
; 默认使用“mysql_native_password”插件认证
default_authentication_plugin=mysql_native_password
[mysql]
; 设置mysql客户端默认字符集
default-character-set=utf8
[client]
; 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8

注意:basedir 和 datadir 要改成你自己的目录。

陷阱:

default_authentication_plugin=mysql_native_password 这一句必须要加上,否则可能导致 root 的初始密码无法登陆。

3、初始化数据库

以管理员身份 运行 cmd,切换至安装目录的 bin 目录下,输入如下命令:

1
复制代码mysqld --initialize --console

默认的服务名就是 mysql,也可以指定服务名

1
复制代码mysqld --initialize --console 服务名

一般是不会去指定服务名的,但是如果你的电脑上需要安装多个 MySQL 服务,就可以用不同的名字区分。

执行成功后,会显示 root 的初始密码,如下图,这个密码需要保存下来。

root 密码

如果命令中不加 --console,则在 cmd 窗口将不显示日志信息。可以到 data 目录(my.ini 中 datadir 配置的目录)下找一个 .err 的文件,也可以查看日志信息。

陷阱 1

可能会报“找不到 MSVCP140.dll”

找不到 MSVCP140.dll

MSVCP140.dll 是 Visual Studio C++ 2015 Redistributable 的组成文件。

一般出现这个问题,是因为没有安装 Visual C++ Redistributable for Visual Studio 2015 所致。这个必须安装,否则后面服务无法启动。
下载地址:www.microsoft.com/zh-CN/downl…

如果已安装,则可以修复一下。

亦可下载一个 MSVCP140.dll,复制到 C:\Windows\System32,运行如下批处理命令注册 dll

1
2
3
4
5
复制代码@echo 开始注册
copy msvcp140.dll %windir%\system32\
regsvr32 %windir%\system32\msvcp140.dll /s
@echo msvcp140.dll注册成功
@pause

注册成功之后再运行上述 MySQL 命令,就可以正常初始化数据库了。当然不建议这么做。


陷阱 2

执行完成之后,仔细查看输出的信息,可能会有如下警告:

1
复制代码'utf8' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.

utf 8 目前是字符集 UTF8MB3 的别名,在将来的版本中将被 UTF8MB4 替换。请考虑使用 UTF8MB4,以便明确无误。

如果出现的话,我们只需将 my.ini 文件中的 utf8 替换成 UTF8MB4。

3.2、安装服务

安装服务:

1
复制代码mysqld -install

启动服务:

1
复制代码net start mysql

如果上一步中你指定了另外的服务名,将 mysql 改为你指定的服务名。

登录数据库:

1
复制代码mysql -u root -p

这时提示需要输入密码,就是前文让你保存的密码。

登录成功后显示如下:

修改密码:
执行以下语句,即可将密码改为 root。

1
复制代码ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';

本文转载自: 掘金

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

容错,高可用,灾备的区别

发表于 2019-11-18

标题里面的三个术语,很容易混淆,专业人员有时也会用错。

本文就用图片解释它们有何区别。

容错

容错(fault tolerance)指的是, 发生故障时,系统还能继续运行。

飞机有四个引擎,如果一个引擎坏了,剩下三个引擎,还能继续飞,这就是”容错”。同样的,汽车的一个轮子扎破了,剩下三个轮子,也还是勉强能行驶。

容错的目的是,发生故障时,系统的运行水平可能有所下降,但是依然可用,不会完全失败。

高可用

高可用(high availability)指的是, 系统能够比正常时间更久地保持一定的运行水平。

汽车的备胎就是一个高可用的例子。如果没有备胎,轮胎坏了,车就开不久了。备胎延长了汽车行驶的可用时间。

注意,高可用不是指系统不中断(那是容错能力),而是指一旦中断能够快速恢复,即中断必须是短暂的。如果需要很长时间才能恢复可用性,就不叫高可用了。上面例子中,更换备胎就必须停车,但只要装上去,就能回到行驶状态。

灾备

灾备(又称灾难恢复,disaster recovery)指的是, 发生灾难时恢复业务的能力。

上图中,飞机是你的 IT 基础设施,飞行员是你的业务,飞行员弹射装置就是灾备措施。一旦飞机即将坠毁,你的基础设施就要没了,灾备可以让你的业务幸存下来。

灾备的目的就是,保存系统的核心部分。一个好的灾备方案,就是从失败的基础设施中获取企业最宝贵的数据,然后在新的基础设施上恢复它们。注意,灾备不是为了挽救基础设置,而是为了挽救业务。

总结

上面三个方面可以结合起来,设计一个可靠的系统。

  • 容错:发生故障时,如何让系统继续运行。
  • 高可用:系统中断时,如何尽快恢复。
  • 灾备:系统毁灭时,如何抢救数据。

参考文献

  • The Difference Between Fault Tolerance, High Availability, & Disaster Recovery , Patrick Benson

(完)

本文转载自: 掘金

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

《我们一起进大厂》系列-秒杀系统设计

发表于 2019-11-18

你知道的越多,你不知道的越多

点赞再看,养成习惯

GitHub上已经开源 github.com/JavaFamily 有一线大厂面试点脑图和个人联系方式,欢迎Star和指教

絮叨

之前写了很多Redis相关的知识点,我又大概回头看了下,除了比较底层的东西没写很深之外,我基本上的点都提到过了,我相信如果只是为了应付面试应该是够了的,但是如果你想把它们真正的吸收纳为己用,还是需要大量的知识积累,和很多实际操作的。

就我自己而言Redis在开发过程中实在用得太普遍了,热点数据的存储啊,整体性能的提升啊都会用到,但是就像我说的技术就是一把双刃剑,使用它们随之而来的问题也会很多的,我在老东家双十二就遇到缓存雪崩问题让整体服务宕机3分钟,相必大家都知道阿里今年的双十一数据了,那三分钟在这种时候到底值多少钱?真的不敢想象。

Redis的普遍我就拿掘金我自己的认知举例,不知道对不对,但是目测是对的。

大家看到问题所在了么?是的热门的赞的数据不是最新的,我盲猜一波上面的热门文章是缓存。失效时间应该是几十分钟的,为啥这么做呢?

热门文章是大家共同都会看到的,也就是热点数据,在那做缓存,他是不需要那么高的实时性的,那下面的文章列表是最新发布的文章,有高实时性的特点,大家访问多的放在缓存还可以给DB减少压力,我也不知道掘金是不是这么做的哈,反正道理是这么个道理了。

那什么场景是使用Redis比较复杂的场景,而且需要大量中间件和业务逻辑去配合的呢?

秒杀!是的就是今天的主题秒杀,我就用我自己的思路带大家一起看一下,设计一个秒杀从前到后,从内到外到底要技术人员做多少准备。

捞一下

上一期吊打系列我们提到了Redis相关的一些知识,还没看的小伙伴可以回顾一下 ,这对于这期的阅读很有帮助,涉及到主从同步、读写分离、持久化这样的知识点。

  • Redis基础
  • 缓存雪崩、击穿、穿透
  • Redis哨兵、持久化、主从、手撕LRU
  • Redis终章凛冬将至、FPX新王登基
  • Redis常见面试题(带答案)

打好基础才可以写出更好的代码哟!不然就等着产品测试怼你吧。

正文

首先设计一个系统之前,我们需要先确认我们的业务场景是怎么样子的,我就带着大家一起假设一个场景好吧。

场景

我们现在要卖100件下面这个婴儿纸尿裤,然后我们根据以往这样秒杀活动的数据经验来看,目测来抢这100件纸尿裤的人足足有10万人。(南极人打钱!)

你一听,完了呀,这我们的服务器哪里顶得住啊!说真的直接打DB肯定挂。但是别急嘛,有暖男敖丙在,我们在开始之前应该先思考下会出现哪些问题?

问题

高并发:

是的高并发这个是我们想都不用想的一个点,一瞬间这么多人进来这不是高并发什么时候是呢?

是吧,秒杀的特点就是这样时间极短、 瞬间用户量大。

正常的店铺营销都是用极低的价格配合上短信、APP的精准推送,吸引特别多的用户来参与这场秒杀,爽了商家苦了开发呀。

秒杀大家都知道如果真的营销到位,价格诱人,几十万的流量我觉得完全不是问题,那单机的Redis我感觉3-4W的QPS还是能顶得住的,但是再高了就没办法了,那这个数据随便搞个热销商品的秒杀可能都不止了。

大量的请求进来,我们需要考虑的点就很多了,缓存雪崩,缓存击穿,缓存穿透这些我之前提到的点都是有可能发生的,出现问题打挂DB那就很难受了,活动失败用户体验差,活动人气没了,最后背锅的还是开发。

超卖:

但凡是个秒杀,都怕超卖,我这里举例的只是尿不湿,要是换成100个华为MatePro30,商家的预算经费卖100个可以赚点还可以造势,结果你写错程序多卖出去200个,你不发货用户投诉你,平台封你店,你发货就血亏,你怎么办?
(没事看了敖丙的文章直接不怕)

那最后只能杀个开发祭天解气了,秒杀的价格本来就低了,基本上都是不怎么赚钱的,超卖了就恐怖了呀,所以超卖也是很关键的一个点。

恶意请求:

你这么低的价格,假如我抢到了,我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛…)肯定也知道的。

那简单啊,我知道你什么时候抢,我搞个几十台机器搞点脚本,我也模拟出来十几万个人左右的请求,那我是不是意味着我基本上有80%的成功率了。

真实情况可能远远不止,因为机器请求的速度比人的手速往往快太多了,在贵州的敖丙我每年回家抢高铁票都是秒光的,我也不知道有没有黄牛的功劳,我要Diss你,黄牛。杰伦演唱会门票抢不到,我也Diss你。

Tip:科普下,小道消息了解到的,黄牛的抢票系统,比国内很多小公司的系统还吊很多,架构设计都是顶级的,我用顶配的服务加上顶配的架构设计,你还想看演唱会?还想回家?

不过不用黄牛我回家都难,我们云贵川跟我一样要回家过年的仔太多了555!

链接暴露:

前面几个问题大家可能都很好理解,一看到这个有的小伙伴可能会比较疑惑,啥是链接暴露呀?

相信是个开发同学都对这个画面一点都不陌生吧,懂点行的仔都可以打开谷歌的开发者模式,然后看看你的网页代码,有的就有URL,但是我写VUE的时候是事件触发然后去调用文件里面的接口看源码看不到,但是我可以点击一下查看你的请求地址啊,不过你好像可以对按钮在秒杀前置灰。

不管怎么样子都有危险,撇开外面的所有的东西你都挡住了,你卖这个东西实在便宜得过分,有诱惑力,你能保证开发不动心?开发知道地址,在秒杀的时候自己提前请求。。。(开发:怎么TM又是我)

数据库:

每秒上万甚至十几万的QPS(每秒请求数)直接打到数据库,基本上都要把库打挂掉,而且你服务不单单是做秒杀的还涉及其他的业务,你没做降级、限流、熔断啥的,别的一起挂,小公司的话可能全站崩溃404。

反正不管你秒杀怎么挂,你别把别的搞挂了对吧,搞挂了就不是杀一个程序员能搞定的。

程序员:我TM好难啊!

问题都列出来了,那怎么设计,怎么解决这些问题就是接下去要考虑的了,我们对症下药。

服务单一职责:

设计个能抗住高并发的系统,我觉得还是得单一职责。

什么意思呢,大家都知道现在设计都是微服务的设计思想,然后再用分布式的部署方式

也就是我们下单是有个订单服务,用户登录管理等有个用户服务等等,那为啥我们不给秒杀也开个服务,我们把秒杀的代码业务逻辑放一起。

单独给他建立一个数据库,现在的互联网架构部署都是分库的,一样的就是订单服务对应订单库,秒杀我们也给他建立自己的秒杀库。

至于表就看大家怎么设计了,该设置索引的地方还是要设置索引的,建完后记得用explain看看SQL的执行计划。(不了解的小伙伴也没事,MySQL章节我会说的)

单一职责的好处就是就算秒杀没抗住,秒杀库崩了,服务挂了,也不会影响到其他的服务。(强行高可用)

秒杀链接加盐:

我们上面说了链接要是提前暴露出去可能有人直接访问url就提前秒杀了,那又有小伙伴要说了我做个时间的校验就好了呀,那我告诉你,知道链接的地址比起页面人工点击的还是有很大优势。

我知道url了,那我通过程序不断获取最新的北京时间,可以达到毫秒级别的,我就在00毫秒的时候请求,我敢说绝对比你人工点的成功率大太多了,而且我可以一毫秒发送N次请求,搞不好你卖100个产品我全拿了。

那这种情况怎么避免?

简单,把URL动态化,就连写代码的人都不知道,你就通过MD5之类的加密算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。

暖男我呢,又准备了一个简单的url加密给大家尝尝鲜,还不点个赞?

Redis集群:

之前不是说单机的Redis顶不住嘛,那简单多找几个兄弟啊,秒杀本来就是读多写少,那你们是不是瞬间想起来我之前跟你们提到过的,Redis集群,主从同步、读写分离,我们还搞点哨兵,开启持久化直接无敌高可用!

Nginx:

Nginx大家想必都不陌生了吧,这玩意是高性能的web服务器,并发也随便顶几万不是梦,但是我们的Tomcat只能顶几百的并发呀,那简单呀负载均衡嘛,一台服务几百,那就多搞点,在秒杀的时候多租点流量机。

Tip:据我所知国内某大厂就是在去年春节活动期间租光了亚洲所有的服务器,小公司也很喜欢在双十一期间买流量机来顶住压力。

这样一对比是不是觉得你的集群能顶很多了。

恶意请求拦截也需要用到它,一般单个用户请求次数太夸张,不像人为的请求在网关那一层就得拦截掉了,不然请求多了他抢不抢得到是一回事,服务器压力上去了,可能占用网络带宽或者把服务器打崩、缓存击穿等等。

资源静态化:

秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所以页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

按钮控制:

大家有没有发现没到秒杀前,一般按钮都是置灰的,只有时间到了,才能点击。

这是因为怕大家在时间快到的最后几秒秒疯狂请求服务器,然后还没到秒杀的时候基本上服务器就挂了。

这个时候就需要前端的配合,定时去请求你的后端服务器,获取最新的北京时间,到时间点再给按钮可用状态。

按钮可以点击之后也得给他置灰几秒,不然他一样在开始之后一直点的。你敢说你们秒杀的时候不是这样的?

限流:

限流这里我觉得应该分为前端限流和后端限流。

前端限流:这个很简单,一般秒杀不会让你一直点的,一般都是点击一下或者两下然后几秒之后才可以继续点击,这也是保护服务器的一种手段。

后端限流:秒杀的时候肯定是涉及到后续的订单生成和支付等操作,但是都只是成功的幸运儿才会走到那一步,那一旦100个产品卖光了,return了一个false,前端直接秒杀结束,然后你后端也关闭后续无效请求的介入了。

Tip:真正的限流还会有限流组件的加入例如:阿里的Sentinel、Hystrix等。我这里就不展开了,就说一下物理的限流。

库存预热:

秒杀的本质,就是对库存的抢夺,每个秒杀的用户来你都去数据库查询库存校验库存,然后扣减库存,撇开性能因素,你不觉得这样好繁琐,对业务开发人员都不友好,而且数据库顶不住啊。

开发:你tm总算为我着想一次了。

那怎么办?

我们都知道数据库顶不住但是他的兄弟非关系型的数据库Redis能顶啊!

那不简单了,我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀结束了,再异步的去修改库存就好了。

但是用了Redis就有一个问题了,我们上面说了我们采用主从,就是我们会去读取库存然后再判断然后有库存才去减库存,正常情况没问题,但是高并发的情况问题就很大了。

这里我就不画图了,我本来想画图的,想了半天我觉得语言可能更好表达一点。

多品几遍!!!就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。咋办?

Lua:

之前的文章就简单的提到了他,我今天就多一定点篇幅说一下吧。

Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。

Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。这点是关键。

知道原理了,我们就写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求,然后再做后面的事情嘛。

限流&降级&熔断&隔离:

这个为啥要做呢,不怕一万就怕万一,万一你真的顶不住了,限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。

削峰填谷:

一说到这个名词,很多小伙伴就知道了,对的MQ,你买东西少了你直接100个请求改库我觉得没问题,但是万一秒杀一万个,10万个呢?服务器挂了,程序员又要背锅的。

Tip:可能小伙伴说我们业务达不到这个量级,没必要。但是我想说我们写代码,就不应该写出有逻辑漏洞的代码,至少以后公司体量上去了,别人一看居然不用改代码,一看代码作者是敖丙?有点东西!

你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。

总结

到这里我想我已经基本上把该考虑的点还有对应的解决方案也都说了一下,不知道还有没有没考虑到的,但是就算没考虑到我想我这个设计,应该也能撑住一个完整的秒杀流程。

(有大佬的话给敖丙点多的思路,去GitHub github.com/JavaFamily 上给我提,也有我的联系)

最后我就画个完整的流程图给大家收个尾吧!

Tip:这个链路还是比较简单的,很多细节的点全部画出来就太复杂了,我上面已经提到了所有的注意点了,大家都看看,真正的秒杀有比我这个简单的,也有比我这个复杂N倍的,之前的电商老东家就做的很高级,有机会也可以跟你们探讨,不过是面试嘛,我就给思路,让你理解比较关键的点。
秒杀这章我脑细胞死了很多,考虑了很多个点,最后还是出来了,忍不住给自己点赞!

(这章是真的不要白嫖,每次都看了不点赞,你们想白嫖我么?你们好坏喲,不过我好喜欢)

总结

我们玩归玩,闹归闹,别拿面试开玩笑。

秒杀不一定是每个同学都会问到的,至少肯定没Redis基础那样常问,但是一旦问到,大家一定要回答到点上。

至少你得说出可能出现的情况,需要注意的情况,以及对于的解决思路和方案。

最后就是需要对整个链路比较熟悉,注意是一个完整的链路,前端怎么设计的呀,网关的作用呀,怎么解决Redis的并发竞争啊,数据的同步方式呀,MQ的作用啊。

(提到MQ又是一整条的知识链路,什么异步、削峰、解耦等等,所以面试,我们还是不打没有把握的胜仗)

流着泪说再见

Redis系列到此是真的要跟大家说再见了,写了7篇文章,其实很多大佬的思路和片段真心赞,其实大家看出来了我的文章个人风格色彩特别浓厚,我个人在生活中就是这么说话的,也希望用这种风格把原本枯燥乏味的知识点让大家都像看小说一样津津有味的看下去,不知道大家什么感受,好的不好的都请给我留言。

我这个系列的我会写到我GitHub github.com/JavaFamily 图中所有的知识点,以后就麻烦大家多多关照了,我写作的时间都是业余时间,基本上周末和晚上的时间都贡献出来了,我也是个新人很多点也没接触到,也要看书看资料才能写出来,所以有时候还是希望大家多多包涵。

那我们下期见!

下期写**____**?

不告诉你,哈哈!

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。

我后面会每周都更新几篇一线互联网大厂面试和常用技术栈相关的文章,非常感谢人才们能看到这里,如果这个文章写得还不错,觉得「敖丙」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!

白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

敖丙 | 文 【原创】

如果本篇博客有任何错误,请批评指教,不胜感激 !


文章每周持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/JavaFamily 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

本文转载自: 掘金

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

1…847848849…956

开发者博客

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