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

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


  • 首页

  • 归档

  • 搜索

细说一下RedisTemplate的使用方法(四)

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

温故才能知新(复习一下)

前面几篇文章我们主要学习了RedisTemplate的几个基础方法,还记得有哪些方法吗?

从今天开始,我们就要开始学习RedisTemplate的推荐使用方法了,也是我们在日常开发中常用到的一些方法了,实用性也比前几篇文章高的多,大家一起来学习吧。

开始学习,RedisTemplate系列方法之战

RedisTemplate时主要提供了一些操作Redis方法的类,但是也有一部分方法并不是在RedisTemplate类中去实现的,比如常用到的opsForValue等方法,其内部实现并不是在RedisTemplate中。

像这种我们也会在后续的文章中说到,总之接下来我们要学习的完全就是实战了。

hasKey(String key)

功能描述:判断Redis相关key是否存在。

使用场景:在需要判断Redis相关key是否存在时使用此方法。比如在删除一个key时,当然要先查询是否存在该key才可以。

具体代码使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vbnet复制代码/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

源码截图:

image.png

keys(K pattern)

功能描述:获取当前所有的key。

使用场景:比如我们需要做一个Redis的key-value值的管理功能,那就必然要获取全部的key值,并以此用于展示或者其他操作。

源码截图:

image.png

randomKey()

功能描述:从redis中随机拿出一个key。

使用场景:这个场景虽然不多见,但是一旦用上绝对是很棒的体验。

源码截图:

image.png

小结

今天我们主要学习了三个方法,keys()、randomKey()、hasKey()三个方法,在操作redis时也是必不可少的,大家学到了吗?

本文转载自: 掘金

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

如何快速搞定第三方登录且易扩展?

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

本文节选自《设计模式就该这样学》

1 使用类适配器重构第三方登录自由适配

我们使用适配模式来实现一个实际的业务场景,解决实际问题。年纪稍微大一点的小伙伴一定经历过这样的过程。很早以前开发的老系统应该都有登录接口,但是随着业务的发展和社会的进步,单纯地依赖用户名密码登录显然不能满足用户需求。现在,大部分系统都已经支持多种登录方式,如QQ登录、微信登录、手机登录、微博登录等,同时保留用户名密码的登录方式。虽然登录形式丰富,但是登录后的处理逻辑可以不必改,都是将登录状态保存到Session,遵循开闭原则。首先创建统一的返回结果ResultMsg类。

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
java复制代码
/**
* Created by Tom.
*/
public class ResultMsg {

private int code;
private String msg;
private Object data;

public ResultMsg(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}
}

假设在老系统中,处理登录逻辑的代码在PassportService类中。

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复制代码
public class PassportService {

/**
* 注册方法
* @param username
* @param password
* @return
*/
public ResultMsg regist(String username,String password){
return new ResultMsg(200,"注册成功",new Member());
}


/**
* 登录方法
* @param username
* @param password
* @return
*/
public ResultMsg login(String username,String password){
return null;
}

}

为了遵循开闭原则,不修改老系统的代码。下面开启代码重构之路,创建Member类。

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
java复制代码
/**
* Created by Tom.
*/
public class Member {

private String username;
private String password;
private String mid;
private String info;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getMid() {
return mid;
}

public void setMid(String mid) {
this.mid = mid;
}

public String getInfo() {
return info;
}

public void setInfo(String info) {
this.info = info;
}
}

我们也不改动运行非常稳定的代码,创建Target角色IPassportForThird接口。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
public interface IPassportForThird {

ResultMsg loginForQQ(String openId);

ResultMsg loginForWechat(String openId);

ResultMsg loginForToken(String token);

ResultMsg loginForTelphone(String phone,String code);

}

创建Adapter角色实现兼容,创建一个新的类PassportForThirdAdapter,继承原来的逻辑。

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
java复制代码
public class PassportForThirdAdapter extends PassportService implements IPassportForThird {

public ResultMsg loginForQQ(String openId) {
return loginForRegist(openId,null);
}

public ResultMsg loginForWechat(String openId) {
return loginForRegist(openId,null);
}

public ResultMsg loginForToken(String token) {
return loginForRegist(token,null);
}

public ResultMsg loginForTelphone(String phone, String code) {
return loginForRegist(phone,null);
}

private ResultMsg loginForRegist(String username,String password){
if(null == password){
password = "THIRD_EMPTY";
}
super.regist(username,password);
return super.login(username,password);
}
}

客户端测试代码如下。

1
2
3
4
5
6
7
java复制代码
public static void main(String[] args) {
PassportForThirdAdapter adapter = new PassportForThirdAdapter();
adapter.login("tom","123456");
adapter.loginForQQ("sjooguwoersdfjhasjfsa");
adapter.loginForWechat("slfsjoljsdo8234ssdfs");
}

2 使用接口适配器优化代码

通过这么一个简单的适配动作,我们完成了代码兼容。当然,代码还可以更加优雅,根据不同的登录方式,创建不同的Adapter。首先创建LoginAdapter接口。

1
2
3
4
5
java复制代码
public interface ILoginAdapter {
boolean support(Object object);
ResultMsg login(String id,Object adapter);
}

然后创建一个抽象类AbstractAdapter继承PassportService原有的功能,同时实现ILoginAdapter接口,再分别实现不同的登录适配,QQ登录LoginForQQAdapter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
public class LoginForQQAdapter extends AbstractAdapter{
public boolean support(Object adapter) {
return adapter instanceof LoginForQQAdapter;
}

public ResultMsg login(String id, Object adapter) {
if(!support(adapter)){return null;}
//accesseToken
//time
return super.loginForRegist(id,null);

}

}

手机登录LoginForTelAdapter。

1
2
3
4
5
6
7
8
9
10
java复制代码
public class LoginForTelAdapter extends AbstractAdapter{
public boolean support(Object adapter) {
return adapter instanceof LoginForTelAdapter;
}

public ResultMsg login(String id, Object adapter) {
return super.loginForRegist(id,null);
}
}

Token自动登录LoginForTokenAdapter。

1
2
3
4
5
6
7
8
9
10
java复制代码
public class LoginForTokenAdapter extends AbstractAdapter {
public boolean support(Object adapter) {
return adapter instanceof LoginForTokenAdapter;
}

public ResultMsg login(String id, Object adapter) {
return super.loginForRegist(id,null);
}
}

微信登录LoginForWechatAdapter。

1
2
3
4
5
6
7
8
9
10
java复制代码
public class LoginForWechatAdapter extends AbstractAdapter{
public boolean support(Object adapter) {
return adapter instanceof LoginForWechatAdapter;
}

public ResultMsg login(String id, Object adapter) {
return super.loginForRegist(id,null);
}
}

接着创建适配器PassportForThirdAdapter类,实现目标接口IPassportForThird完成兼容。

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复制代码
public class PassportForThirdAdapter implements IPassportForThird {

public ResultMsg loginForQQ(String openId) {
return processLogin(openId, LoginForQQAdapter.class);
}

public ResultMsg loginForWechat(String openId) {

return processLogin(openId, LoginForWechatAdapter.class);

}

public ResultMsg loginForToken(String token) {

return processLogin(token, LoginForTokenAdapter.class);
}

public ResultMsg loginForTelphone(String phone, String code) {
return processLogin(phone, LoginForTelAdapter.class);
}


private ResultMsg processLogin(String id,Class<? extends ILoginAdapter> clazz){
try {
ILoginAdapter adapter = clazz.newInstance();
if (adapter.support(adapter)){
return adapter.login(id,adapter);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

}

客户端测试代码如下。

1
2
3
4
5
java复制代码
public static void main(String[] args) {
IPassportForThird adapter = new PassportForThirdAdapter();
adapter.loginForQQ("sdfasdfasfasfas");
}

最后来看如下图所示的类图。

file

至此,在遵循开闭原则的前提下,我们完整地实现了一个兼容多平台登录的业务场景。当然,目前的这个设计并不完美,仅供参考,感兴趣的小伙伴们可以继续完善这段代码。例如适配器类中的参数目前是设置为String,改为Object[]应该更合理。

学习到这里,相信小伙伴们会有一个疑问:适配器模式与策略模式好像区别不大?我要强调一下,适配器模式主要解决的是功能兼容问题,单场景适配可能不会和策略模式有对比。但复杂场景适配大家就很容易混淆。其实,大家有没有发现一个细节,笔者给每个适配器类都加上了一个support()方法,用来判断是否兼容,support()方法的参数类型也是Object,而support()来自接口。适配器类的实现逻辑并不依赖接口,完全可以将ILoginAdapter接口去掉。而加上接口,只是为了代码规范。上面代码可以说是策略模式、简单工厂模式和适配器模式的综合运用。

关注『 Tom弹架构 』回复“设计模式”可获取完整源码。

【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

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

Spring如何解决循环依赖? - springboot实战

发表于 2021-11-03

Spring如何解决循环依赖?

springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)

java开源商城系统

1
2
3
4
5
6
7
8
java复制代码@component
class A {
private B b;
}
@component
class B {
private A a;
}

类A依赖了B作为属性,类B又使用类A作为属性,彼此循环依赖。

循环依赖.jpg

源码理解:

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
java复制代码//调用AbstractBeanFactory.doGetBean(),向IOC容器获取Bean,触发依赖注入的方法
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
...
// 第一次getSingleton获取对象实例
// 先从缓存中取是否已经有被创建过的单例类型的Bean[没有的话就去获取半成品的,也就是earlySingletonObjects,缓存二的东西]
// 对于单例模式的Bean整个IOC容器中只创建一次,不需要重复创建
Object sharedInstance = getSingleton(beanName);
...
try {
//创建单例模式Bean的实例对象
if (mbd.isSingleton()) {
//第二次getSingleton尝试创建目标对象,并且注入属性
//这里使用了一个匿名内部类,创建Bean实例对象,并且注册给所依赖的对象
sharedInstance = getSingleton(beanName, () -> {
try {
//创建一个指定Bean实例对象,如果有父级继承,则合并子类和父类的定义
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
//显式地从容器单例模式Bean缓存中清除实例对象
destroySingleton(beanName);
throw ex;
}
});
// 如果传入的是factoryBean,则会调用其getObject方法,得到目标对象
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
//IOC容器创建原型模式Bean实例对象
}
...
} catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
...
return (T) bean;
}

也就是实例化A的时候在缓存中没找到*[第一个getSingleton],就去第二个getSingleton实例化A[实际上是调用了doCreateBean()]*,由于A需要B,又去doGetBean尝试获取B,发现B也不在缓存中,继续调用第二个getSingleton去实例化,当要注入属性A的时候在二级缓存找到了半成品A,成功注入返回到A实例化的阶段,将B注入。

第一个getSingleton代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码	@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//从spring容器中获取bean
Object singletonObject = this.singletonObjects.get(beanName);//缓存1
//如果获取不到 判断要获取的对象是不是正在创建过程中----如果是,则去缓存(三级缓存)中取对象(不是bean)
//isSingletonCurrentlyInCreation() 存放的对象 的时机是在getBean中第二次调用getSingleton时候beforeSingletonCreation(beanName);存进去的
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);//缓存2
if (singletonObject == null && allowEarlyReference) {//allowEarlyReference--判断是否支持循环依赖,默认为true
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);//缓存3
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
//将三级缓存升级为二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
//从三级缓存中删除 为什么删除?防止重复创建。设置三级缓存的目的是为了提高性能,因为每次创建都需要经过factory,会花费很多时间
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

在实例化AB的时候,三个缓存都是找不到这两个类的,因为两者均未创建;

三级缓存

1
2
3
4
5
6
7
8
java复制代码	/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);//一级缓存,存放完整的bean信息

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);//三级缓存,bean创建完了就放进去,还有他的bean工厂

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);//二级缓存,存放还没进行属性赋值的bean对象,也即半成品bean

第二个getSingleton代码

此处将A先创建好放入三级缓存中,实际上是委托给另一个**doGetBean()**完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
...
synchronized (this.singletonObjects) {
// 再次判断ioc容器中有无该bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
..before..
try {
// 回调到doCreateBean
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
..after..
}
return singletonObject;
}
}

由于第一个获取单例的方法找不到AB,故此将会进入第二个获取单例的方法试图找到,这个方法里singletonFactory.getObject()为核心,将会回调到doCreateBean方法继续创建Bean。

doCreateBean代码

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
java复制代码protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {

//封装被创建的Bean对象
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
//单例的情况下尝试从factoryBeanInstanceCache获取 instanceWrapper,并清除同名缓存
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 创建Bean实例
// instanceWrapper会包装好目标对象
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
...
//向容器中缓存单例模式的Bean对象,以防循环引用,allowCircularReferences是判断是否支持循环依赖,这个值可以改为false
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
//判断是否允许循环依赖
if (earlySingletonExposure) {
...
//将这个对象的工厂放入缓存中 (注册bean工厂singletonFactories,第一个getSingleton使用的),此时这个bean还没做属性注入
//这里是一个匿名内部类,为了防止循环引用,尽早持有对象的引用
// getEarlyBeanReference很特别,这里面会做aop代理
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
//Bean对象的初始化,依赖注入在此触发
//这个exposedObject在初始化完成之后返回作为依赖注入完成后的Bean
Object exposedObject = bean;
try {
//填充属性 -- 自动注入
//前面是实例化,并没有设置值,这里是设置值.将Bean实例对象封装,并且Bean定义中配置的属性值赋值给实例对象
//这里做注入的时候会判断依赖的属性在不在,不在就调用doGetBean继续创建
populateBean(beanName, mbd, instanceWrapper);
// 该方法主要是对bean做一些扩展
// 初始化Bean对象。属性注入已完成,处理各种回调
// (对实现Aware接口(BeanNameAware、BeanClassLoaderAware、BeanFactoryAware)的bean执行回调、
// aop、init-method、destroy-method?、InitializingBean、DisposableBean等)
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
...
return exposedObject;
}

在这个方法里,实例化A的时候,已经把A作为一个半成品通过调用addSingletonFactory方法将其加入了三级缓存singletonFactories,方便在递归实例化B的时候可以获取到A的半成品实例,详细代码如下:

将创建的bean加入三级缓存

发生在addSingletonFactory这个方法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
//放到三级缓存
// 注册bean工厂
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

那么是在什么时候会发生一个递归的调用呢?

实际上是在populateBean(beanName, mbd, instanceWrapper);要做属性注入的时候,假设是根据名称自动注入的,调用autowireByName(),该法会去循环遍历在getBean之前已经把xml文件的属性加入到注册表之类的属性,

populateBean有一个autowireByName的方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码protected void autowireByName(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {

//对Bean对象中非简单属性(不是简单继承的对象,如8中原始类型,字符串,URL等都是简单属性)进行处理
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
for (String propertyName : propertyNames) {
//如果Spring IOC容器中包含指定名称的Bean,就算还没进行初始化,A发现有B属性时,B属性已经被写入注册表之类的东西,所以这个判断返回true
if (containsBean(propertyName)) {
//调用getBean方法向IOC容器索取指定名称的Bean实例,迭代触发属性的初始化和依赖注入
Object bean = getBean(propertyName); //实际上就是委托doGetBean()
pvs.add(propertyName, bean);
//指定名称属性注册依赖Bean名称,进行属性依赖注入
registerDependentBean(propertyName, beanName);
if (logger.isDebugEnabled()) {
logger.debug("Added autowiring by name from bean name '" + beanName +
"' via property '" + propertyName + "' to bean named '" + propertyName + "'");
}
}
...
}
}

Object bean = getBean(propertyName); 这个方法实际上又去委托了doGetBean(),又一次递归的走上了流程,也就是A在实例化到该步时发现,还有一个B,就会又从doGetBean()开始,一步步的寻找创建,不同的是,当B走到这个根据名称注入的方法时,此时的已经能在二级缓存里找到A的身影了,无需再次创建A对象。

总结

spring运用三级缓存解决了循环依赖的问题;

采用递归的方式,逐步的去实例化对象,并将上一步已经加入缓存的半成品对象作为属性注入;

等到走到最后一个递归时,将会逐步返回,把对应的实例一个个创建好。

springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)

java开源商城系统

本文转载自: 掘金

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

Spring MVC传递接收参数的几种方式

发表于 2021-11-03

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

大家在开发中经常会用到Spring MVC Controller来接收请求参数,主要常用的接收方式就是通过实体对象以及形参等方式、有些用于GET请求,有些用于POST请求,有些用于两者。 下面的几种常见的后台接收参数的方式分享一下。主要方式如下:

  • 实体 Bean 接收请求参数
  • 处理方法的形参接收请求参数
  • HttpServletRequest 接收请求参数
  • @PathVariable 接收 URL 中的请求参数
  • @RequestParam 接收请求参数
  • @ModelAttribute 接收请求参数

可以根据实际情况选择合适的接收方式。

一、通过实体Bean接收请求参数

1
2
3
4
5
6
7
8
9
10
11
Java复制代码@RequestMapping("/login")
public String login(User user, Model model) {
if ("admin".equals(user.getName())
&& "123456".equals(user.getPwd())) {
model.addAttribute("message", "登录成功");
return "main"; // 登录成功,跳转到主页
} else {
model.addAttribute("message", "用户名或密码错误");
return "login";
}
}

二、通过处理方法的形参接收请求参数

通过处理方法的参数接收请求参数,就是将表单参数直接写入控制器类对应方法的参数中,即参数名称与请求参数名称完全相同。 接收参数模式适用于GET和POST提交请求

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RequestMapping("/login")
public String login(String name, String pwd, Model model) {
if ("admin".equals(user.getName())
&& "123456".equals(user.getPwd())) {

model.addAttribute("message", "登录成功");
return "main"; // 登录成功,跳转到跳转到主页
} else {
model.addAttribute("message", "用户名或密码错误");
return "login";
}
}

三、通过HttpServletRequest接收请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java复制代码@RequestMapping("/login")
public String login(HttpServletRequest request, Model model) {
String name = request.getParameter("name");
String pwd = request.getParameter("pwd");

if ("admin".equals(name)
&& "123456".equals(pwd)) {
model.addAttribute("message", "登录成功");
return "main"; // 登录成功,跳转到 main.jsp
} else {
model.addAttribute("message", "用户名或密码错误");
return "login";
}
}

四、通过@PathVariable接收URL中的请求参数

通过 @PathVariable 获取 URL 中的参数,示例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码@RequestMapping("/login/{name}/{pwd}")
public String login(@PathVariable String name, @PathVariable String pwd, Model model) {

if ("admin".equals(name)
&& "123456".equals(pwd)) {

model.addAttribute("message", "登录成功");
return "main"; // 登录成功,跳转到主页
} else {
model.addAttribute("message", "用户名或密码错误");
return "login";
}
}

五、通过@RequestParam接收请求参数

在方法入参处使用 @RequestParam 注解指定其对应的请求参数。@RequestParam 有以下三个参数:

  • value:参数名
  • required:是否必须,默认为 true,表示请求中必须包含对应的参数名,若不存在将抛出异常
  • defaultValue:参数默认值
1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码@RequestMapping("/login")
public String login(@RequestParam String name, @RequestParam String pwd, Model model) {

if ("admin".equals(name)
&& "123456".equals(pwd)) {
model.addAttribute("message", "登录成功");
return "main"; // 登录成功,跳转到主页
} else {
model.addAttribute("message", "用户名或密码错误");
return "login";
}
}

六、通过@ModelAttribute接收请求参数

@ModelAttribute 注释用于将多个请求参数封装到单个实体对象中,以简化数据绑定过程,并自动作为模型数据公开,以便在视图页面表示中使用。
“通过实体bean接收请求参数”简单地将多个请求参数封装到一个实体对象中,而不将它们作为模型数据公开

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码@RequestMapping("/login")
public String login(@ModelAttribute("user") User user, Model model) {

if ("admin".equals(name)
&& "123456".equals(pwd)) {

model.addAttribute("message", "登录成功");
return "main"; // 登录成功,跳转到主页
} else {
model.addAttribute("message", "用户名或密码错误");
return "login";
}
}

本文转载自: 掘金

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

高级JAVA开发必备技能:java8 新日期时间API((三

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

❤️作者简介:Java领域优质创作者🏆,CSDN博客专家认证🏆,华为云享专家认证🏆

❤️技术活,该赏

❤️点赞 👍 收藏 ⭐再看,养成习惯

大家好,我是小虚竹。之前有粉丝私聊我,问能不能把JAVA8 新的日期时间API(JSR-310)知识点梳理出来。答案是肯定的,谁让我宠粉呢。由于内容偏多,会拆成多篇来写。

闲话就聊到这,请看下面的正文。

DateTimeFormatter

DateTimeFormatter类说明

DateTimeFormatter的作用是进行格式化日期时间显示,且DateTimeFormatter是不可变类且是线程安全的。

1
2
3
java复制代码public final class DateTimeFormatter {
...
}

说到时间的格式化显示,就要说老朋友SimpleDateFormat了,之前格式化Date就要用上。但是我们知道SimpleDateFormat是线程不安全的,还不清楚的,请看这篇文章java的SimpleDateFormat线程不安全出问题了,虚竹教你多种解决方案

DateTimeFormatter常用的用法

格式化

1
2
3
4
5
6
7
8
9
java复制代码		ZonedDateTime zonedDateTime = ZonedDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZZ");
System.out.println(formatter.format(zonedDateTime));

DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("E, MMMM/dd/yyyy HH:mm", Locale.US);
System.out.println(usFormatter.format(zonedDateTime));

DateTimeFormatter chinaFormatter = DateTimeFormatter.ofPattern("yyyy MMM dd EE HH:mm", Locale.CHINA);
System.out.println(chinaFormatter.format(zonedDateTime));

image-202107209416958

解析

1
2
3
4
java复制代码		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
String dateTime = "2021年08月22日 13时14分20秒";
LocalDateTime localDateTime = LocalDateTime.parse(dateTime, formatter);
System.out.println(localDateTime);

image-2021082291306050

大家有没有注意到,parse方法 是放在LocalDateTime类中 的,而不是DateTimeFormatter类中 。这样的设计符合正常的思路想法,想解析出LocalDateTime 的日期时间,那就用LocalDateTime 。想解析其他的JSR-310的日期时间对象,那就用对应的日期时间对象去解析。

博主把常用的日期时间API都看了,这些里面除了Clock (时钟不需要解析的),其他都有实现parse方法 。

image-20210824903956

DateTimeFormatter的坑

1、在正常配置按照标准格式的字符串日期,是能够正常转换的。如果月,日,时,分,秒在不足两位的情况需要补0,否则的话会转换失败,抛出异常。

1
2
3
java复制代码		DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
LocalDateTime dt1 = LocalDateTime.parse("2021-7-20 23:46:43.946", DATE_TIME_FORMATTER);
System.out.println(dt1);

会报错:

image-202107208183

1
java复制代码java.time.format.DateTimeParseException: Text '2021-7-20 23:46:43.946' could not be parsed at index 5

分析原因:是格式字符串与实际的时间不匹配

“yyyy-MM-dd HH:mm:ss.SSS”

“2021-7-20 23:46:43.946”

中间的月份格式是MM,实际时间是7

解决方案:保持格式字符串与实际的时间匹配

1
2
3
java复制代码	DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
LocalDateTime dt1 = LocalDateTime.parse("2021-07-20 23:46:43.946", DATE_TIME_FORMATTER);
System.out.println(dt1);

image-20210720504067

2、YYYY和DD谨慎使用

1
2
3
4
java复制代码		LocalDate date = LocalDate.of(2020,12,31);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYYMM");
// 结果是 202112
System.out.println( formatter.format(date));

image-202107208183

1
java复制代码Java’s DateTimeFormatter pattern “YYYY” gives you the week-based-year, (by default, ISO-8601 standard) the year of the Thursday of that week.

YYYY是取的当前周所在的年份,week-based year 是 ISO 8601 规定的。2020年12月31号,周算年份,就是2021年

image-2021072059555

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 private static void tryit(int Y, int M, int D, String pat) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pat);
LocalDate dat = LocalDate.of(Y,M,D);
String str = fmt.format(dat);
System.out.printf("Y=%04d M=%02d D=%02d " +
"formatted with " +
"\"%s\" -> %s\n",Y,M,D,pat,str);
}
public static void main(String[] args){
tryit(2020,01,20,"MM/DD/YYYY");
tryit(2020,01,21,"DD/MM/YYYY");
tryit(2020,01,22,"YYYY-MM-DD");
tryit(2020,03,17,"MM/DD/YYYY");
tryit(2020,03,18,"DD/MM/YYYY");
tryit(2020,03,19,"YYYY-MM-DD");
}
1
2
3
4
5
6
ini复制代码Y=2020 M=01 D=20 formatted with "MM/DD/YYYY" -> 01/20/2020
Y=2020 M=01 D=21 formatted with "DD/MM/YYYY" -> 21/01/2020
Y=2020 M=01 D=22 formatted with "YYYY-MM-DD" -> 2020-01-22
Y=2020 M=03 D=17 formatted with "MM/DD/YYYY" -> 03/77/2020
Y=2020 M=03 D=18 formatted with "DD/MM/YYYY" -> 78/03/2020
Y=2020 M=03 D=19 formatted with "YYYY-MM-DD" -> 2020-03-79

最后三个日期是有问题的,因为大写的DD代表的是处于这一年中那一天,不是处于这个月的那一天,但是dd就没有问题。

例子参考于:www.cnblogs.com/tonyY/p/121…

所以建议使用yyyy和dd。

3、DateTimeFormatter.format(Instant)会报错

报错信息:

1
java复制代码java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra

分析原因:

代码**DateTimeFormatter.format(Instant)**是怎么处理的呢?

1
2
3
4
5
java复制代码    public String format(TemporalAccessor temporal) {
StringBuilder buf = new StringBuilder(32);
formatTo(temporal, buf);
return buf.toString();
}

首先new了个StringBuilder对象,用来拼接字符串;

然后调用**formatTo(temporal, buf)**方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public void formatTo(TemporalAccessor temporal, Appendable appendable) {
Objects.requireNonNull(temporal, "temporal");
Objects.requireNonNull(appendable, "appendable");
try {
DateTimePrintContext context = new DateTimePrintContext(temporal, this);
if (appendable instanceof StringBuilder) {
printerParser.format(context, (StringBuilder) appendable);
} else {
// buffer output to avoid writing to appendable in case of error
StringBuilder buf = new StringBuilder(32);
printerParser.format(context, buf);
appendable.append(buf);
}
} catch (IOException ex) {
throw new DateTimeException(ex.getMessage(), ex);
}
}

**formatTo(temporal, buf)**方法也是先判断两个入参空处理。

然后,Instant对象被封装在一个新new的DateTimePrintContext对象

运行demo有问题,进行排查

1
2
3
4
java复制代码		//根据特定格式格式化日期
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String dateStr = DateUtil.format(new Date(),dtf);
System.out.println(dateStr);

image-20210725195348793

到这里已经是jdk的源码了DateTimeFormatter.format

image-20210725195424950

image-20210725195522610

image-20210725195636339

从上面可知,会调用 NumberPrinterParser.format() NumberPrinterParser是在DateTimeFormatterBuilder类中的。

image-20210725195947802

到这一步会报错

image-20210725200153850

为什么会报错呢,我们来看下context.getValue(field)发生了什么:

image-20210725200349650

从上面代码可行,temporal实际上是Instant对象,Instant.getLong只支持四种字段类型。。

1
2
3
4
复制代码NANO_OF_SECOND
MICRO_OF_SECOND
MILLI_OF_SECOND
INSTANT_SECONDS

image-20210725200551164

如果不是上面这几种字段类型,则抛出异常

DateUtil.format当遇到DateTimeFormatter会将Date对象首先转换为Instant,因为缺少时区,导致报错。

解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 根据特定格式格式化日期
*
* @param date 被格式化的日期
* @param format
* @return 格式化后的字符串
* @since 5.0.0
*/
public static String format(Date date, DateTimeFormatter format) {
if (null == format || null == date) {
return null;
}
Instant instant = date.toInstant();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
return format.format(localDateTime);
}

先把date类型转化为LocalDateTime类型,然后再进行DateTimeFormatter.format(LocalDateTime)的格式化

测试demo

1
2
3
4
5
6
7
java复制代码//根据特定格式格式化日期
String str = "2021-07-25 20:11:25";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:dd");
Date date = DateUtil.parse(str);
String dateStr = DateUtil.format(date,dtf);
System.out.println(dateStr);
Assert.assertEquals(str, dateStr);

image-20210725201444728

DateTimeFormatterBuilder

DateTimeFormatterBuilder类说明

DateTimeFormatter 的所有格式化器都是用DateTimeFormatterBuilder 建造器类创建的。

看下面两个ofPattern 源码:

1
2
3
4
5
6
7
8
java复制代码//DateTimeFormatter
public static DateTimeFormatter ofPattern(String pattern) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
}

public static DateTimeFormatter ofPattern(String pattern, Locale locale) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale);
}

解析风格配置

官方提供了四种解析风格的配置,如下枚举 SettingsParser :

1
2
3
4
5
6
7
8
9
10
11
java复制代码 static enum SettingsParser implements DateTimePrinterParser {
// 大小写敏感
SENSITIVE,
// 大小写不敏感
INSENSITIVE,
//严格
STRICT,
//宽松
LENIENT;
...
}

对应DateTimeFormatterBuilder 类中的方法:

1
2
3
4
5
6
7
8
java复制代码// 大小写敏感
public DateTimeFormatterBuilder parseCaseSensitive()
// 大小写不敏感
public DateTimeFormatterBuilder parseCaseInsensitive()
// 严格
public DateTimeFormatterBuilder parseStrict()
// 宽松
public DateTimeFormatterBuilder parseLenient()

这四个方法对应的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 大小写敏感
public DateTimeFormatterBuilder parseCaseSensitive() {
appendInternal(SettingsParser.SENSITIVE);
return this;
}
// 大小写不敏感
public DateTimeFormatterBuilder parseCaseInsensitive() {
appendInternal(SettingsParser.INSENSITIVE);
return this;
}
// 严格
public DateTimeFormatterBuilder parseStrict() {
appendInternal(SettingsParser.STRICT);
return this;
}
// 宽松
public DateTimeFormatterBuilder parseLenient() {
appendInternal(SettingsParser.LENIENT);
return this;
}

可以看出,都是调用appendInternal 方法。

接着往下看 appendInternal 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码 private int appendInternal(DateTimePrinterParser pp) {
Objects.requireNonNull(pp, "pp");
if (active.padNextWidth > 0) {
if (pp != null) {
pp = new PadPrinterParserDecorator(pp, active.padNextWidth, active.padNextChar);
}
active.padNextWidth = 0;
active.padNextChar = 0;
}
active.printerParsers.add(pp);
active.valueParserIndex = -1;
return active.printerParsers.size() - 1;
}

其中active 是一个DateTimeFormatterBuilder 实例,且这个DateTimeFormatterBuilder 实例内部有一个列表 List< DateTimePrinterParser > ,看了源码可知,真正做解析工作的是DateTimePrinterParser 对应的实例来做的。

DateTimePrinterParser 的源码:

1
2
3
4
5
6
java复制代码 interface DateTimePrinterParser {

boolean format(DateTimePrintContext context, StringBuilder buf);

int parse(DateTimeParseContext context, CharSequence text, int position);
}

源码有一共有16个DateTimePrinterParser 的实例。

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
java复制代码//1.Composite printer and parser.
static final class CompositePrinterParser implements DateTimePrinterParser {...}

//2.Pads the output to a fixed width.
static final class PadPrinterParserDecorator implements DateTimePrinterParser {...}

//3.Enumeration to apply simple parse settings.
static enum SettingsParser implements DateTimePrinterParser{...}

//4. Defaults a value into the parse if not currently present.
static class DefaultValueParser implements DateTimePrinterParser {...}

//5.Prints or parses a character literal.
static final class CharLiteralPrinterParser implements DateTimePrinterParser {...}

//6.Prints or parses a string literal.
static final class StringLiteralPrinterParser implements DateTimePrinterParser {...}

//7.Prints and parses a numeric date-time field with optional padding.
static class NumberPrinterParser implements DateTimePrinterParser {...}

//8.Prints and parses a numeric date-time field with optional padding.
static final class FractionPrinterParser implements DateTimePrinterParser {...}

//9.Prints or parses field text.
static final class TextPrinterParser implements DateTimePrinterParser {...}

//10.Prints or parses an ISO-8601 instant.
static final class InstantPrinterParser implements DateTimePrinterParser {...}

//11.Prints or parses an offset ID.
static final class OffsetIdPrinterParser implements DateTimePrinterParser {...}

//12.Prints or parses an offset ID.
static final class LocalizedOffsetIdPrinterParser implements DateTimePrinterParser {...}

//13.Prints or parses a zone ID.
static class ZoneIdPrinterParser implements DateTimePrinterParser {...}

//14. Prints or parses a chronology.
static final class ChronoPrinterParser implements DateTimePrinterParser {...}

//15.Prints or parses a localized pattern.
static final class LocalizedPrinterParser implements DateTimePrinterParser {...}

//16.Prints or parses a localized pattern from a localized field.
static final class WeekBasedFieldPrinterParser implements DateTimePrinterParser {...}

推荐相关文章

hutool日期时间系列文章

1DateUtil(时间工具类)-当前时间和当前时间戳

2DateUtil(时间工具类)-常用的时间类型Date,DateTime,Calendar和TemporalAccessor(LocalDateTime)转换

3DateUtil(时间工具类)-获取日期的各种内容

4DateUtil(时间工具类)-格式化时间

5DateUtil(时间工具类)-解析被格式化的时间

6DateUtil(时间工具类)-时间偏移量获取

7DateUtil(时间工具类)-日期计算

8ChineseDate(农历日期工具类)

9LocalDateTimeUtil(JDK8+中的{@link LocalDateTime} 工具类封装)

10TemporalAccessorUtil{@link TemporalAccessor} 工具类封装

其他

要探索JDK的核心底层源码,那必须掌握native用法

万字博文教你搞懂java源码的日期和时间相关用法

java的SimpleDateFormat线程不安全出问题了,虚竹教你多种解决方案

源码分析:JDK获取默认时区的风险和最佳实践

本文转载自: 掘金

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

【算法学习】LCP 06 拿硬币(java / c / c

发表于 2021-11-03

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

非常感谢你阅读本文~
欢迎【👍点赞】【⭐收藏】【📝评论】~
放弃不难,但坚持一定很酷~
希望我们大家都能每天进步一点点~
本文由 二当家的白帽子 https://juejin.cn/user/2771185768884824/posts 博客原创~


LCP 06. 拿硬币:

桌上有 n 堆力扣币,每堆的数量保存在数组 coins 中。我们每次可以选择任意一堆,拿走其中的一枚或者两枚,求拿完所有力扣币的最少次数。

样例 1

1
2
3
4
5
6
7
8
markdown复制代码输入:
[4,2,1]

输出:
4

解释:
第一堆力扣币最少需要拿 2 次,第二堆最少需要拿 1 次,第三堆最少需要拿 1 次,总共 4 次即可拿完。

样例 2

1
2
3
4
5
markdown复制代码输入:
[2,3,10]

输出:
8

提示

  • 1 <= n <= 4
  • 1 <= coins[i] <= 10

分析

  • 这道算法题二当家的相信大家都能做出来,但是不是已经仔细优化过了呢?
  • 每次任意选择一堆去拿,很显然每一堆之间互不影响,所以累加计算每一堆最少拿几次之和就是想要的结果。
  • 每次可以拿一枚或两枚,孩子也知道往多了拿嘛,肯定每次都拿两枚是最快的,只不过如果某一堆硬币是奇数,那最后一次就只能拿一枚。
  • 直接用二取整除的话,奇数就会少算一次,所以我们需要判断奇数还是偶数,如果是奇数就要多算一次。
  • 是不是可以统一处理奇数和偶数呢?可以直接先加一再用二取整除 (coins[i] + 1) / 2。如果原本是偶数,则不影响取整除的结果,如果是奇数则会使取整除的结果加一。
  • 位运算要比算术运算快,所以我们可以优化为 (coins[i] + 1) >> 1。

题解

java

1
2
3
4
5
6
7
8
9
java复制代码class Solution {
public int minCount(int[] coins) {
int ans = 0;
for (int c : coins) {
ans += (c + 1) >> 1;
}
return ans;
}
}

c

1
2
3
4
5
6
7
c复制代码int minCount(int* coins, int coinsSize){
int ans = 0;
for (int i = 0; i < coinsSize; ++i) {
ans += (coins[i] + 1) >> 1;
}
return ans;
}

c++

1
2
3
4
5
6
7
8
9
10
cpp复制代码class Solution {
public:
int minCount(vector<int>& coins) {
int ans = 0;
for (int& c : coins) {
ans += (c + 1) >> 1;
}
return ans;
}
};

python

1
2
3
python复制代码class Solution:
def minCount(self, coins: List[int]) -> int:
return sum([(c + 1) >> 1 for c in coins])

go

1
2
3
4
5
6
7
go复制代码func minCount(coins []int) int {
ans := 0
for _, c := range coins {
ans += (c + 1) >> 1
}
return ans
}

rust

1
2
3
4
5
6
7
rust复制代码impl Solution {
pub fn min_count(coins: Vec<i32>) -> i32 {
coins.iter().map(|c|{
(c + 1) >> 1
}).sum()
}
}

在这里插入图片描述


原题传送门:https://leetcode-cn.com/problems/na-ying-bi/


本文转载自: 掘金

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

kafka集群图形界面管理工具kafka-manager

发表于 2021-11-03

应用说明:

图形web相对于命令行很多时候显得更直观,kafka-manager是yahoo开源出来的项目,web界面还挺好用,安装更是很便捷。

安装环境:

具体安装:

1. 下载已经编译好的zip包

  下载链接:download.csdn.net/download/da…

  说明:编译时间较长,直接下载已经编译好的包就可以使用。

   [root@server-1 ~]# unzip kafka-manager-1.3.3.15.zip -d /opt/     //这里将下载好的包解压到/opt目录

  如果需要自行编译安装,到Github下载:github.com/yahoo/kafka…

2. 编辑配置文件

  切换目录至kafka-manager的conf目录,编辑application.conf

image.png

image.png

保存后,退出;

3. 启动kafka-manager

  切换目录至bin目录

  

  以后台方式启动kafka-manager

1 [root@server-1 bin]``# nohup ./kafka-manager &

  说明:kafka-manager默认端口是9000,如需更改端口,可以配置文件中新增http.port=…或者在命令行中如下格式添加:

1 [root@server-1 bin]``# nohup ./kafka-manager -Dhttp.port=9999

  如果配置文件没修改zookeeper集群,也可以填写在命令行中:

1 [root@server-1 bin]``# nohup ./kafka-manager -Dhttp.port=9999 -Dkafka-manager.zkhosts=192.168.1.21:2181,192.168.1.22:2181,192.168.1.23:2181

  启动后,检查一下对应端口,确保端口已经启动。

4. 登录Web界面

  在浏览器输入http://192.168.1.21:9000,登录后操作如下添加kafka集群相应信息和对应zookeeper

image.png

image.png

image.png

创建Topic,删除Topic,等等都可以在图形界面操作,具体略。

本文转载自: 掘金

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

Docker从入门到干事,看这一篇就够了 前言 安装dock

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

点赞再看,养成习惯。微信搜索【一条coding】关注这个在互联网摸爬滚打的程序员。

领取学习路线、系列文章、面试题库、自学资料、100本电子书等。


岁月无情,余生有涯,将生活扛在肩上,风雨兼程。

前言

哈喽,大家好,我是一条。

《大厂面试突击》专栏目前已发布三篇万字总结,收获500+的订阅,感谢各位的支持。

面试10多家中大厂后的万字总结——❤️集合篇❤️

面试10多家中大厂后的万字总结——❤️JavaWeb篇❤️

面试10多家中大厂后的万字总结——❤️java基础篇❤️

今天给大家带来docker的万字总结,虽说我们是开发,但docker不能不会,技术的广度就在这里体现。

安装docker

鉴于同学们用的设备都不一样,不能让大家在第一步就被劝退,所以三个平台的安装方式都准备了,请自行选择。

不推荐在windows安装

mac

命令行安装

需要先安装homebrew

homebrew国内镜像

1
shell复制代码/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

执行后选择中科大的镜像,即数字1

clone时间过长,约5-10分钟。

安装docker

1
shell复制代码brew install --cask --appdir=/Applications docker

Installing cask docker时请耐心等待,时间较长

dmg安装

点击链接下载安装即可,并带有可视化界面。但个人觉得并不好用。

download.docker.com/mac/edge/Do…

启动docker服务

点击图标或者

1
shell复制代码open /Applications/Docker.app

windows

不推荐在windows安装,如果实在没有也可以装。

教程参考:www.runoob.com/docker/wind…

Docker 并非是一个通用的容器工具,它依赖于已存在并运行的 Linux 内核环境。

Docker 实质上是在已经运行的 Linux 下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的 Linux 主机。

因此,Docker 必须部署在 Linux 内核的系统上。如果其他系统想部署 Docker 就必须安装一个虚拟 Linux 环境。

img

在 Windows 上部署 Docker 的方法都是先安装一个虚拟机,并在安装 Linux 系统的的虚拟机中运行 Docker。

Docker Desktop 是 Docker 在 Windows 10 和 macOS 操作系统上的官方安装方式,这个方法依然属于先在虚拟机中安装 Linux 然后再安装 Docker 的方法。

Docker Desktop 官方下载地址: hub.docker.com/editions/co…

**注意:**此方法仅适用于 Windows 10 操作系统专业版、企业版、教育版和部分家庭版!

安装 Hyper-V

Hyper-V 是微软开发的虚拟机,类似于 VMWare 或 VirtualBox,仅适用于 Windows 10。这是 Docker Desktop for Windows 所使用的虚拟机。

但是,这个虚拟机一旦启用,QEMU、VirtualBox 或 VMWare Workstation 15 及以下版本将无法使用!如果你必须在电脑上使用其他虚拟机(例如开发 Android 应用必须使用的模拟器),请不要使用 Hyper-V!

开启 Hyper-V

右键开始菜单并以管理员身份运行 PowerShell,执行以下命令:

1
shell复制代码Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

安装 Docker Desktop for Windows

点击 Get started with Docker Desktop,并下载 Windows 的版本,如果你还没有登录,会要求注册登录:

img

运行安装文件

双击下载的 Docker for Windows Installer 安装文件,一路 Next,点击 Finish 完成安装。

img

安装完成后,Docker 会自动启动。通知栏上会出现个小鲸鱼的图标img,这表示 Docker 正在运行。

我们可以在命令行执行 docker version 来查看版本号。

阿里云(linux)

基于阿里云服务器的安装方式,推荐!

查看配置

1
2
shell复制代码# 内核版本查看
uname -r

系统版本:CentOS7

内核版本:3.10.0-514.26.2.el7.x86_64

安装

安装有两种方式:

1.官方脚本安装(本文讲解)

2.手动安装

安装:此为国内镜像。安装完提示如果想在非root用户使用,需将用命名加入组,并重启。

1
shell复制代码curl -fsSL https://get.docker.com/ | sh

⚠️如果报错缺少deltarpm,执行下面命令

1
2
shell复制代码yum provides '*/applydeltarpm'    #查看依赖包的位置
yum -y install deltarpm #安装命令

启动docker服务

1
shell复制代码service docker start

❤️本文以下全部讲解均基于Linux系统❤️

运行实例

本着一切语言都是从hello-world开始的原则,我们先运行官方的实例体验一下。

官方提供了hello-world实例。运行前需要在官网注册docker id并创建仓库。

官网地址:hub.docker.com

注册时注意id起的复杂一点,很容易重复。

启动docker服务

1
shell复制代码systemctl start docker

拉取镜像

1
shell复制代码docker pull hello-world

运行镜像

1
shell复制代码docker run hello-world

查看容器

1
2
shell复制代码# -a 查看所有
docker ps -a

镜像加速

如果刚才拉取镜像时感觉速度过慢可以配置加速,速度正常可跳过此步。

鉴于国内网络问题,后续拉取 Docker 镜像十分缓慢,我们可以需要配置加速器来解决。

网易的镜像地址:hub-mirror.c.163.com。

在任务栏点击 Docker for mac 应用图标

Perferences... -> Daemon -> Registry mirrors

在列表中填写加速器地址即可。

修改完成之后,点击 Apply & Restart 按钮,Docker 就会重启并应用配置的镜像地址了。

通俗理解什么是docker?

Docker的思想来自于集装箱,集装箱解决了什么问题?

在一艘大船上,可以把货物规整的摆放起来。并且各种各样的货物被集装箱标准化了,集装箱和集装箱之间不会互相影响。那么我就不需要专门运送水果的船和专门运送化学品的船了。只要这些货物在集装箱里封装的好好的,那我就可以用一艘大船把他们都运走。

docker就是类似的理念。现在都流行云计算了,云计算就好比大货轮。docker就是集装箱。

  • 不同的应用程序可能会有不同的应用环境,比如.net开发的网站和php开发的网站依赖的软件就不一样,如果把他们依赖的软件都安装在一个服务器上就要调试很久,而且很麻烦,还会造成一些冲突。比如IIS和Apache访问端口冲突。这个时候你就要隔离.net开发的网站和php开发的网站。常规来讲,我们可以在服务器上创建不同的虚拟机在不同的虚拟机上放置不同的应用,但是虚拟机开销比较高。docker可以实现虚拟机隔离应用环境的功能,并且开销比虚拟机小,小就意味着省钱了。
  • 你开发软件的时候用的是Ubuntu,但是运维管理的都是centos,运维在把你的软件从开发环境转移到生产环境的时候就会遇到一些Ubuntu转centos的问题,比如:有个特殊版本的数据库,只有Ubuntu支持,centos不支持,在转移的过程当中运维就得想办法解决这样的问题。这时候要是有docker你就可以把开发环境直接封装转移给运维,运维直接部署你给他的docker就可以了。而且部署速度快。
  • 在服务器负载方面,如果你单独开一个虚拟机,那么虚拟机会占用空闲内存的,docker部署的话,这些内存就会利用起来。

总之docker就是集装箱原理。

docker与虚拟机的对比

物理机:别墅

虚拟机:楼房

docker:酒店式公寓

docker三大概念

库:一个总的仓库,包含所有的镜像,使用时可以从库拉取镜像到本地。

镜像:从库中拉取下来的应用,比如mysql。

容器:镜像运行之后就是容器,容器和镜像可以互相转换。

docker工作流程

docker命令

docker指令基本用法:

1
shell复制代码docker 命令关键字 -参数

基本操作

1
2
3
4
5
6
7
8
9
10
11
shell复制代码# 查看docker信息
docker info

# docker版本
docker version

# 查找镜像
docker search nginx

#拉取镜像
docker pull nginx

入门案例

快速搭建wordpress博客

查找镜像

1
2
3
shell复制代码docker search name
## wordpress
## mariadb

拉取镜像

1
2
3
shell复制代码docker pull wordpress
# mariadb就是mysql
docker pull mariadb

运行镜像

1
2
shell复制代码docker run --name db -p 3306:3306 --env MYSQL_ROOT_PASSWORD=root -d mariadb
docker run --name mywordpress --link db:mysql -p 8080:80 -d wordpress

运行成功,访问wordpress

1
html复制代码http://libiao:8080

根据提示配置数据库信息,一个个人博客网站就搭建好了

查看端口映射

1
2
3
shell复制代码docker ps

docker port CONTAINER_ID

举例:xxjob的8080端口映射到宿主机的8089端口

linux设置docker开机自启

1
shell复制代码systemctl enable docker

查看镜像

1
2
3
shell复制代码docker images

#因为docker是分层,所以显示的文件大小要大于实际占用磁盘的大小

运行镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码docker run --name db -p 3306:3306 --env MYSQL_ROOT_PASSWORD=root -d mariadb

# --name 别名

# --env 环境变量

#-d 后台执行

docker run --name mywordpress --link db:mysql -p 8080:80 -d wordpress

# --link ip映射

# -p 端口映射

docker logs -f 7a38a1ad55c6

# 像tail -f一样查看容器内日志

docker top name

#查看容器内的进程

删除镜像

1
2
3
4
5
shell复制代码docker rmi hello-world:latest

docker rmi id

# 4位即可

复合命令

1
2
3
shell复制代码docker rm -f $(docker ps -a -q)

#删除全部容器

查看运行的容器

1
shell复制代码docker ps

docker compose

一个方便维护多个容器的yaml文件,docker认为一个容器对应一个进程,但一个应用会有多个进程,例如上面的mysql和wordpress。

个人觉得docker compose类似于shell脚本,但他实际都python实现,访问的是docker的一些api。

Docker compose一般随docker一起安装,所以要求版本对应

1
2
3
css复制代码docker version

docker-compose --version

在yaml文件制定镜像的名字,版本,端口映射后用 up -d 启动

1
复制代码docker-compose.yaml up -d

查看日志

1
复制代码docker-compose logs

容器管理

进入容器

1
shell复制代码docker exec -it name /bin/sh

查看容器详细信息

容器的详细信息会以json的形式返回。

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
json复制代码# docker inspect name
[root@lib mysh]# docker inspect mywordpress

[
{
"Id": "6253e66959047c6f8de891abe1c661f7766fdef7407f00e07d1788310e0ea6a9",

"Created": "2021-08-04T20:11:43.649001354Z",

"Path": "docker-entrypoint.sh",

"Args": [

"apache2-foreground"

],

"State": {

"Status": "running",

"Running": true,

"Paused": false,

"Restarting": false,

"OOMKilled": false,

"Dead": false,

"Pid": 28041,

"ExitCode": 0,

"Error": "",

"StartedAt": "2021-08-04T20:11:43.947511209Z",

"FinishedAt": "0001-01-01T00:00:00Z"

}

"Name": "/mywordpress",

......

容器启停

1
2
3
4
5
shell复制代码docker start name

docker stop name

docker restart name

删除容器

1
2
3
shell复制代码# 删除时容器需要处于停止状态

docker rm name

查看日志

1
2
shell复制代码# docker ps -a
docker logs container_Id

占用资源

1
shell复制代码docker stats name

镜像特性

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,他包含运行某个软件所需的所有内容,包括代码、运行时库、环境变量和配置文件。将所有的应用和环境直接打包为docker镜像,就可以直接运行。

镜像名和版本号共同组成唯一标识,默认是最新版——lastest

分层原理

Docker的镜像通过联合文件系统将将各层文件系统叠加在一起。

引导方式

  • bootfs:传统操作系统,用于系统引导的文件系统,包括BootLoader和kernel,容器启动完成后会被卸载以节省内存资源。

  • rootfs:位于bootfs之上,表现为docker容器的根文件系统
    • 传统模式中,系统启动时,内核首先挂载为”只读“模式,完成全部自检后挂载为”读写“模式。
    • docker中,rootfs由内核挂载为”只读“模式,而后通过UFS技术挂载一个”可写“层。

⚠️注意:已有的分层只能读不能写,上层镜像优先级大于底层镜像

当我们使用pull命令时,我们可以看到docker的镜像是一层一层的在下载。这样做最大的好处就是资源共享了。

比如多个镜像都从Base镜像构建而来,那么宿主机只需要在磁盘上保留一份base镜像,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。查看镜像分层的方式可以通过docker image inspect命令。

所有的Docker镜像都起始于一个基础镜像,当进行修改或者增加新的内容时,就会在当前的镜像层之上,创建新的镜像层。在添加额外的镜像层的同时,镜像始终保持当前所有镜像的组合,
Docker通过存储引擎的方式来实现镜像层堆栈,并保证多镜像层对外展示为统一的文件系统。

UFS(联合文件系统)

UFS时一种分层、轻量级并且高性能的文件系统。

它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。

UnionFS是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像,可以制作各种具体的应用镜像。一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件,系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。

加载原理

Linux刚启动时会加载bootfs文件系统,在Docker镜像的最底层时bootfs。

当boot加载完成后整个内核就在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。rootfs在bootfs之上,rootfs包含的就是典型Linux系统中的/dev、/proc、/bin、/etc等目录和文件。rootfs就是各种不同的操作系统发行版。

Docker File

仓库没有的镜像怎么办?

可以自己创建镜像吗?

容器->镜像

1
shell复制代码docker commit CID -t xx.xx.xx

⚠️⚠️⚠️⚠️ 工作在前台的守护进程至少一个

网易蜂巢:开源镜像仓库

编写docker file

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

指令讲解

FROM

指定基础镜像,必须为第一个命令,有且只有一个

1
2
3
4
dockerfile复制代码# FROM <image>
# FROM <image>:<tag>
# FROM <image>@<digest>
FROM mysql:5.6

MAINTAINER

创建者信息

1
2
dockerfile复制代码# MAINTAINER <name>
MAINTAINER yitiao

RUN

用于在镜像容器中执行命令,其有以下两种命令执行方式:

1
2
3
4
5
6
dockerfile复制代码#shell执行
# RUN <command>
#exec执行
# RUN ["executable", "param1", "param2"]
RUN apk update
RUN ["/etc/execfile", "arg1", "arg1"]

ADD

将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget

1
2
dockerfile复制代码# ADD <src>... <dest>
ADD hom?.txt /mydir/ # ? 替代一个单字符,例如:"home.txt"

COPY

功能类似ADD,但是是不会自动解压文件,也不能访问网络资源

CMD

构建容器后调用,也就是在容器启动时才进行调用。

1
2
3
dockerfile复制代码# CMD command param1 param2 (执行shell内部命令)
CMD echo "This is a test." | wc -
#CMD不同于RUN,CMD用于指定在容器启动时所要执行的命令,而RUN用于指定镜像构建时所要执行的命令

ENTRYPOINT

配置容器,使其可执行化。配合CMD可省去”application”,只使用参数。

1
2
3
4
dockerfile复制代码# ENTRYPOINT ["executable", "param1", "param2"] (可执行文件, 优先)
# ENTRYPOINT command param1 param2 (shell内部命令)FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

LABEL

用于为镜像添加元数据

1
2
dockerfile复制代码# LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL version="1.0" description="一条coding" by="一条"

ENV

设置环境变量

1
2
3
4
5
6
7
dockerfile复制代码# ENV <key> <value>  
# <key>之后的所有内容均会被视为其<value>的组成部分,因此,一次只能设置一个变量
# ENV <key>=<value> ...
# 可以设置多个变量,每个变量为一个"<key>=<value>"的键值对
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat=fluffy

EXPOSE

指定于外界交互的端口

1
2
3
4
5
6
7
8
dockerfile复制代码格式:
EXPOSE <port> [<port>...]
示例:
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp
注:
  EXPOSE并不会让容器的端口访问到主机。要使其可访问,需要在docker run运行容器时通过-p来发布这些端口,或通过-P参数来发布EXPOSE导出的所有端口

VOLUME

用于指定持久化目录

1
2
3
4
5
6
7
8
9
10
11
12
dockerfile复制代码格式:
VOLUME ["/path/to/dir"]
示例:
VOLUME ["/data"]
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"
注:
  一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:
1 卷可以容器间共享和重用
2 容器并不一定要和其它容器共享卷
3 修改卷后会立即生效
4 对卷的修改不会对镜像产生影响
5 卷会一直存在,直到没有任何容器在使用它

WORKDIR

工作目录,类似于cd命令

1
2
3
4
5
dockerfile复制代码# WORKDIR /path/to/workdir
WORKDIR /a #(这时工作目录为/a)
WORKDIR b #(这时工作目录为/a/b)
WORKDIR c #(这时工作目录为/a/b/c)
#通过WORKDIR设置工作目录后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT、ADD、COPY等命令都会在#该目录下执行。在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

USER

指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户。

使用USER指定用户后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。镜像构建完成后,通过docker run运行容器时,可以通过-u参数来覆盖所指定的用户。

1
2
3
4
5
dockerfile复制代码# USER user
# USER user:group
# USER uid
# USER uid:gid
USER www

ARG

用于指定传递给构建运行时的变量

1
2
3
dockerfile复制代码# ARG <name>[=<default value>]
ARG site
ARG build_user=www

ONBUILD器

用于设置镜像触发

1
2
3
4
dockerfile复制代码# ONBUILD [INSTRUCTION]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
#当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发

一图理解,yyds

图片源于网络

docker file demo

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
dockerfile复制代码# 一条coding
# Version 1.0

# Base images 基础镜像
FROM centos

#MAINTAINER 维护者信息
MAINTAINER tianfeiyu

#ENV 设置环境变量
ENV PATH /usr/local/nginx/sbin:$PATH

#ADD 文件放在当前目录下,拷过去会自动解压
ADD nginx-1.8.0.tar.gz /usr/local/
ADD epel-release-latest-7.noarch.rpm /usr/local/

#RUN 执行以下命令
RUN rpm -ivh /usr/local/epel-release-latest-7.noarch.rpm
RUN yum install -y wget lftp gcc gcc-c++ make openssl-devel pcre-devel pcre && yum clean all
RUN useradd -s /sbin/nologin -M www

#WORKDIR 相当于cd
WORKDIR /usr/local/nginx-1.8.0

RUN ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-pcre && make && make install

RUN echo "daemon off;" >> /etc/nginx.conf

#EXPOSE 映射端口
EXPOSE 80

#CMD 运行以下命令
CMD ["nginx"]

网络通信

docker是如何与内部和外部进行数据交换的?

  • 容器内部
  • 内部访问外部
  • 外部访问内部

网络知识补充

eth0

eth0 物理网卡是指服务器上实际的网络接口设备。设备用于接收以太网数据接口,数据包在各个节点中转发和路由。

veth

veth 顾名思义,veth-pair 是一对的虚拟设备接口,它都是成对出现的。

一端连着协议栈,一端彼此相连着。一个设备从协议栈读取数据后,会将数据发送到另一个设备上去。

正因为有这个特性,它常常充当着一个桥梁,连接着各种虚拟网络设备,典型的例子像“两个 namespace 之间的连接”,“Bridge、OVS 之间的连接”,“Docker 容器之间的连接” 等等,以此构建出非常复杂的虚拟网络结构,比如 OpenStack Neutron。

bridge
Bridge 设备是一种纯软件实现的虚拟交换机,可以实现交换机的二层转发。与现实世界中的交换机功能相似。

与其他虚拟网络设备一样,可以配置 IP、MAC。Bridge 的主要功能是在多个接入 Bridge 的网络接口间转发数据包。

网络模型

我们在使用docker run创建Docker容器时,可以用--net选项指定容器的网络模式,Docker有以下4种网络模式:

  • host模式,使用–net=host指定。
  • container模式,使用–net=container:NAME_or_ID指定。
  • none模式,使用–net=none指定。
  • bridge模式,使用–net=bridge指定,默认设置。

除这四种基本的之外,还支持各种自定义模型。

容器内部访问

通常情况下,docker使用网桥+NAT的方式进行通信。Bridge 模式会为容器创建独立的网络 namespace ,拥有独立的网卡等网格栈。

NAT:可以理解为网卡

Dcoker0:就是网桥,交换机,ifconfig可见

同一宿主机上,Bridge 模式创建的容器会通过 DHCP 链接到 docker0 上,通过 docker0 实现网络的互通。「容器之间都是连接掉docker0这个网桥上的,它作为虚拟交换机使容器可以相互通信」。

内外部通信

宿主机的 IP 地址与容器 veth pair 的 IP 地址不在同一个网段,宿主机外的网络无法主动发现容器的存在,不能直接进行容器通信。所以 Docker 提供了端口映射的方式,就是将宿主机上的端口流量映射转发到容器内的端口上。


ok,至此docker的全部知识总结完成,作为java开发,掌握这些足以让你如鱼得水。

可以点赞啦!

最后

为了回馈各位粉丝,礼尚往来,给大家准备了一条多年积累下来的优质资源,包括学习路线、面试资料、100本电子书等。领取

本文转载自: 掘金

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

信不信,90%程序猿都不会正确使用分布式锁

发表于 2021-11-03

大家好,我是 沐子。

分布式锁的话题,很多文章已经写烂了,我为什么还要写这篇文章呢?

因为我发现网上 90% 的文章,并没有把这个问题真正讲清楚。导致很多读者看了很多文章,依旧云里雾里。例如下面这些问题,你能清晰地回答上来吗?

  • 数据库通过乐观锁怎么实现分布式锁?
  • 基于 Redis 如何实现一个分布式锁?
  • Redis 如何避免死锁?
  • Redis 如何合理的设置超时时间?
  • Zookeeper如何规避羊群效应?
  • 三种分布式锁的优缺点分别是什么?

这篇文章,我就来把这些问题彻底讲清楚。

读完这篇文章,你不仅可以彻底了解分布式锁,还会对「分布式系统」有更加深刻的理解。

图片.png

一、 为什么需要分布式锁

为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

二、 常见的分布式锁方案

我们在使用分布式锁的时候,大部分同学可能都忽略了一点,那就是分布式锁经常出现哪些问题,以及如何解决。

可用问题:无论何时都要保证锁服务的可用性(这是系统正常执行锁操作的基础)。

死锁问题:客户端一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达(这是避免死锁的设计原则)。

脑裂问题:集群同步时产生的数据不一致,导致新的进程有可能拿到锁,但之前的进程以为自己还有锁,那么就出现两个进程拿到了同一个锁的问题。

总的来说,设计分布式锁服务,至少要解决上面最核心的几个问题,才能评估锁的优劣,一般分布式锁有三种常见的实现方式:

1. 数据库乐观锁;
2. 基于分布式缓存Redis的分布式锁;
3. 基于ZooKeeper的分布式锁

1. 基于关系型数据库实现分布式锁

1)基于悲观锁的方式实现分布式锁

基于关系型数据库(如 MySQL) 来实现分布式锁是任何阶段的研发同学都需要掌握的,做法如下:先查询数据库是否存在记录,为了防止幻读取(幻读取:事务 A 按照一定条件进行数据读取,这期间事务 B 插入了相同搜索条件的新数据,事务 A 再次按照原先条件进行读取时,发现了事务 B 新插入的数据 )通过数据库行锁 select for update 锁住这行数据,然后将查询和插入的 SQL 在同一个事务中提交。以订单表为例:

select id from order where order_id = xxx for update

基于关系型数据库实现分布式锁比较简单,不过你要注意,基于 MySQL 行锁的方式会出现交叉死锁,比如事务 1 和事务 2 分别取得了记录 1 和记录 2 的排它锁,然后事务 1 又要取得记录 2 的排它锁,事务 2 也要获取记录 1 的排它锁,那这两个事务就会因为相互锁等待,产生死锁。

图片

当然,你可以通过“超时控制”解决交叉死锁的问题,但在高并发情况下,出现的大部分请求都会排队等待,所以 “基于关系型数据库实现分布式锁”的方式在性能上存在缺陷。

2)基于乐观锁的方式实现分布式锁

在数据库层面,select for update 是悲观锁,会一直阻塞直到事务提交,所以为了不产生锁等待而消耗资源,你可以基于乐观锁的方式来实现分布式锁,比如基于版本号的方式,首先在数据库增加一个 int 型字段 ver,然后在 SELECT 同时获取 ver 值,最后在 UPDATE 的时候检查 ver 值是否为与第 2 步或得到的版本值相同。

// SELECT 同时获取 ver 值

select amount, old_ver from order where order_id = xxx

// UPDATE 的时候检查 ver 值是否与第 2 步获取到的值相同

update order set ver = old_ver + 1, amount = yyy where order_id = xxx and ver = old_ver

此时,如果更新结果的记录数为1,就表示成功,如果更新结果的记录数为 0,就表示已经被其他应用更新过了,需要做异常处理。

这个策略源于 mysql 的 mvcc 机制,使用这个策略其实本身没有什么问题,主要的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 sql 每次进行判断,增加了数据库操作的次数,在高并发的要求下,对数据库连接的开销也是无法忍受的。

2. 基于分布式缓存Redis实现分布式锁

因为数据库的性能限制了业务的并发量,所以针对“ 618 和双 11 大促”等请求量剧增的场景,需要引入基于缓存的分布式锁,这个方案可以避免大量请求直接访问数据库,提高系统的响应能力。基于缓存实现的分布式锁,就是将数据仅存放在系统的内存中,不写入磁盘,从而减少 I/O 读写。接下来,我以 Redis 为例讲解如何实现分布式锁。

我们从最简单的开始讲起。

想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX 命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。

两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。

客户端 1 申请加锁,加锁成功:

1
2
csharp复制代码127.0.0.1:6379> SETNX lock 1
(integer) 1     // 客户端1,加锁成功

客户端 2 申请加锁,因为它后到达,加锁失败:

1
2
csharp复制代码127.0.0.1:6379> SETNX lock 1
(integer) 0     // 客户端2,加锁失败

此时,加锁成功的客户端,就可以去操作「共享资源」,例如,修改 MySQL 的某一行数据,或者调用一个 API 请求。

操作完成后,还要及时释放锁,给后来者让出操作共享资源的机会。如何释放锁呢?

也很简单,直接使用 DEL 命令删除这个 key 即可:

1
2
csharp复制代码127.0.0.1:6379> DEL lock // 释放锁
(integer) 1

这个逻辑非常简单,整体的路程就是这样:

图片

但是,它存在一个很大的问题,当客户端 1 拿到锁后,如果发生下面的场景,就会造成「死锁」:

  1. 程序处理业务逻辑异常,没及时释放锁
  2. 进程挂了,没机会释放锁

这时,这个客户端就会一直占用这个锁,而其它客户端就「永远」拿不到这把锁了。怎么解决这个问题呢?

如何避免死锁?

我们很容易想到的方案是在 Redis 中实现时,就是给这个 key 设置一个「过期时间」。这里我们假设,操作共享资源的时间不会超过 10s,那么在加锁时,给这个 key 设置 10s 过期即可:

1
2
3
4
csharp复制代码127.0.0.1:6379> SETNX lock 1    // 加锁
(integer) 1
127.0.0.1:6379> EXPIRE lock 10  // 10s后自动过期
(integer) 1

这样一来,无论客户端是否异常,这个锁都可以在 10s 后被「自动释放」,其它客户端依旧可以拿到锁。

但这样真的没问题吗?

还是有问题。

现在的操作,加锁、设置过期是 2 条命令,有没有可能只执行了第一条,第二条却「来不及」执行的情况发生呢?例如:

  1. SETNX 执行成功,执行 EXPIRE 时由于网络问题,执行失败
  2. SETNX 执行成功,Redis 异常宕机,EXPIRE 没有机会执行
  3. SETNX 执行成功,客户端异常崩溃,EXPIRE 也没有机会执行

总之,这两条命令不能保证是原子操作(一起成功),就有潜在的风险导致过期时间设置失败,依旧发生「死锁」问题。

怎么办?

在 Redis 2.6.12 版本之前,我们需要想尽办法,保证 SETNX 和 EXPIRE 原子性执行,还要考虑各种异常情况如何处理。

但在 Redis 2.6.12 之后,Redis 扩展了 SET 命令的参数,用这一条命令就可以了:

1
2
3
csharp复制代码// 一条命令保证原子性执行
127.0.0.1:6379> SET lock 1 EX 10 NX
OK

这样就解决了死锁问题,也比较简单。

我们再来看分析下,它还有什么问题?

试想这样一种场景:

  1. 客户端 1 加锁成功,开始操作共享资源
  2. 客户端 1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」
  3. 客户端 2 加锁成功,开始操作共享资源
  4. 客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)

看到了么,这里存在两个严重的问题:

  1. 锁过期:客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
  2. 释放别人的锁:客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁

导致这两个问题的原因是什么?我们一个个来看。

第一个问题,可能是我们评估操作共享资源的时间不准确导致的。

例如,操作共享资源的时间「最慢」可能需要 15s,而我们却只设置了 10s 过期,那这就存在锁提前过期的风险。

过期时间太短,那增大冗余时间,例如设置过期时间为 20s,这样总可以了吧?

这样确实可以「缓解」这个问题,降低出问题的概率,但依旧无法「彻底解决」问题。

为什么?

原因在于,客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。

既然是「预估」时间,也只能是大致计算,除非你能预料并覆盖到所有导致耗时变长的场景,但这其实很难。

有什么更好的解决方案吗?

别急,关于这个问题,我会在后面详细来讲对应的解决方案。

我们继续来看第二个问题。

第二个问题在于,一个客户端释放了其它客户端持有的锁。

想一下,导致这个问题的关键点在哪?

重点在于,每个客户端在释放锁时,都是「无脑」操作,并没有检查这把锁是否还「归自己持有」,所以就会发生释放别人锁的风险,这样的解锁流程,很不「严谨」!

如何解决这个问题呢?

有一个更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key。但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放

if redis.call(“get”,KEYS[1]) == ARGV[1] then

return redis.call("del",KEYS[1])

else

return0

end

以上,就是基于 Redis 的 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁、解锁,不过在实际面试中,你不能仅停留在操作上,因为这并不能满足应对面试需要掌握的知识深度, 所以你还要清楚基于 Redis 实现分布式锁的优缺点;Redis 的超时时间设置问题;站在架构设计层面上 Redis 怎么解决集群情况下分布式锁的可靠性问题。需要注意的是,你不用一股脑全部将其说出来,而是要做好准备,以便跟上面试官的思路,同频沟通。

基于 Redis 实现分布式锁的优缺点

基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁主要的优点主要有三点。

a. 性能高效(这是选择缓存实现分布式锁最核心的出发点)。

b. 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。但是需要注意的是,在 Redis2.6.12 的之前的版本中,由于加锁命令和设置锁过期时间命令是两个操作(不是原子性的),当出现某个线程操作完成 setnx 之后,还没有来得及设置过期时间,线程就挂掉了,就会导致当前线程设置 key 一直存在,后续的线程无法获取锁,最终造成死锁的问题,所以要选型 Redis 2.6.12 后的版本或通过 Lua 脚本执行加锁和设置超时时间(Redis 允许将 Lua 脚本传到 Redis 服务器中执行, 脚本中可以调用多条 Redis 命令,并且 Redis 保证脚本的原子性)。

c. 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。
当然,基于 Redis 实现分布式锁也存在缺点,主要是不合理设置超时时间,以及 Redis 集群的数据同步机制,都会导致分布式锁的不可靠性。

如何合理设置超时时间

通过超时时间来控制锁的失效时间,不太靠谱,比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,后续线程 B 又意外的持有了锁,当线程 A 再次恢复后,通过 del 命令释放锁,就错误的将线程 B 中同样 key 的锁误删除了。

图片

所以,如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短,有可能业务阻塞没有处理完成,能否合理设置超时时间,是基于缓存实现分布式锁很难解决的一个问题。

那么如何合理设置超时时间呢? 你可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。不过这种方式实现起来相对复杂,我建议你结合业务场景进行回答,所以针对超时时间的设置,要站在实际的业务场景中进行衡量。

Redis 如何解决集群情况下分布式锁的可靠性?

由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。其实 Redis 官方已经设计了一个分布式锁算法 Redlock 解决了这个问题。而如果你能基于 Redlock 原理回答出怎么解决 Redis 集群节点实现分布式锁的问题,会成为面试的加分项。那官方是怎么解决的呢?

为了避免 Redis 实例故障导致锁无法工作的问题,Redis 的开发者 Antirez 设计了分布式锁算法 Redlock,引入该算法后即使有某个 Redis 实例发生故障,因为锁的数据在其他实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。那 Redlock 算法是如何做到的呢?我们假设目前有 N 个独立的 Redis 实例, 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,但是需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况。当客户端完成了和所有 Redis 实例的加锁操作之后,如果有超过半数的 Redis 实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。

3. 基于Zookeeper实现分布式锁

在介绍 ZooKeeper 分布式锁前需要先了解一下 ZooKeeper 中节点(Znode),ZooKeeper 的数据存储数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个 Znode(如 /locks/my_lock)。每个 Znode 上都会保存自己的数据内容,同时还会保存一系列属性信息。Znode 又分为以下四种类型:

图片.png

Zookeeper实现分布锁的大致思想为:每个客户端对某个方法加锁时,在 Zookeeper 上与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

1)排它锁

  排他锁,又称写锁或独占锁。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取或更新操作,其他任务事务都不能对这个数据对象进行任何操作,直到T1释放了排他锁。

  排他锁核心是保证当前有且仅有一个事务获得锁,并且锁释放之后,所有正在等待获取锁的事务都能够被通知到。

  Zookeeper 的强一致性特性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。可以利用Zookeeper这个特性,实现排他锁。

  1️⃣定义锁:通过Zookeeper上的数据节点来表示一个锁

2️⃣获取锁:客户端通过调用 create 方法创建表示锁的临时节点,可以认为创建成功的客户端获得了锁,同时可以让没有获得锁的节点在该节点上注册Watcher监听,以便实时监听到lock节点的变更情况

3️⃣释放锁:以下两种情况都可以让锁释放

当前获得锁的客户端发生宕机或异常,那么Zookeeper上这个临时节点就会被删除

正常执行完业务逻辑,客户端主动删除自己创建的临时节点

基于Zookeeper实现排他锁流程:

          图片

2)共享锁

  共享锁,又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。

  共享锁与排他锁的区别在于,加了排他锁之后,数据对象只对当前事务可见,而加了共享锁之后,数据对象对所有事务都可见。

  1️⃣定义锁:通过Zookeeper上的数据节点来表示一个锁,是一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点

2️⃣获取锁:客户端通过调用 create 方法创建表示锁的临时顺序节点,如果是读请求,则创建 /lockpath/[hostname]-R-序号 节点,如果是写请求则创建 /lockpath/[hostname]-W-序号节点

3️⃣判断读写顺序:大概分为4个步骤

1)创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听

2)确定自己的节点序号在所有子节点中的顺序

3.1)对于读请求:1. 如果没有比自己序号更小的子节点,或者比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑 2. 如果有比自己序号小的子节点有写请求,那么等待

3.2)对于写请求,如果自己不是序号最小的节点,那么等待

4)接收到Watcher通知后,重复步骤1)

4️⃣释放锁:与排他锁逻辑一致

            图片

  基于Zookeeper实现共享锁流程:

            图片

3)羊群效应

  在实现共享锁的 “判断读写顺序” 的第1个步骤是:创建完节点后,获取 /lockpath 节点下的所有子节点,并对该节点注册子节点变更的Watcher监听。这样的话,任何一次客户端移除共享锁之后,Zookeeper将会发送子节点变更的Watcher通知给所有机器,系统中将有大量的 “Watcher通知” 和 “子节点列表获取” 这个操作重复执行,然后所有节点再判断自己是否是序号最小的节点(写请求)或者判断比自己序号小的子节点是否都是读请求(读请求),从而继续等待下一次通知。

  然而,这些重复操作很多都是 “无用的”,实际上每个锁竞争者只需要关注序号比自己小的那个节点是否存在即可。

  当集群规模比较大时,这些 “无用的” 操作不仅会对Zookeeper造成巨大的性能影响和网络冲击,更为严重的是,如果同一时间有多个客户端释放了共享锁,Zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知–这就是所谓的 “羊群效应”。

  改进后的分布式锁实现:

  1️⃣客户端调用 create 方法创建一个类似于 /lockpath/[hostname]-请求类型-序号 的临时顺序节点。

  2️⃣客户端调用 getChildren 方法获取所有已经创建的子节点列表(这里不注册任何Watcher)。

  3️⃣如果无法获取任何共享锁,那么调用 exist 来对比自己小的那个节点注册Watcher

读请求:向比自己序号小的最后一个写请求节点注册Watcher监听

写请求:向比自己序号小的最后一个节点注册Watcher监听

4️⃣等待Watcher监听,继续进入步骤2️⃣

Zookeeper羊群效应改进前后Watcher监听图:

            图片

三、 三种分布式锁对比

1. 数据库分布式锁实现的优点及缺点

a. 优点:

理解起来简单,不需要维护额外的第三方中间件(比如Redis,Zk)

b. 缺点:

1.db操作性能较差,并且有锁表的风险

2.非阻塞操作失败后,需要轮询,占用cpu资源;

3.长时间不commit或者长时间轮询,可能会占用较多连接资源

2. Redis(缓存)分布式锁实现的优点及 缺点

a. 优点:

对于Redis实现简单,性能对比ZK和Mysql较好。如果不需要特别复杂的要求,那么自己就可以利用setNx进行实现,如果自己需要复杂的需求的话那么可以利用或者借鉴Redission。对于一些要求比较严格的场景来说的话可以使用RedLock。

b. 缺点:

1.锁删除失败 过期时间不好控制

2.非阻塞,操作失败后,需要轮询,占用cpu资源;


3. ZK分布式锁实现的优点及 缺点

a.优点:

ZK可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,并且支持读写锁,ZK获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用ZK集群进行保证。

b.缺点:
性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。

根据性能、可靠性、实现的复杂性和理解的难易程度等方面,对上面的三种方案总结如下:

图片.png

总之上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

如果文章对你有用或有所启发,欢迎关注公众号:首席架构师专栏。最后, Redlock解决Redis 集群节点实现分布式锁的问题,但是RedLock真的安全吗?欢迎读者们在评论区留言,一起探讨学习。

本文转载自: 掘金

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

Linux wget 命令详解

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战。

wget命令是Linux系统用于从Web下载文件的命令行工具,支持 HTTP、HTTPS及FTP协议下载文件,而且wget还提供了很多选项,例如下载多个文件、后台下载,使用代理等等,使用非常方便。

接下来就介绍一下wget的使用方法。

wget命令的使用

语法格式

1
shell复制代码wget [options] [url]

例如,使用wget下载redis的tar.gz文件:

1
shell复制代码wget https://download.redis.io/releases/redis-6.0.8.tar.gz

该命令会下载文件到当前工作目录中,在下载过程中,会显示进度条、文件大小、下载速度等。

接下来介绍几个常用的选项参数。

使用 -O 选项以其他名称保存下载的文件

要以其他名称保存下载的文件,使用-O选项,后跟指定名称即可:

1
shell复制代码wget -O redis.tar.gz https://download.redis.io/releases/redis-6.0.8.tar.gz

使用 -P 选项将文件下载到指定目录

默认情况下,wget将下载的文件保存在当前工作目录中,使用-P选项可以将文件保存到指定目录下,例如,下面将将文件下载到/usr/software目录下:

1
shell复制代码wget -P /usr/software https://download.redis.io/releases/redis-6.0.8.tar.gz

使用 -c 选项断点续传

当我们下载一个大文件时,如果中途网络断开导致没有下载完成,我们就可以使用命令的-c选项恢复下载,让下载从断点续传,无需从头下载。

1
shell复制代码wget -c https://download.redis.io/releases/redis-6.0.8.tar.gz

使用 -b 选项在后台下载

我们可以使用-b选项在后台下载文件:

1
shell复制代码wget -b https://download.redis.io/releases/redis-6.0.8.tar.gz

默认情况下,下载过程日志重定向到当前目录中的wget-log文件中,要查看下载状态,可以使用tail -f wget-log查看。

使用 -i 选项下载多个文件

如果先要一次下载多个文件,首先需要创建一个文本文件,并将所有的url添加到该文件中,每个url都必须是单独的一行。

1
shell复制代码vim download_list.txt

然后使用-i选项,后跟该文本文件:

1
shell复制代码wget -i download_list.txt

使用 –limit-rate 选项限制下载速度

默认情况下,wget命令会以全速下载,但是有时下载一个非常大的资源的话,可能会占用大量的可用带宽,影响其他使用网络的任务,这时就要限制下载速度,可以使用--limit-rate选项。
例如,以下命令将下载速度限制为1m/s:

1
shell复制代码wget --limit-rate=1m https://download.redis.io/releases/redis-6.0.8.tar.gz

使用 -U 选项设定代理下载

如果远程服务器阻止wget下载资源,我们可以通过-U选项模拟浏览器进行下载,例如下面模拟谷歌浏览器下载。

1
shell复制代码wget -U 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.43 Safari/537.36' https://download.redis.io/releases/redis-6.0.8.tar.gz

使用 –tries 选项增加重试次数

如果网络有问题或下载一个大文件有可能会下载失败,wget默认重试20次,我们可以使用-tries选项来增加重试次数。

1
shell复制代码wget --tries=40 https://download.redis.io/releases/redis-6.0.8.tar.gz

通过FTP下载

如果要从受密码保护的FTP服务器下载文件,需要指定用户名和密码,格式如下:

1
shell复制代码wget --ftp-user=<username> --ftp-password=<password> url

除此之外,wget还有很多可用的选项,这里不一一列举了,总之wget是一个非常好用的工具。

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

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

1…429430431…956

开发者博客

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