数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

Hi,大家好,我是明哥。
在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。 我的在线博客: http://golang.iswbm.com 我的 Github:github.com/iswbm/GolangCodingTime
刚接触 Go 语言的信道的时候,经常会遇到死锁的错误,而导致这个错误的原因有很多种,这里整理了几种常见的。 fatal error: all goroutines are asleep - deadlock!
错误示例一
看下面这段代码 package main import "fmt" func main() { pipline := make(chan string) pipline <- "hello world" fmt.Println(<-pipline) }
运行会抛出错误,如下 fatal error: all goroutines are asleep - deadlock!
看起来好像没有什么问题?先往信道中存入数据,再从信道中读取数据。
回顾前面的基础,我们知道使用 make 创建信道的时候,若不传递第二个参数,则你定义的是无缓冲信道,而对于无缓冲信道,在接收者未准备好之前,发送操作是阻塞的.
因此,对于解决此问题有两种方法: 使接收者代码在发送者之前执行 使用缓冲信道,而不使用无缓冲信道
第一种方法 :
若要程序正常执行,需要保证接收者程序在发送数据到信道前就进行阻塞状态,修改代码如下 package main import "fmt" func main() { pipline := make(chan string) fmt.Println(<-pipline) pipline <- "hello world" }
运行的时候还是报同样的错误。问题出在哪里呢?
原来我们将发送者和接收者写在了同一协程中,虽然保证了接收者代码在发送者之前执行,但是由于前面接收者一直在等待数据 而处于阻塞状态,所以无法执行到后面的发送数据。还是一样造成了死锁。
有了前面的经验,我们将接收者代码写在另一个协程里,并保证在发送者之前执行,就像这样的代码 package main func hello(pipline chan string) { <-pipline } func main() { pipline := make(chan string) go hello(pipline) pipline <- "hello world" }
运行之后 ,一切正常。
第二种方法 :
接收者代码必须在发送者代码之前 执行,这是针对无缓冲信道才有的约束。
既然这样,我们改使用可缓冲信道不就OK了吗? package main import "fmt" func main() { pipline := make(chan string, 1) pipline <- "hello world" fmt.Println(<-pipline) }
运行之后,一切正常。
错误示例二
每个缓冲信道,都有容量,当信道里的数据量等于信道的容量后,此时再往信道里发送数据,就失造成阻塞,必须等到有人从信道中消费数据后,程序才会往下进行。
比如这段代码,信道容量为 1,但是往信道中写入两条数据,对于一个协程来说就会造成死锁。 package main import "fmt" func main() { ch1 := make(chan string, 1) ch1 <- "hello world" ch1 <- "hello China" fmt.Println(<-ch1) }
错误示例三
当程序一直在等待从信道里读取数据,而此时并没有人会往信道中写入数据。此时程序就会陷入死循环,造成死锁。
比如这段代码,for 循环接收了两次消息("hello world"和“hello China”)后,再也没有人发送数据了,接收者就会处于一个等待永远接收不到数据的囧境。陷入死循环,造成死锁。 package main import "fmt" func main() { pipline := make(chan string) go func() { pipline <- "hello world" pipline <- "hello China" // close(pipline) }() for data := range pipline{ fmt.Println(data) } }
包子铺里的包子已经卖完了,可还有人在排队等着买,如果不再做包子,就要告诉排队的人:不用等了,今天的包子已经卖完了,明日请早呀。
不能让人家死等呀,不跟客人说明一下,人家还以为你们店后面还在蒸包子呢。
所以这个问题,解决方法很简单,只要在发送完数据后,手动关闭信道,告诉 range 信道已经关闭,无需等待就行。 package main import "fmt" func main() { pipline := make(chan string) go func() { pipline <- "hello world" pipline <- "hello China" close(pipline) }() for data := range pipline{ fmt.Println(data) } }
系列导读
01. 开发环境的搭建(Goland & VS Code)
02. 学习五种变量创建的方法
**03. 详解数据类型:** 整形与浮点型
04. 详解数据类型:byte、rune与string
05. 详解数据类型:数组与切片
06. 详解数据类型:字典与布尔类型
07. 详解数据类型:指针
08. 面向对象编程:结构体与继承
09. 一篇文章理解 Go 里的函数
10. Go语言流程控制:if-else 条件语句
11. Go语言流程控制:switch-case 选择语句
12. Go语言流程控制:for 循环语句
13. Go语言流程控制:goto 无条件跳转
14. Go语言流程控制:defer 延迟调用
15. 面向对象编程:接口与多态
16. 关键字:make 和 new 的区别?
17. 一篇文章理解 Go 里的语句块与作用域
18. 学习 Go 协程:goroutine
19. 学习 Go 协程:详解信道/通道
20. 几个信道死锁经典错误案例详解
21. 学习 Go 协程:WaitGroup
22. 学习 Go 协程:互斥锁和读写锁
23. Go 里的异常处理:panic 和 recover
24. 超详细解读 Go Modules 前世今生及入门使用
25. Go 语言中关于包导入必学的 8 个知识点
26. 如何开源自己写的模块给别人用?
27. 说说 Go 语言中的类型断言?
28. 这五点带你理解Go语言的select用法
软件开发
2020-06-03 08:10:00
1.首先导入使用Maven导入jar包
org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 1.2.62

2.在application.properties配置信息
# Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password=123456 # 连接池最大连接数(使用负值表示没有限制) spring.redis.pool.max-active=200 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.pool.max-idle=10 # 连接池中的最小空闲连接 spring.redis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=1000ms

3.编写Redis工具类
@Configuration @ConditionalOnClass(RedisOperations.class) //系统中有RedisOperations类时 @EnableConfigurationProperties(RedisProperties.class) //启动RedisProperties这个类 @EnableCaching // www.1b23.com public class RedisConfig extends CachingConfigurerSupport { @Autowired RedisTemplate redisTemplate; // 配置缓存管理器 @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { LettuceConnectionFactory jedisConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory(); jedisConnectionFactory.setDatabase(2); //指定dbindex redisTemplate.setConnectionFactory(jedisConnectionFactory); jedisConnectionFactory.resetConnection(); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(60*20)) // 20分钟缓存失效 // 设置key的序列化方式 // .entryTtl(Duration.ofSeconds(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置value的序列化方式 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new FastJsonRedisSerializer(Object.class))) // 不缓存null值 .disableCachingNullValues(); RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .transactionAware() .build(); return redisCacheManager; } }
package com.FireService.config; import java.nio.charset.Charset; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; public class FastJsonRedisSerializer implements RedisSerializer { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class clazz; static { ParserConfig.getGlobalInstance().addAccept("com.FireService"); } public FastJsonRedisSerializer(Class clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (null == t) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return (T) JSON.parseObject(str, clazz); } }

4.SpringBoot有关缓存的几个注解
@Cacheable:查询
可选属性:
cacheNames/value:指定缓存组件的名字;
key:缓存数据使用的key,可以用来指定。默认即使用方法参数的值
keyGenerator:key的生成器,可以自己指定key的生成器的组件id
//自定义配置类配置keyGenerator @Configuration public class MyCacheConfig { @Bean("myKeyGenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { return method.getName()+"["+ Arrays.asList(params).toString() +"]"; } }; } }

cacheManager:指定缓存管理器;或者cacheResolver获取指定解析器
condition:指定符合条件的情况下才缓存;如condition="#id>0"
unless:否定缓存,当unless指定的条件为true,方法的返回值不会被缓存,可以获取到结果进行判断;如unless="#result==null";
sync:是否使用异步模式
例如:
@Cacheable(value = "RedisInfo", key = "#root.methodName+'['+#account+']'") @ResponseBody @RequestMapping("/RedisTest") public Result findUserOrder(String account) throws Exception{ if(account!=null) { List> list=orderFindGoods.findUserOrder(account); return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.desc()); }else { return Results.failure(); } }

运行项目查看结果
1.第一次访问

查看Druid连接信息

软件开发
2020-06-03 07:45:00
简介
你知道序列化可以使用代理吗?你知道序列化的安全性吗?每个java程序员都听说过序列化,要存储对象需要序列化,要在网络上传输对象要序列化,看起来很简单的序列化其实里面还隐藏着很多小秘密,今天本文将会为大家一一揭秘。
更多精彩内容且看: 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新 Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新 Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新 java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程 更多内容请访问 www.flydean.com
什么是序列化
序列化就是将java对象按照一定的顺序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的java对象。
所以序列化的目的就是为了传输对象,对于一些复杂的对象,我们可以使用第三方的优秀框架,比如Thrift,Protocol Buffer等,使用起来非常的方便。
JDK本身也提供了序列化的功能。要让一个对象可序列化,则可以实现java.io.Serializable接口。
java.io.Serializable是从JDK1.1开始就有的接口,它实际上是一个marker interface,因为java.io.Serializable并没有需要实现的接口。继承java.io.Serializable就表明这个class对象是可以被序列化的。 [@Data](https://my.oschina.net/difrik) @AllArgsConstructor public class CustUser implements java.io.Serializable{ private static final long serialVersionUID = -178469307574906636L; private String name; private String address; }
上面我们定义了一个CustUser可序列化对象。这个对象有两个属性:name和address。
接下看下怎么序列化和反序列化: public void testCusUser() throws IOException, ClassNotFoundException { CustUser custUserA=new CustUser("jack","www.flydean.com"); CustUser custUserB=new CustUser("mark","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); objectOutputStream.writeObject(custUserB); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUser custUser1 = (CustUser) objectInputStream.readObject(); CustUser custUser2 = (CustUser) objectInputStream.readObject(); log.info("{}",custUser1); log.info("{}",custUser2); } }
上面的例子中,我们实例化了两个CustUser对象,并使用objectOutputStream将对象写入文件中,最后使用ObjectInputStream从文件中读取对象。
上面是最基本的使用。需要注意的是CustUser class中有一个serialVersionUID字段。
serialVersionUID是序列化对象的唯一标记,如果class中定义的serialVersionUID和序列化存储中的serialVersionUID一致,则表明这两个对象是一个对象,我们可以将存储的对象反序列化。
如果我们没有显示的定义serialVersionUID,则JVM会自动根据class中的字段,方法等信息生成。很多时候我在看代码的时候,发现很多人都将serialVersionUID设置为1L,这样做是不对的,因为他们没有理解serialVersionUID的真正含义。
重构序列化对象
假如我们有一个序列化的对象正在使用了,但是突然我们发现这个对象好像少了一个字段,要把他加上去,可不可以加呢?加上去之后原序列化过的对象能不能转换成这个新的对象呢?
答案是肯定的,前提是两个版本的serialVersionUID必须一样。新加的字段在反序列化之后是空值。
序列化不是加密
有很多同学在使用序列化的过程中可能会这样想,序列化已经将对象变成了二进制文件,是不是说该对象已经被加密了呢?
这其实是序列化的一个误区,序列化并不是加密,因为即使你序列化了,还是能从序列化之后的数据中知道你的类的结构。比如在RMI远程调用的环境中,即使是class中的private字段也是可以从stream流中解析出来的。
如果我们想在序列化的时候对某些字段进行加密操作该怎么办呢?
这时候可以考虑在序列化对象中添加writeObject和readObject方法: private String name; private String address; private int age; private void writeObject(ObjectOutputStream stream) throws IOException { //给age加密 age = age + 2; log.info("age is {}", age); stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); log.info("age is {}", age); //给age解密 age = age - 2; }
上面的例子中,我们为CustUser添加了一个age对象,并在writeObject中对age进行了加密(加2),在readObject中对age进行了解密(减2)。
注意,writeObject和readObject都是private void的方法。他们的调用是通过反射来实现的。
使用真正的加密
上面的例子, 我们只是对age字段进行了加密,如果我们想对整个对象进行加密有没有什么好的处理办法呢?
JDK为我们提供了javax.crypto.SealedObject 和java.security.SignedObject来作为对序列化对象的封装。从而将整个序列化对象进行了加密。
还是举个例子: public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { CustUser custUserA=new CustUser("jack","www.flydean.com"); Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES"); IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes()); enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv); SealedObject sealedObject= new SealedObject(custUserA, enCipher); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(sealedObject); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); SealedObject custUser1 = (SealedObject) objectInputStream.readObject(); CustUser custUserV2= (CustUser) custUser1.getObject(deCipher); log.info("{}",custUserV2); } }
上面的例子中,我们构建了一个SealedObject对象和相应的加密解密算法。
SealedObject就像是一个代理,我们写入和读取的都是这个代理的加密对象。从而保证了在数据传输过程中的安全性。
使用代理
上面的SealedObject实际上就是一种代理,考虑这样一种情况,如果class中的字段比较多,而这些字段都可以从其中的某一个字段中自动生成,那么我们其实并不需要序列化所有的字段,我们只把那一个字段序列化就可以了,其他的字段可以从该字段衍生得到。
在这个案例中,我们就需要用到序列化对象的代理功能。
首先,序列化对象需要实现writeReplace方法,表示替换成真正想要写入的对象: public class CustUserV3 implements java.io.Serializable{ private String name; private String address; private Object writeReplace() throws java.io.ObjectStreamException { log.info("writeReplace {}",this); return new CustUserV3Proxy(this); } }
然后在Proxy对象中,需要实现readResolve方法,用于从系列化过的数据中重构序列化对象。如下所示: public class CustUserV3Proxy implements java.io.Serializable{ private String data; public CustUserV3Proxy(CustUserV3 custUserV3){ data =custUserV3.getName()+ "," + custUserV3.getAddress(); } private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); CustUserV3 result = new CustUserV3(pieces[0], pieces[1]); log.info("readResolve {}",result); return result; } }
我们看下怎么使用: public void testCusUserV3() throws IOException, ClassNotFoundException { CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject(); log.info("{}",custUser1); } }
注意,我们写入和读出的都是CustUserV3对象。
Serializable和Externalizable的区别
最后我们讲下Externalizable和Serializable的区别。Externalizable继承自Serializable,它需要实现两个方法: void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
什么时候需要用到writeExternal和readExternal呢?
使用Serializable,Java会自动为类的对象和字段进行对象序列化,可能会占用更多空间。而Externalizable则完全需要我们自己来控制如何写/读,比较麻烦,但是如果考虑性能的话,则可以使用Externalizable。
另外Serializable进行反序列化不需要执行构造函数。而Externalizable需要执行构造函数构造出对象,然后调用readExternal方法来填充对象。所以Externalizable的对象需要一个无参的构造函数。
总结
本文详细分析了序列化对象在多种情况下的使用,并讲解了Serializable和Externalizable的区别,希望大家能够喜欢。 本文作者:flydean程序那些事
本文链接: http://www.flydean.com/java-serialization/
本文来源:flydean的博客
欢迎关注我的公众号:程序那些事,更多精彩等着您!
软件开发
2020-06-03 06:19:00

$event代表当前事件对象,这个事件对象是默认传的,可以不传也行。 附上事件默认行为 百度一下 阻止事件冒泡

将一个html属性值变成表达式,可以用强制绑定,在属性前面加一个冒号即可
:src="imageUrl"

1. 双大括号表达式

{{ content }}

{{ content . toUpperCase ()}}

2. 指令一 : 强制数据绑定

访问指定站点
访问指定站点 2
访问指定站点 2

3. 指令二 : 绑定事件监听


Vue实例的方法以$符号开头,比如下面的写法
vm.$swtich(){}
停止事件冒泡,当事件外层还有事件可,可以停止外层事件
@Click.stop
阻止事件的默认行为
@Click.prevent
键盘按键修饰符
@Click.enter
表单数据的自动收集,使用v-model
生命周期常用的两个方法
mounted(),初始化阶段,这个方法只执行一次,比如改善Ajax方法,执行定时期
更新阶段的方法执行N次,可以是0次
BeforDestory(),死亡阶段只执行一次
自定义过滤器,比如可以对页面上的时间日期进行特定的格式化
比如2020-02-02 // 定义过滤器 Vue . filter ( 'dateString' , function (value , format= 'YYYY-MM-DD HH:mm:ss' ) { return moment(value). format (format) ; })
这个网址是日期格式化的API,比较好用
https://www.bootcdn.cn/moment.js/ 常用内置指令 v:text : 更新元素的 textContent v-html : 更新元素的 innerHTML v-if : 如果为 true, 当前标签才会输出到页面 v-else: 如果为 false, 当前标签才会输出到页面 v-show : 通过控制 display 样式来控制显示 / 隐藏 v-for : 遍历数组 / 对象 v-on : 绑定事件监听 , 一般简写为 @ v-bind : 强制绑定解析表达式 , 可以省略 v-bind v-model : 双向数据绑定 ref : 为某个元素注册一个唯一标识 , vue 对象通过 $refs 属性访问这个元素对象 v-cloak : 使用它防止闪现表达式 , 与 css 配合 : [v-cloak] { display: none } 1. 注册全局指令 Vue.directive('my-directive', function(el, binding){ el.innerHTML = binding.value.toupperCase() }) 2. 注册局部指令 directives : { 'my-directive' : { bind (el, binding) { el.innerHTML = binding.value.toupperCase() } } } 3. 使用指令 v-my-directive='xxx' -->


软件开发
2020-06-03 00:22:00
以前使用的,查看函数运行时间的方法: #encoding:utf-8 import time def trainModel(): print("training...") time.sleep(2) def testModel(): print("testing...") time.sleep(1) if __name__ == "__main__": start = time.time() trainModel() #训练模型 end = time.time() print("cost time:",end-start) start = time.time() testModel() #测试模型 end = time.time() print("cost time:",end-start) """ training... cost time: 2.002230644226074 testing... cost time: 1.0011751651763916 """
或者是 #encoding:utf-8 import time def trainModel(): start = time.time() #写入函数内部 print("training...") end = time.time() print("cost time:",end-start) def testModel(): start = time.time() print("testing...") end = time.time() print("cost time:",end-start) if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1682510375976562e-05 testing... cost time: 2.1457672119140625e-06 """
使用装饰器后发现真好用!!! 装饰器(装饰器为函数,被装饰的也是函数) #encoding:utf-8 import time def costTime(func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() print("cost time:",end-start) return f return wrapper #相当于 costTime(trainModel()) #将trainModel拿到costTime中的wrapper中运行 #即可在trainModel()前后添加装饰代码 @costTime def trainModel(): print("training...") @costTime def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1682510375976562e-05 testing... cost time: 2.1457672119140625e-06 """ #装饰器(装饰器为函数,被装饰的也是函数,传参数) #encoding:utf-8 import time def costTime(flag): def wrapper(func): def inner_wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if flag == "True": print("cost time:",end-start) return f return inner_wrapper return wrapper @costTime(flag = "True") def trainModel(): print("training...") @costTime(flag = "False") def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2159347534179688e-05 testing... """ #装饰器(装饰器为函数,被装饰的是类) #encoding:utf-8 import time def costTime(func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() print("cost time:",end-start) return f return wrapper @costTime class trainModel: def __init__(self): print("training...") @costTime class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2159347534179688e-05 testing... cost time: 2.86102294921875e-06 """ #装饰器(装饰器为函数,被装饰的是类,传参数) #encoding:utf-8 import time def costTime(flag): def wrapper(func): def inner_wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if flag == "True": print("cost time:",end-start) return f return inner_wrapper return wrapper @costTime(flag = "True") class trainModel: def __init__(self): print("training...") @costTime(flag = "False") class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1920928955078125e-05 testing... """ #装饰器(装饰器为类,被装饰的是函数) #encoding:utf-8 import time class costTime(): def __init__(self,func): self.fun = func #重载__call__方法是就需要接受一个函数并返回一个函数 def __call__(self,*args, **kwargs): start = time.time() f = self.fun(*args, **kwargs) end = time.time() print("cost time:",end-start) return f @costTime def trainModel(): print("training...") @costTime def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1682510375976562e-05 testing... cost time: 2.1457672119140625e-06 """ #装饰器(装饰器为类,被装饰的也是函数,传参数) #encoding:utf-8 import time class costTime: def __init__(self,flag): self.flag = flag def __call__(self,func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if self.flag == "True": print("cost time:",end-start) return f return wrapper @costTime(flag = "True") def trainModel(): print("training...") @costTime(flag = "False") def testModel(): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2874603271484375e-05 testing... """ #装饰器(装饰器为类,被装饰的也是类) #encoding:utf-8 import time class costTime(): def __init__(self,func): self.fun = func #重载__call__方法是就需要接受一个函数并返回一个函数 def __call__(self,*args, **kwargs): start = time.time() f = self.fun(*args, **kwargs) end = time.time() print("cost time:",end-start) return f @costTime class trainModel: def __init__(self): print("training...") @costTime class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.2159347534179688e-05 testing... cost time: 2.86102294921875e-06 """ #装饰器(装饰器为函数,被装饰的是类,传参数) #encoding:utf-8 import time class costTime: def __init__(self,flag): self.flag = flag def __call__(self,func): def wrapper(*args, **kwargs): start = time.time() f = func(*args, **kwargs) end = time.time() if self.flag == "True": print("cost time:",end-start) return f return wrapper @costTime(flag = "True") class trainModel: def __init__(self): print("training...") @costTime(flag = "False") class testModel: def __init__(self): print("testing...") if __name__ == "__main__": trainModel() #训练模型 testModel() #测试模型 """ training... cost time: 1.1920928955078125e-05 testing... """
软件开发
2020-06-02 23:40:00
v-for遍历数组

  • 用这个v-for时,后面最好跟一个 :key 这是标准,当然,不跟也行,只是不标准。
    数组更新检测,
    变更方法
    Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括: push() pop() shift() unshift() splice() sort() reverse()
    理解这些这些方法,这是VUE对JavaScript原生的数组方法进行了重写,重写的目的是为了达到监视数组,以及数组元素的变化。从而达到当数组元素数据变化的时候,页面也相应的发生改变。说白了就是想达到监视数组变化,实现数据绑定的效果。
    这些重写的方法内部执行过程,先调用原生JavaScript数组,再更新页面
    其中seplice()方法比较强大,可以做增加,删除,修改三个操作。
  • 软件开发
    2020-06-02 23:38:00
    Spring boot 版本: 2.2.1.RELEASE Spring cloud 版本 : Hoxton.RC1
    项目中同时使用了 spring-cloud-starter-zipkin spring-boot-starter-data-redis org.springframework.cloud spring-cloud-starter-zipkin org.springframework.boot spring-boot-starter-data-redis //www.1b23.com
    在编码中有多个Service同时注入了RedisTemplate redisTemplate导致在Tomcat启动时报错,导致无法运行。
    错误如下
    [ main] o.a.c.loader.WebappClassLoaderBase : The web application
    [ROOT] appears to have started a thread named [lettuce-eventExecutorLoop-1-1] but has failed to stop it. This is very likely to create a memory leak.
    Stack trace of thread:
    sun.misc.Unsafe.park(Native Method)`

    Caused by: org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisException: Cannot retrieve initial cluster partitions from initial URIs [RedisURI [host=‘172.31.214.234’, port=6380], RedisURI [host=‘172.31.214.234’, port=6381], RedisURI [host=‘172.31.214.235’, port=6380], RedisURI [host=‘172.31.214.235’, port=6381], RedisURI [host=‘172.31.214.236’, port=6380], RedisURI [host=‘172.31.214.236’, port=6381]
    最终原因是Redis 客户端lettuce无法连接上Redis导致该报错。
    解决
    移除spring-boot-starter-data-redis中对lettuce的引用,加入jedis的依赖 。springcloud框架www.1b23.com org.springframework.boot spring-boot-starter-data-redis io.lettuce lettuce-core redis.clients jedis //www.1b23.com
    当我移除spring-cloud-starter-zipkin 之后再次启动项目发现启动成功,spring-boot-starter-data-redis默认使用lettuce作为Redis的客户端,因此推断为spring-cloud-starter-zipkin 可能和lettuce有兼容性问题。
    因此尝试把lettuce换为jedis,问题解决。
    软件开发
    2020-06-17 13:07:00
    >作者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文: https://www.jianshu.com/p/6cbf452666bd
    > 《C语言探索之旅》 全系列
    内容简介 前言 题目规定 优化建议 第二部分第十课预告
    1. 前言
    第二部分的理论知识基本讲完了。上一课我们经历了很有意思的 C语言探索之旅 | 第二部分第八课:动态分配 。
    这一课我们来实战一下,要实现的游戏叫“悬挂小人”。
    >这个“小人”,不是“君子和小人”的小人。是 little man(小小的人)的意思。
    读者:“你有必要这么强调吗?简直无聊嘛...”
    好的,话休絮烦...
    俗语说得好:“实践是必要的!”
    对于大家来说这又尤为重要,因为我们刚刚结束了一轮 C语言的高级技术的“猛烈进攻”,需要好好复习一下,消化消化。
    不论你多厉害,在编程领域,不实践是永远不行的。尽管你可能读懂了之前的所有课程,但是如果不配合一定的实践,是不能深刻理解的。
    以前我大学里入门编程以前看 C语言的书,觉得看懂了,但是一上手要写程序,就像挤牙膏一样费劲。
    这次的实战练习,我们一起来实现一个小游戏:“悬挂小人”,或叫 “上吊游戏”。英语叫 HangMan ,是挺著名的一个休闲益智游戏。
    虽说是游戏,但是比较可惜的是还不能有图形界面 (不过课程后面会说怎么实现在控制台绘制小人,其实也可以实现简陋的“图形化”): 因为 C语言本身不具备绘制 GUI(Graphical User Interface 的缩写,表示“图形用户接口”)的能力,需要引入第三方的库。
    悬挂小人游戏是一个经典的字母游戏,在规定步数内一个字母一个字母地猜单词,直到猜出整个单词。
    所以我们的游戏暂时还是以控制台的形式(黑框框)与大家见面,当然如果你会图形编程,也可以把这个游戏扩展成图形界面的。
    相信不少读者应该见过这个游戏的图形界面版本,就是每猜错一个字母画一笔,直到用完规定次数,小人被“吊死”。
    这个实战的目的是让我们可以复习之前学过的所有 C语言知识:指针,字符串,文件读写,结构体,数组,等等,都是好家伙!
    2. 题目规定
    既然是出题目的实战,那么就需要委屈大家按照我的题目要求来编写这个游戏啦。
    好,就来公布我们的题目要求: 游戏每一轮有 7 次(次数可以设置,不一定是 7 次)猜测的机会,用完则此轮失败。 每轮会从字典中随机抽取一个单词供玩家猜,初始时单词是以若干个星号( * )的方式来表示。说明所有字母都还隐藏着。 字典的所有单词储存在一个文本文件中(在 Windows 下通常是 txt 文件,在 Unix/Linux/macOS 下一般可以是任意后缀名的文件)。 每猜错一个字母就扣掉一次机会,猜对一个字母不扣除机会数。猜对的字母会显示在屏幕上的单词中,替换掉星号。
    一个回合的运作机制
    假设要猜的单词是 OSCAR。
    假设我们给程序输入一个字母 B(猜的第一个字母),程序会验证字母是否在这个单词里。
    有两种情况: 所猜的字母在单词中,此时程序会显示这个单词,不是全部显示,而是显示猜到的那些字母,其他的还未猜到的字母用 * 表示。 所猜的字母不在单词中(目前的情况,因为字母 B 不在单词 OSCAR 中),此时程序会告诉玩家“你猜错了”,剩余的机会数会被扣除一个。如果剩余机会数变为 0,游戏结束。
    在图形化的“悬挂小人”(Hangman)游戏中,每猜一次会有一个小人被画出来。我们的游戏,虽然还不能真正实现图形化,但是如果优化一下,也可以在控制台实现类似这样的效果:
    假设玩家输入一个 C,因为 C 在单词 OSCAR 中,那么程序不会扣除玩家的剩余机会数,而且会显示已猜到的字母,如下: 单词:**C**
    如果玩家继续输入,这回输入的是 O,那么程序会显示如下: 单词:O*C**
    多个相同字母的情况
    有一些单词中,同一个字母会出现多次。比如在 APPLE(表示“苹果”)中,P 这个字母就出现了 2 次;在 ELEGANCE(表示“优雅”)中,E 这个字母出现了 3 次。
    Hangman 游戏对此的规则很简单:只要猜出一个字母,其他重复的字母会同时显示。
    假如要猜的单词是 ELEGANCE,用户输入了一个 E,那么会如下显示: 单词:E*E****E
    一个回合的例子 欢迎来到悬挂小人游戏! 您还剩 7 次机会 神秘单词是什么呢?***** 输入一个字母:E 您还剩 6 次机会 神秘单词是什么呢?***** 输入一个字母:S 您还剩 6 次机会 神秘单词是什么呢?*S*** 输入一个字母:R 您还剩 6 次机会 神秘单词是什么呢?*S**R 输入一个字母:
    游戏就会这样进行下去,直到玩家在 7 个机会用完前猜到单词,或者用完 7 个机会还没猜到单词,游戏结束。
    例如: 您还剩 2 次机会 神秘单词是什么呢?OS*AR 输入一个字母:C 胜利了!神秘单词是:OSCAR
    在控制台输入一个字母
    在控制台中让程序读入一个字母,看起来简单,但其实暗藏玄机。不信我们来试一下。
    要输入一个字母,一般大家会认为是这样做: scanf("%c", &myLetter);
    确实是不错的,因为 %c 标明了等待用户输入一个字符。输入的字符会储存在 myLetter 这个变量(类型是 char)中。
    如果我们只写一个 scanf,那是没问题的。但是假如有好几个 scanf,会怎么样呢?我们来测试一下: int main(int argc, char* argv[]) { char myLetter = 0; scanf("%c", &myLetter); printf("%c", myLetter); scanf("%c", &myLetter); printf("%c", myLetter); return 0; }
    照我们的设想,上述程序应该会请求用户输入一个字符,再打印出来: 进行两次。
    测试一下,实际情况是怎么样的呢?你输入了一个字符,没错,然后呢...
    程序为你打印出来了你输入的那个字符,假如你输入的是 a,那么程序输出 a
    然后程序就退出了,没有下文了。为什么不提示我输入第二个字符了呢?就好像它忽略了第二个 scanf 一样。到底发生了什么呢?
    事实上,当你在控制台(console)里面输入时,你输入的内容都被记录到内存的某处,当然也包括按下 Enter 键(回车键)时产生的输入: \n
    因此,你先输入了一个字符(例如 a),然后你按了一下回车键:
    字符 a 就被第一个 scanf 取走了,第二个 scanf 则把你的回车键( \n )取走了。
    为了避免这个问题,我们写一个函数 readCharacter() 来处理: char readCharacter() { char character = 0; character = getchar(); // 读取输入的第一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它们) while (getchar() != '\n') ; return character; // 返回读到的第一个字母 }
    可以看到,以上程序中,我们使用了 getchar 函数,这个函数是在标准库的 stdio.h 中,用于读取一个用户输入的字符,效果相当于 scanf("%c", &letter);
    然后,我们又用到了一个在本课程中还没学习过的函数:toupper。
    根据字面意思 to + upper 是英语“转换为大写”的意思,所以这个函数就是用于把一个字母转成大写字母。
    看到了吧,如果函数名起得好,几乎就不需要注释,看名字就知道大致是干什么的(论编程命名的重要性)。
    借着 toupper 这个函数,玩家就可以输入小写字母或者大写字母了,因为在“悬挂小人”游戏中,我们显示的单词中的字母都是大写的。
    toupper 这个函数定义在 ctype.h 这个标准库的头文件中,所以需要 #include
    继续看我们的函数,可以看到其中最关键的地方是: while (getchar() != '\n') ;
    这一小段代码使得我们可以清除第一个输入的字母外的其他字符,直到遇见 \n (回车符)。
    函数返回的就是第一个输入的字母,这样可以保证不再受回车符的影响了。
    我们用了一个 while 循环,而循环体部分只有一个分号( ; ),很简洁吧。
    也许你会问,之前的课程中 while 循环的循环体不是由大括号围起来的么,怎么这里只有一个分号呢?
    事实上,这个分号就相当于 { }
    就是空循环体,什么都不做,所以其实以上的代码相当于: while (getchar() != '\n') { }
    但是分号比大括号写起来更简单么,不要忘了程序员是懂得如何偷懒的一群人!
    此 while 循环一直执行,直到用户输入回车符,其他的字符都被从内存中清除了,我们称其为 “清空缓冲区”。
    因此: 
    为了在我们的程序中每次读取用户输入的一个字母,我们不要使用 scanf("%c", &myLetter);
    而须要借助我们写的函数: myLetter = readCharacter();
    于是,我们的测试程序变成这样: #include #include char readCharacter() { char character = 0; character = getchar(); // 读取一个字母 character = toupper(character); // 把这个字母转成大写 // 读取其他的字符,直到 \n (为了忽略它) while (getchar() != '\n') ; return character; // 返回读到的第一个字母 } int main(int argc, char* argv[]) { char myLetter = 0; myLetter = readCharacter(); printf("%c\n", myLetter); myLetter = readCharacter(); printf("%c\n", myLetter); return 0; }
    运行,输出类似如下(假如用户输入 o,回车;输入 k,回车): o O k K
    字典 / 词库
    因为我们的游戏是一步步写成的,所以一开始,肯定先写简单的,再逐步完善游戏。
    因此,猜测的单词一开始我们只用一个。所以,我们一开始会这么写: char secretWord[] = "BOTTLE";
    你会说:“这样不是很无聊嘛,猜测的单词总是这一个”。
    是的,但之后我们肯定会扩展。一开始这样做是为了不把问题复杂化,一次做一件事情,慢慢来么。
    之后如果猜测一个单词的代码可以运行了,我们再用一个文件来储存所有可能的单词,这个文件可以起名为 dictionary(表示“字典”)。
    那什么是字典或词库呢?
    在我们的游戏里,就是一个文件,文件中的每一行存放了一个单词,之后我们的程序会随机从此文件中抽取一个单词来作为每一轮的猜测单词。
    词库是类似这样的: YOU MOTHER LOVE PANDA BOTTLE FUNNY HONEY LIKE JAZZ MUSIC BREAD APPLE WATER PEOPLE
    至于这个文件里有多少单词,因为我们的词库是可扩展的(之后肯定可以添加新的单词),所以其实只要统计回车符( \n )的数目就可以,因为是每行一个单词。
    好了,游戏的基本点我们介绍到这里,其实有了前面所有课程的基础,你已经有能力来完成这个看似有点复杂的游戏了,不过要组织得好还是不那么容易的,你可以用多个函数来实现不同的功能。
    加油,坚持不懈就是胜利,期待你的成果!
    3. 优化建议
    >如果你是在 Windows 下用 CodeBlocks 等 IDE 来编译的,那么请将字典文件 dictionary 改成 dictionary.txt。 因为 Windows 的文件储存形式和 Linux/Unix/macOS 有些不一样。
    改进游戏 目前来说,我们只让玩家玩一轮,如果能加一个循环,使得游戏每次询问玩家是否要再玩一次,那“真真是极好的”。 目前还是单机模式,可以创建一个二人模式,就是一个玩家输入一个单词,第二个玩家来猜。 为什么不用 printf 函数来打印(绘制)一个悬挂小人呢?在每次我们猜错的时候,就把它画出来,每错一个,多画一笔,这样可以增加乐趣,可以用如下的代码: if (猜错1个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错2个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | |\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错3个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错4个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错5个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" | |\n"); printf(" |\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错6个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" | |\n"); printf(" | /\n"); printf(" |\n"); printf("_|__\n"); } else if (猜错7个字母) { printf(" _____\n"); printf(" | |\n"); printf(" | O\n"); printf(" | \\|/\n"); printf(" | |\n"); printf(" | / \\\n"); printf(" |\n"); printf("_|__\n"); }
    上面代码中的空格也许不同平台的显示不一样,可能需要大家自行调整。
    如果 7 次机会全部用完,则小人挂掉,游戏结束。
    请大家花点时间,好好理解这个游戏,并且尽可能地改进它。如果你可以不看我们的答案,而自己完成游戏和改进,那么你会收获很多的!
    4. 第二部分第十课预告
    今天的课就到这里,一起加油吧!
    下一课我们就会公布悬挂小人游戏的解题思路和答案咯。
    下一课: C语言探索之旅 | 第二部分第十课: 实战"悬挂小人"游戏 答案
    >我是 谢恩铭 ,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师 ,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」
    软件开发
    2020-06-17 12:15:00
    (点击图片进入关卡)
    食人魔再次来袭,只有更多参数能够救你!
    简介
    你的函数可以定义不止一个参数。
    def maybeBuildTrap(x, y):
    # 当如下函数被调用时,
    # x将是43,y将是50
    maybeBuildTrap(43, 50)
    默认代码
    # 函数maybeBuildTrap定义了两个参数!
    def maybeBuildTrap(x, y):
    # 使用x和y作为移动的坐标。
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
    pass
    # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap"

    while True:
    # 这会调用maybeBuildTrap,使用下方入口的坐标。
    maybeBuildTrap((38, 20)
    # 下面在右侧入口使用maybeBuildTrap!

    # 现在在上方入口处使用maybeBuildTrap! !
    概览
    就像 moveXY 接收两个参数那样,你创建的函数也可以定义多个参数!
    def maybeBuildTrap(x, y):
    # 当函数被调用时,
    # x 会是 43,y 会是 50
    maybeBuildTrap(43, 50)
    形参 vs. 实参
    为啥有时我们叫它形参,有时叫它实参?
    形参 (parameter) 是在函数定义里的参数。
    实参 (argument) 是函数被调用时传进的实际参数值!
    返回荆棘农场 B 解法
    # 函数maybeBuildTrap定义了两个参数!
    def maybeBuildTrap(x, y):
    # 使用x和y作为移动的坐标。
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
    # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap"
    hero.buildXY("fire-trap", x, y)

    while True:
    # 这会调用maybeBuildTrap,使用下方入口的坐标。
    maybeBuildTrap(38, 20)

    # 下面在右侧入口使用maybeBuildTrap!
    maybeBuildTrap(56, 34)
    # 现在在上方入口处使用maybeBuildTrap!
    maybeBuildTrap(38, 48)

    本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-fanhuijingjinongchangb
    极客战记——学编程,用玩的
    软件开发
    2020-06-17 11:26:00
    (点击图片进入关卡)
    食人魔再次来袭,只有更多参数能够救你!
    简介
    你的函数可以定义不止一个参数。
    def maybeBuildTrap(x, y):
    # 当如下函数被调用时,
    # x将是43,y将是50
    maybeBuildTrap(43, 50)
    默认代码
    # 函数maybeBuildTrap定义了两个参数!
    def maybeBuildTrap(x, y):
    # 使用x和y作为移动的坐标。
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
    # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap"

    pass
    while True:
    # 这会调用 maybeBuildTrap,并使用上方入口的坐标。
    maybeBuildTrap(43, 50)
    # 下面在下方入口处使用maybeBuildTrap!

    # 下面在右侧入口使用maybeBuildTrap! !
    概览
    就像 moveXY 接收两个参数那样,你创建的函数也可以定义多个参数!
    def maybeBuildTrap(x, y):
    # 当函数被调用时,
    # x 会是 43,y 会是 50
    maybeBuildTrap(43, 50)
    形参 vs. 实参
    为啥有时我们叫它形参,有时叫它实参?
    形参 (parameter) 是在函数定义里的参数。
    实参 (argument) 是函数被调用时传进的实际参数值!
    返回荆棘农场 A 解法
    # 函数maybeBuildTrap定义了两个参数!
    def maybeBuildTrap(x, y):
    # 使用x和y作为移动的坐标。
    hero.moveXY(x, y)
    enemy = hero.findNearestEnemy()
    if enemy:
    # 使用 buildXY 在特定 x 和 y 处建造 "fire-trap"
    hero.buildXY("fire-trap", x, y)

    while True:
    # 这会调用maybeBuildTrap,使用左侧入口的坐标。
    maybeBuildTrap(20, 34)

    # 下面在下方入口处使用maybeBuildTrap!
    maybeBuildTrap(38, 20)
    # 下面在右侧入口使用maybeBuildTrap! !
    maybeBuildTrap(56, 34)

    本攻略发于极客战记官方教学栏目,原文地址为: https://codecombat.163.com/news/jikezhanji-fanhuijingjinongchanga 极客战记——学编程,用玩的
    软件开发
    2020-06-17 11:24:00
    本文来自: PerfMa技术社区
    PerfMa(笨马网络)官网
    最近2周开始接手apache flink全链路监控数据的作业,包括指标统计,业务规则匹配等逻辑,计算结果实时写入elasticsearch. 昨天遇到生产环境有作业无法正常重启的问题,我负责对这个问题进行排查跟进。
    第一步,基础排查
    首先拿到jobmanager和taskmanager的日志,我从taskmanager日志中很快发现2个基础类型的报错,一个是npe,一个是索引找不到的异常
    elasticsearch sinker在执行写入数据的前后提供回调接口让作业开发人员对异常或者成功写入进行处理,如果在处理异常过程中有异常抛出,那么框架会让该task失败,导致作业重启。
    npe很容易修复,索引找不到是创建索引的服务中的一个小bug,这些都是小问题。
    重点是在日志中我看到另一个错误: java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Unknown Source) at org.apache.flink.runtime.io.network.api.writer.RecordWriter.(RecordWriter.java:122) at org.apache.flink.runtime.io.network.api.writer.RecordWriter.createRecordWriter(RecordWriter.java:321) at org.apache.flink.streaming.runtime.tasks.StreamTask.createRecordWriter(StreamTask.java:1202) at org.apache.flink.streaming.runtime.tasks.StreamTask.createRecordWriters(StreamTask.java:1170) at org.apache.flink.streaming.runtime.tasks.StreamTask.(StreamTask.java:212) at org.apache.flink.streaming.runtime.tasks.StreamTask.(StreamTask.java:190) at org.apache.flink.streaming.runtime.tasks.OneInputStreamTask.(OneInputStreamTask.java:52) at sun.reflect.GeneratedConstructorAccessor4.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at org.apache.flink.runtime.taskmanager.Task.loadAndInstantiateInvokable(Task.java:1405) at org.apache.flink.runtime.taskmanager.Task.run(Task.java:689) at java.lang.Thread.run(Unknown Source)
    这种异常,一般是nproc设置太小导致的,或者物理内存耗尽,检查完ulimit和内存,发现都很正常,这就比较奇怪了。
    第二步、分析jstack和jmap
    perfma有一个产品叫xland,我也是第一次使用,不得不说,确实牛逼,好用! 首先把出问题的taskmanager的线程栈信息和内存dump出来,具体命令: jstatck pid > 生成的文件名 jmap -dump:format=b,file=生成的文件名 进程号
    接着把这两个文件导入xland,xland可以直接看到线程总数,可以方便搜索统计线程数、实例个数等等
    最先发现的问题是这个taskmanager 线程总数竟然有17000+,这个数字显然有点大,这个时候我想看一下,哪一种类型的线程比较大,xland可以很方便的搜索,统计,这时候我注意到有一种类型的线程非常多,总数15520
    更上层的调用信息看不到了,只看到来自apache http client,根据作业流程,首先想到的就是es sinker的RestHighLevelClient用到这个东西
    那么我们在xland中统计RestHighLevelClient对象个数,发现有几百个,很显然这里有问题
    第三步、定位具体问题
    有了前面xland的帮助,我们很容易定位到是esclient出了问题 在我们的作业里面有2个地方用到了es client,一个是es sinker,es sinker使用的就是RestHighLevelClient,另一个是我们同学自己写的一个es client,同样是使用RestHighLevelClient,在es sinker的ElasticsearchSinkFunction中单独构造,用于在写入es前,先搜索一些东西拿来合并,还做了cache
    1、怀疑RestHighLevelClient bug
    我们通过一个测试,来验证是不是RestHighLevelClient的问题
    启动一个单纯使用es sinker的job,调整并发度,观察前面出现较多的 I/O dispatcher线程的个数,最后发现单个es sinker也会有240+个 I/O dispatcher线程,通过调整并发,所有taskmanager的 I/O dispatcher线程总数基本和并发成正向比例 停掉写es作业,此时所有taskmanager是不存在I/O dispatcher线程的
    看起来I/O dispatcher那种线程数量大,似乎是“正常的”
    2、杀掉作业,观察线程是否被正常回收 杀掉作业,I/O dispatcher线程变成0了,看起来es sinker使用是正常的
    这时候基本上可以判断是我们自己写的es client的问题。到底是什么问题呢?
    我们再做一个测试进一步确认
    3、启动问题作业,杀死job后,观察I/O dispatcher线程个数 重启flink的所有taskmanager,给一个“纯净”的环境,发现杀死作业后,还有I/O dispatcher线程。 这个测试可以判断是我们的es client存在线程泄漏
    四、背后的原理
    es sinker本质上是一个RichSinkFunction,RichSinkFunction带了open 和close 方法,在close方法中,es sinker正确关闭了http client @Override public void close() throws Exception { if (bulkProcessor != null) { bulkProcessor.close(); bulkProcessor = null; } if (client != null) { client.close(); client = null; } callBridge.cleanup(); // make sure any errors from callbacks are rethrown checkErrorAndRethrow(); }
    而我们的es client是没有被正确关闭的。
    具体原理应该是是这样的,当es sinker出现npe或者写es rejected等异常时,job会被flink重启,es sinker这种RichSinkFunction类型的算子会被flink 调用close关闭释放掉一些资源,而我们写在ElasticsearchSinkFunction中es client,是不会被框架关照到的,而这种写法我们自己也无法预先定义重启后关闭client的逻辑.
    如果在构造时使用单例,理论上应该是可以避免作业反复重启时es client不断被构造导致线程泄漏和内存泄漏的,但是编写单例写法有问题,虽然有double check,但是没加volatile,同时锁的是this, 而不是类。
    五、小结
    1、xland确实好用,排查问题帮助很大。
    2、flink作业用到的外部客户端不要单独构造,要使用类似RichFunction这种方式,提供open,close方法,确保让资源能够被flink正确释放掉。
    3、用到的对象,创建的线程,线程池等等最好都起一个名字,方便使用xland事后排查问题,如果有经验的话,应该一开始就统计下用于构造es client的那个包装类对象个数。
    一起来学习吧 :
    PerfMa KO 系列课之 JVM 参数【Memory篇】
    JCU之 FutureTask 源码与工作原理分析
    软件开发
    2020-06-17 10:46:00
    1.定义客户端接口 public interface ApiClient { VirtualFile upload(MultipartFile file); VirtualFile upload(File file); VirtualFile upload(InputStream is, String fileName); boolean removeFile(String key); byte[] download(String fileName); }
    2.定义实现接口父类客户端 public abstract class BaseApiClient implements ApiClient { /** * 存储类型 */ protected String storageType; /** * 文件名称 */ protected String objectName; /** * 后缀 */ protected String suffix; /** * 默认文件大小 5M */ private static final long DEFAULT_MAX_SIZE = 5 * 1024 * 1024; public BaseApiClient() { } public BaseApiClient(String storageType) { this.storageType = storageType; } @Override public VirtualFile upload(MultipartFile file) { this.assertClient(); if (file == null) { throw new RuntimeException("[" + this.storageType + "]文件上传失败:文件不可为空"); } this.assertSize(file.getSize()); try { VirtualFile virtualFile = this.upload(file.getInputStream(), file.getOriginalFilename()); virtualFile.setSize(file.getSize()); return virtualFile; } catch (IOException e) { throw new RuntimeException("[" + this.storageType + "]文件上传失败:" + e.getMessage()); } } @Override public VirtualFile upload(File file) { this.assertClient(); if (file == null) { throw new RuntimeException("[" + this.storageType + "]文件上传失败:文件不可为空"); } this.assertSize(file.length()); try { VirtualFile virtualFile = this.upload(new FileInputStream(file), file.getName()); virtualFile.setSize(file.length()); return virtualFile; } catch (FileNotFoundException ex) { throw new RuntimeException("[" + this.storageType + "]文件上传失败:" + ex.getMessage()); } } @Override public abstract VirtualFile upload(InputStream is, String fileName); @Override public abstract boolean removeFile(String fileName); protected abstract void assertClient(); /** * 生成key * * @param prefix * @param fileName */ public void createOjectName(String dir, String fileName) { this.suffix = FileUtils.getSuffix(fileName); if (!FileUtils.allAssertSuffix(this.suffix, MimeTypeConstants.DEFAULT_ALLOWED_EXTENSION)) { throw new RuntimeException("[" + this.storageType + "] 非法的文件[" + fileName + "]!目前只支持以下格式:" + Arrays.toString(MimeTypeConstants.DEFAULT_ALLOWED_EXTENSION)); } this.objectName = getPathFileName(dir, fileName); } public String getPathFileName(String dir, String fileName) { fileName = fileName.replace("_", " "); String temporary = DateUtil.format(new Date(), "yyyyMMdd") + "/" + SecureUtil.md5(fileName + System.nanoTime()); return dir + (temporary + "." + this.suffix); } /** * 文件大小校验 * * @param allExtension * @throws IOException */ public void assertSize(long size) { if (DEFAULT_MAX_SIZE != -1 && DEFAULT_MAX_SIZE < size) { throw new RuntimeException( "[" + this.storageType + "]允许的文件最大大小是" + (DEFAULT_MAX_SIZE / 1024 / 1024) + "MB!"); } } }
    3.定义阿里云OSS客户端实现 public class AliyunApiClient extends BaseApiClient { private static final String STORAGE_CLIENT = "阿里云OSS"; private static final String ENDPOINT = "YOUR ENDPOINT"; private static final String ACCESSKEYID = "YOUR ACCESSKEYID"; private static final String ACCESSKEYSECRET = "YOUR ACCESSKEYSECRET"; private static final String BUCKETNAME = "YOUR BUCKETNAME"; private static final String BUCKETURL = "YOUR BUCKETURL"; /** * 默认上传目录 */ private static final String DEFAULT_DIR = "upload/"; private AliyunOSSApi ossApi; public AliyunApiClient() { super(STORAGE_CLIENT); ossApi = new AliyunOSSApi(ENDPOINT, ACCESSKEYID, ACCESSKEYSECRET); } @Override public VirtualFile upload(InputStream is, String fileName) { this.assertClient(); this.createOjectName(DEFAULT_DIR, fileName); try { ossApi.uploadFile(BUCKETNAME, this.objectName, is); VirtualFile virtualFile = new VirtualFile(); virtualFile.setSuffix(this.suffix); virtualFile.setFilePath(this.objectName); virtualFile.setFullFilePath(BUCKETURL + this.objectName); virtualFile.setOriginalFileName(fileName); virtualFile.setUploadTime(new Date()); virtualFile.setClient(STORAGE_CLIENT); return virtualFile; } catch (Exception e) { throw new RuntimeException("[" + this.storageType + "]文件上传失败:" + e.getMessage()); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Override public boolean removeFile(String fileName) { this.assertClient(); if (StringUtils.isEmpty(fileName)) { throw new RuntimeException("[" + this.storageType + "]删除文件失败:文件key为空"); } try { ossApi.deleteFile(BUCKETNAME, fileName); return true; } catch (Exception e) { throw new RuntimeException("[" + this.storageType + "]删除文件失败:" + e.getMessage()); } } @Override public byte[] download(String objectName) { this.assertClient(); if (StringUtils.isEmpty(objectName)) { throw new RuntimeException("[" + this.storageType + "]下载文件失败:文件key为空"); } try { return ossApi.download(BUCKETNAME, objectName); } catch (Exception e) { throw new RuntimeException("[" + this.storageType + "]下载文件失败:" + e.getMessage()); } } /** * 客户端配置校验 */ @Override public void assertClient() { if (null == ossApi) { throw new RuntimeException("[" + this.storageType + "]尚未配置阿里云OSS,文件上传功能暂时不可用!"); } } }
    连接阿里云OSS客户端 public class AliyunOSSApi { private OSSClient client; public AliyunOSSApi(OSSClient oss) { this.client = oss; } public AliyunOSSApi(String endpoint, String accessKeyId, String accessKeySecret) { // 创建ClientConfiguration实例,按照您的需要修改默认参数。 // ClientBuilderConfiguration conf = new ClientBuilderConfiguration(); // 开启支持CNAME。CNAME是指将自定义域名绑定到存储空间上。 // conf.setSupportCname(true); client = new OSSClient(endpoint, accessKeyId, accessKeySecret); } /** * 创建存储空间 * * @param bucketName 存储空间名称 */ public boolean createBucket(String bucketName) { try { boolean exists = this.client.doesBucketExist(bucketName); if (exists) { throw new RuntimeException("[阿里云OSS] Bucket创建失败!Bucket名称[" + bucketName + "]已被使用!"); } // 创建CreateBucketRequest对象 CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); // 设置bucket权限为公共读,默认是私有读写 createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead); // 此处以设置存储空间的存储类型为标准存储为例。 createBucketRequest.setStorageClass(StorageClass.Standard); // 创建存储空间。 this.client.createBucket(createBucketRequest); return true; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return false; } /** * 列举存储空间 * * @return */ public List listBuckets() { try { return this.client.listBuckets(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 获取存储空间的信息 * * @param bucketName 存储空间名称 * @return */ public BucketInfo getBucketInfo(String bucketName) { try { boolean exists = this.client.doesBucketExist(bucketName); if (!exists) { throw new RuntimeException("获取[阿里云OSS] 获取存储空间的信息失败!Bucket名称[" + bucketName + "]不存在!"); } BucketInfo info = client.getBucketInfo(bucketName); return info; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 设置存储空间的访问权限 * * @param bucketName 存储空间名称 * @param controlList 存储空间的访问权限 * @return */ public boolean setBucketAcl(String bucketName, CannedAccessControlList controlList) { try { boolean exists = this.client.doesBucketExist(bucketName); if (!exists) { throw new RuntimeException("设置[阿里云OSS]设置存储空间的访问权限!Bucket名称[" + bucketName + "]不存在!"); } client.setBucketAcl(bucketName, controlList); return true; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return false; } /** * 获取存储空间访问权限 * * @param bucketName 存储空间名称 */ public AccessControlList getBucketAcl(String bucketName) { try { boolean exists = this.client.doesBucketExist(bucketName); if (!exists) { throw new RuntimeException("获取[阿里云OSS]存储空间访问权限!Bucket名称[" + bucketName + "]不存在!"); } AccessControlList acl = client.getBucketAcl(bucketName); return acl; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 设置防盗链 * * @param bucketName 存储空间名称 * @param RefererList 允许指定的域名访问OSS资源 */ public boolean setBucketReferer(String bucketName, List RefererList) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法设置Referer白名单!Bucket不存在:" + bucketName); } if (CollectionUtils.isEmpty(RefererList)) { return false; } // 设置存储空间Referer列表。设为true表示Referer字段允许为空 BucketReferer br = new BucketReferer(true, RefererList); this.client.setBucketReferer(bucketName, br); return true; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return false; } /** * 获取防盗链信息 * * @param bucketName 存储空间名 */ public List getReferers(String bucketName) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法清空Referer白名单!Bucket不存在:" + bucketName); } BucketReferer br = this.client.getBucketReferer(bucketName); return br.getRefererList(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 清空防盗链信息 * * @param bucketName 存储空间名 */ public boolean removeReferers(String bucketName) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法清空Referer白名单!Bucket不存在:" + bucketName); } // 默认允许referer字段为空,且referer白名单为空。 BucketReferer br = new BucketReferer(); client.setBucketReferer(bucketName, br); return true; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return false; } /** * 授权访问 * * @param bucketName * @param objectName * @param expiration 失效时长 * @param style 图片样式 * @return */ public String generatePresignedUrl(String bucketName, String objectName, Date expiration, String style) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法进行URL临时授权!Bucket不存在:" + bucketName); } if (!this.client.doesObjectExist(bucketName, objectName)) { throw new RuntimeException("[阿里云OSS] 无法进行URL临时授权!文件不存在:" + bucketName + "/" + objectName); } if (null == expiration) { // 设置URL过期时间为1小时 expiration = new Date(new Date().getTime() + 1000 * 60 * 10); } GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(bucketName, objectName, HttpMethod.GET); req.setExpiration(expiration); req.setProcess(style); // 生成以GET方法访问的签名URL,访客可以直接通过浏览器访问相关内容。 URL url = client.generatePresignedUrl(req); return url.toString(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } public String uploadFile(String bucketName, String objectName, String filePath) { return uploadFile(bucketName, objectName, new File(filePath)); } /** * 文件上传 * * @param file 待上传的文件 * @param ObjectName 文件名:最终保存到云端的文件名 * @param bucketName 需要上传到的目标bucket */ public String uploadFile(String bucketName, String objectName, File file) { try { PutObjectResult result = client.putObject(bucketName, objectName, file); return result.getETag(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 文件上传 * * @param inputStream 待上传的文件流 * @param ObjectName 文件名:最终保存到云端的文件名 * @param bucketName 需要上传到的目标bucket */ public String uploadFile(String bucketName, String objectName, InputStream inputStream) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法上传文件!Bucket不存在:" + bucketName); } PutObjectResult result = this.client.putObject(bucketName, objectName, inputStream); return result.getETag(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 流式下载 * * @param inputStream 待上传的文件流 * @param bucketName 需要上传到的目标bucket * @param objectName 需要下到的目标bucket */ public byte[] download(String bucketName, String ObjectName) throws IOException { BufferedInputStream in = null; ByteArrayOutputStream out = null; try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!Bucket不存在:" + bucketName); } if (!this.client.doesObjectExist(bucketName, ObjectName)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!文件不存在:" + bucketName + "/" + ObjectName); } OSSObject ossObject = client.getObject(bucketName, ObjectName); // 流下载,一次处理部分内容,需要添加缓存区式 in = new BufferedInputStream(ossObject.getObjectContent()); out = new ByteArrayOutputStream(); byte[] bs = new byte[1024]; int n; while ((n = in.read(bs)) != -1) { out.write(bs, 0, n); } return out.toByteArray(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (null != out) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != in) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (this.client != null) { this.client.shutdown(); } } return null; } /** * 下载到本地文件 * * @param inputStream 待上传的文件流 * @param bucketName 需要上传到的目标bucket * @param localPath 本地路径 * @return */ public ObjectMetadata dowload(String bucketName, String objectName, String localPath) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!Bucket不存在:" + bucketName); } if (!this.client.doesObjectExist(bucketName, objectName)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!文件不存在:" + bucketName + "/" + objectName); } File file = new File(localPath + objectName); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } // 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖,不存在则新建。 ObjectMetadata metadata = this.client.getObject(new GetObjectRequest(bucketName, objectName), file); return metadata; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 下载到本地文件 * * @param inputStream 待上传的文件流 * @param bucketName 需要上传到的目标bucket * @param style 图片样式 * @param localPath 本地路径 * @return */ public ObjectMetadata dowloadImage(String bucketName, String objectName, String style, String localPath) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!Bucket不存在:" + bucketName); } if (!FileUtils.allAssertSuffix(FileUtils.getSuffix(objectName), MimeTypeConstants.IMAGE_ALLOWED_EXTENSION)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!不支持此图像格式:" + objectName); } if (!this.client.doesObjectExist(bucketName, objectName)) { throw new RuntimeException("[阿里云OSS] 无法下载文件!文件不存在:" + bucketName + "/" + objectName); } File file = new File(localPath + objectName); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } GetObjectRequest request = new GetObjectRequest(bucketName, objectName); request.setProcess(style); // 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖,不存在则新建。 ObjectMetadata metadata = this.client.getObject(request, file); return metadata; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 判断文件是否存在 * * @param ObjectName OSS中保存的文件名 * @param bucketName 存储空间 */ public boolean isExistFile(String bucketName, String objectName) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] Bucket不存在:" + bucketName); } return this.client.doesObjectExist(bucketName, objectName); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return false; } /** * 列举文件 */ public List getOSSObjectSummary(String bucketName) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] Bucket不存在:" + bucketName); } ObjectListing objectListing = client.listObjects(bucketName); return objectListing.getObjectSummaries(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 修改指定bucket下的文件的访问权限 * * @param ObjectName OSS中保存的文件名 * @param bucketName 保存文件的目标bucket * @param acl 权限 */ public void setFileAcl(String bucketName, String ObjectName, CannedAccessControlList acl) { try { boolean exists = this.client.doesBucketExist(bucketName); if (!exists) { throw new RuntimeException("[阿里云OSS] 无法修改文件的访问权限!Bucket不存在:" + bucketName); } if (!this.client.doesObjectExist(bucketName, ObjectName)) { throw new RuntimeException("[阿里云OSS] 无法修改文件的访问权限!文件不存在:" + bucketName + "/" + ObjectName); } this.client.setObjectAcl(bucketName, ObjectName, acl); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } } /** * 获取指定bucket下的文件的访问权限 * * @param ObjectName OSS中保存的文件名 * @param bucketName 存储空间 * @return */ public ObjectPermission getFileAcl(String bucketName, String ObjectName) { try { if (!this.client.doesBucketExist(bucketName)) { throw new RuntimeException("[阿里云OSS] 无法获取文件的访问权限!Bucket不存在:" + bucketName); } if (!this.client.doesObjectExist(bucketName, ObjectName)) { throw new RuntimeException("[阿里云OSS] 无法获取文件的访问权限!文件不存在:" + bucketName + "/" + ObjectName); } return this.client.getObjectAcl(bucketName, ObjectName).getPermission(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } /** * 删除单文件 * * @param bucketName 保存文件的目标bucket * @param ObjectName OSS中保存的文件名 */ public boolean deleteFile(String bucketName, String ObjectName) { try { boolean exists = this.client.doesBucketExist(bucketName); if (!exists) { throw new RuntimeException("[阿里云OSS] 文件删除失败!Bucket不存在:" + bucketName); } if (!this.client.doesObjectExist(bucketName, ObjectName)) { throw new RuntimeException("[阿里云OSS] 文件删除失败!文件不存在:" + bucketName + "/" + ObjectName); } this.client.deleteObject(bucketName, ObjectName); return true; } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return false; } /** * 删除多文件 * * @param bucketName 保存文件的目标bucket * @param ObjectName OSS中保存的文件名 */ public List deleteFile(String bucketName, List keys) { try { boolean exists = this.client.doesBucketExist(bucketName); if (!exists) { throw new RuntimeException("[阿里云OSS] 文件删除失败!Bucket不存在:" + bucketName); } for (String ObjectName : keys) { if (!this.client.doesObjectExist(bucketName, ObjectName)) { throw new RuntimeException("[阿里云OSS] 文件删除失败!文件不存在:" + bucketName + "/" + ObjectName); } } DeleteObjectsResult deleteObjectsResult = this.client .deleteObjects(new DeleteObjectsRequest(bucketName).withKeys(keys)); return deleteObjectsResult.getDeletedObjects(); } catch (OSSException oe) { oe.printStackTrace(); } catch (ClientException ce) { ce.printStackTrace(); } finally { if (this.client != null) { this.client.shutdown(); } } return null; } }
    3.定义本地客户端实现 public class LocalApiClient extends BaseApiClient { private static final String STORAGE_CLIENT = "local"; private static final String URL = Constants.WEB_PATH; /** * 默认上传目录 */ private static final String DEFAULT_DIR = "upload/"; public LocalApiClient() { super(STORAGE_CLIENT); } @Override public VirtualFile upload(InputStream is, String fileName) { this.assertClient(); this.createOjectName(DEFAULT_DIR, fileName); File realFilePath = FileUtils.getAbsoluteFile(URL, this.objectName); try (FileOutputStream out = new FileOutputStream(realFilePath)) { FileCopyUtils.copy(is, out); VirtualFile virtualFile = new VirtualFile(); virtualFile.setSuffix(this.suffix); virtualFile.setFilePath(this.objectName); virtualFile.setFullFilePath(URL + this.objectName); virtualFile.setOriginalFileName(fileName); virtualFile.setUploadTime(new Date()); virtualFile.setClient(STORAGE_CLIENT); return virtualFile; } catch (Exception e) { throw new RuntimeException("[" + this.storageType + "]文件上传失败:" + e.getMessage()); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Override public boolean removeFile(String fileName) { this.assertClient(); if (StringUtils.isEmpty(fileName)) { throw new RuntimeException("[" + this.storageType + "]删除文件失败:文件key为空"); } File file = new File(URL + fileName); if (!file.exists()) { throw new RuntimeException("[" + this.storageType + "]删除文件失败:文件不存在[" + URL + fileName + "]"); } try { return file.delete(); } catch (Exception e) { throw new RuntimeException("[" + this.storageType + "]删除文件失败:" + e.getMessage()); } } @Override public byte[] download(String fileName) { this.assertClient(); if (StringUtils.isEmpty(fileName)) { throw new RuntimeException("[" + this.storageType + "]下载文件失败:文件key为空"); } try { InputStream stream = new FileInputStream(URL + fileName); return IoUtil.readBytes(stream); } catch (Exception e) { throw new RuntimeException("[" + this.storageType + "]下载文件失败:" + e.getMessage()); } } @Override protected void assertClient() { if (StringUtils.isEmpty(URL) || StringUtils.isEmpty(DEFAULT_DIR)) { throw new RuntimeException("[" + this.storageType + "]尚未配置,文件上传功能暂时不可用!"); } } }
    选择需要连接的客户端 public class ConnectClient { public ApiClient getClient() { // 实际查询数据库配置 String storage = "aliyun"; if (StringUtils.isEmpty(storage)) { throw new RuntimeException("[文件服务]当前系统暂未配置文件服务相关的内容!"); } ApiClient client = null; switch (storage) { case "local": client = new LocalApiClient(); break; case "aliyun": client = new AliyunApiClient(); break; default: break; } if (null == client) { throw new RuntimeException("[文件服务]当前系统暂未配置文件服务相关的内容!"); } return client; } }
    编写上传接口 public interface FileUpload { /** * 上传文件 * * @param file 待上传的文件流 */ VirtualFile upload(InputStream file,String fileName); /** * 上传文件 * * @param file 待上传的文件 */ VirtualFile upload(File file); /** * 上传文件 * * @param file 待上传的文件 */ VirtualFile upload(MultipartFile file); /** * 删除文件 * * @param filePath 文件路径 */ boolean delete(String filePath); }
    连接客户端,实现上传接口 public class GlobalFileUpload extends ConnectClient implements FileUpload { @Override public VirtualFile upload(InputStream is, String fileName) { ApiClient client = this.getClient(); return client.upload(is, fileName); } @Override public VirtualFile upload(File file) { ApiClient client = this.getClient(); return client.upload(file); } @Override public VirtualFile upload(MultipartFile file) { ApiClient client = this.getClient(); return client.upload(file); } @Override public boolean delete(String filePath) { if (StringUtils.isEmpty(filePath)) { throw new RuntimeException("[文件服务]文件删除失败,文件为空!"); } ApiClient apiClient = this.getClient(); return apiClient.removeFile(filePath); } }
    编写下载接口 public interface FileDownload { byte[] download(String fileName); }
    连接客户端,实现下载接口 public class GlobalFileDownload extends ConnectClient implements FileDownload { @Override public byte[] download(String fileName) { ApiClient client = this.getClient(); return client.download(fileName); } }
    文件上传封装 public class FileUploadUtils { public static final List upload(List files) { List virtualFiles = new ArrayList(); for (MultipartFile file : files) { virtualFiles.add(upload(file)); } return virtualFiles; } public static final VirtualFile upload(MultipartFile file) { GlobalFileUpload globalFileUpload = new GlobalFileUpload(); return globalFileUpload.upload(file); } public static final VirtualFile upload(InputStream file, String fileName) { GlobalFileUpload globalFileUpload = new GlobalFileUpload(); return globalFileUpload.upload(file, fileName); } public static final VirtualFile upload(File file) { GlobalFileUpload globalFileUpload = new GlobalFileUpload(); return globalFileUpload.upload(file); } public static final boolean delete(String filePath) { GlobalFileUpload globalFileUpload = new GlobalFileUpload(); return globalFileUpload.delete(filePath); } }
    文件下载封装 public class FileDownloadUtils { public static void download(HttpServletRequest request, HttpServletResponse response, String filePath, String fileName) { if (StrUtil.isBlank(fileName)) { fileName = StrUtil.subAfter(filePath, "/", true); } response.setCharacterEncoding(CharsetUtil.UTF_8); response.setContentType("multipart/form-data"); OutputStream out = null; try { response.setHeader("Content-Disposition", "attachment;fileName=" + setFileDownloadHeader(request, fileName)); out = response.getOutputStream(); GlobalFileDownload globalFileDownload = new GlobalFileDownload(); out.write(globalFileDownload.download(filePath)); } catch (IOException e) { e.printStackTrace(); } finally { IoUtil.close(out); } } public static void download(String filePath, String fileName) { if (StrUtil.isBlank(fileName)) { fileName = StrUtil.subAfter(filePath, "/", true); } GlobalFileDownload globalFileDownload = new GlobalFileDownload(); try { Filedownload.save(globalFileDownload.download(fileName), "application/octet-stream", URLEncoder.encode(fileName, CharsetUtil.UTF_8)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } /** * 下载文件名重新编码 * * @param request 请求对象 * @param fileName 文件名 * @return 编码后的文件名 */ public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException { final String agent = request.getHeader("USER-AGENT"); String filename = fileName; if (agent.contains("MSIE")) { // IE浏览器 filename = URLEncoder.encode(filename, CharsetUtil.UTF_8); filename = filename.replace("+", " "); } else if (agent.contains("Firefox")) { // 火狐浏览器 filename = new String(fileName.getBytes(), CharsetUtil.ISO_8859_1); } else if (agent.contains("Chrome")) { // google浏览器 filename = URLEncoder.encode(filename, CharsetUtil.UTF_8); } else { // 其它浏览器 filename = URLEncoder.encode(filename, CharsetUtil.UTF_8); } return filename; } }
    返回实体 public class VirtualFile { /** * 文件大小 */ public Long size; /** * 文件后缀(Suffix) */ public String suffix; /** * 文件路径 (不带域名) */ private String filePath; /** * 文件全路径 (带域名) */ private String fullFilePath; /** * 原始文件名 */ private String originalFileName; /** * 业务类型 */ private String bizeType; /** * 业务唯一标识 */ private String bizeId; /** * 上传用户 */ private String uploadUser; /** * 文件上传时间 */ private Date uploadTime; /** * 客户端 */ private String client; public Long getSize() { return size; } public void setSize(Long size) { this.size = size; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } public String getFilePath() { return filePath; } public void setFilePath(String filePath) { this.filePath = filePath; } public String getFullFilePath() { return fullFilePath; } public void setFullFilePath(String fullFilePath) { this.fullFilePath = fullFilePath; } public String getOriginalFileName() { return originalFileName; } public void setOriginalFileName(String originalFileName) { this.originalFileName = originalFileName; } public String getBizeType() { return bizeType; } public void setBizeType(String bizeType) { this.bizeType = bizeType; } public String getBizeId() { return bizeId; } public void setBizeId(String bizeId) { this.bizeId = bizeId; } public String getUploadUser() { return uploadUser; } public void setUploadUser(String uploadUser) { this.uploadUser = uploadUser; } public Date getUploadTime() { return uploadTime; } public void setUploadTime(Date uploadTime) { this.uploadTime = uploadTime; } public String getClient() { return client; } public void setClient(String client) { this.client = client; } @Override public String toString() { return "VirtualFile [size=" + size + ", suffix=" + suffix + ", filePath=" + filePath + ", fullFilePath=" + fullFilePath + ", originalFileName=" + originalFileName + ", bizeType=" + bizeType + ", bizeId=" + bizeId + ", uploadUser=" + uploadUser + ", uploadTime=" + uploadTime + ", client=" + client + "]"; } }
    工具类 public class FileUtils { /** * 文件后缀 * * @param file * @return */ public static String getSuffix(File file) { return getSuffix(file.getName()); } /** * 后缀 * * @param fileName * @return */ public static String getSuffix(String fileName) { int index = fileName.lastIndexOf("."); index = -1 == index ? fileName.length() : index; return fileName.substring(index + 1); } /** * 文件后缀比较 */ public static final boolean allAssertSuffix(String suffix, String[] allSuffix) { return !StringUtils.isEmpty(suffix) && Arrays.asList(allSuffix).contains(suffix.toLowerCase()); } /** * 创建文件目录 * * @param baseDir * @param path * @return */ public static final File getAbsoluteFile(String dir, String path) { if (StringUtils.isEmpty(dir)) { return null; } File file = new File(dir + path); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } return file; } }
    多媒体常量 public class MimeTypeConstants { public static final String IMAGE_PNG = "image/png"; public static final String IMAGE_JPG = "image/jpg"; public static final String IMAGE_JPEG = "image/jpeg"; public static final String IMAGE_BMP = "image/bmp"; public static final String IMAGE_GIF = "image/gif"; public static final String[] IMAGE_ALLOWED_EXTENSION = { // 图片 "jpg", "png", "bmp", "gif", "webp", "tiff" }; /** * 文件类型 */ public static final String[] DEFAULT_ALLOWED_EXTENSION = { // 图片 "jpg", "png", "bmp", "gif", "webp", "tiff", // word "doc", "docx", "xls", "xlsx", "txt", // 压缩文件 "rar", "zip", "gz", "bz2", }; public static String getExtension(String prefix) { switch (prefix) { case IMAGE_PNG: return "png"; case IMAGE_JPG: return "jpg"; case IMAGE_JPEG: return "jpeg"; case IMAGE_BMP: return "bmp"; case IMAGE_GIF: return "gif"; default: return ""; } } }
    MinioClientApi public class MinioClientApi { private MinioClient minioClient; public MinioClientApi() { try { minioClient = new MinioClient("https://play.min.io", "Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"); } catch (InvalidEndpointException | InvalidPortException e) { e.printStackTrace(); } } /** * 删除一个对象 * * @param bucketName * @param objectName */ public void removeObject(String bucketName, String objectName) { try { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { throw new MinioClientApiException(bucketName + "存储桶不存在"); } minioClient.statObject(bucketName, objectName); minioClient.removeObject(bucketName, objectName); } catch (Exception e) { throw new MinioClientApiException(objectName + "上传对象异常", e); } } /** * 通过文件上传到对象中 * * @param bucketName * @param objectName * @param fileName */ public void putObject(String bucketName, String objectName, String fileName) { try { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { throw new MinioClientApiException(bucketName + "存储桶不存在"); } minioClient.putObject(bucketName, objectName, fileName); } catch (Exception e) { throw new MinioClientApiException(objectName + "上传对象异常", e); } } /** * 通过InputStream上传对象 * * @param bucketName * @param objectName * @param stream * @param contentType */ public void putObject(String bucketName, String objectName, InputStream stream) { putObject(bucketName, objectName, stream, "application/octet-stream"); } /** * 通过InputStream上传对象 * * @param bucketName * @param objectName * @param stream * @param contentType */ public void putObject(String bucketName, String objectName, InputStream stream, String contentType) { try { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { throw new MinioClientApiException(bucketName + "存储桶不存在"); } minioClient.putObject(bucketName, objectName, stream, stream.available(), contentType); } catch (Exception e) { throw new MinioClientApiException(objectName + "上传对象异常", e); } } /** * 下载并将文件保存到本地 * * @param bucketName * @param objectName * @param filePath */ public void getObject(String bucketName, String objectName, String fileName) { try { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { throw new MinioClientApiException(bucketName + "存储桶不存在"); } // 调用statObject()来判断对象是否存在。 // 如果不存在, statObject()抛出异常, // 否则则代表对象存在。 minioClient.statObject(bucketName, objectName); minioClient.getObject(bucketName, objectName, fileName); } catch (Exception e) { throw new MinioClientApiException(objectName + "对象下载异常", e); } } /** * 以流的形式下载一个对象 * * @param bucketName * @param objectName * @return */ public InputStream getObject(String bucketName, String objectName) { InputStream stream = null; try { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { throw new MinioClientApiException(bucketName + "存储桶不存在"); } // 调用statObject()来判断对象是否存在。 // 如果不存在, statObject()抛出异常, // 否则则代表对象存在。 minioClient.statObject(bucketName, objectName); stream = minioClient.getObject(bucketName, objectName); } catch (Exception e) { throw new MinioClientApiException(objectName + "对象下载异常", e); } finally { if (null != stream) { try { stream.close(); } catch (IOException e) { e.printStackTrace(); } } } return stream; } /** * 以流的形式下载一个对象,返回byte数组 * * @param bucketName * @param objectName * @return */ public byte[] getObjectByte(String bucketName, String objectName) { ByteArrayOutputStream out = null; BufferedInputStream in = null; try { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { throw new MinioClientApiException(bucketName + "存储桶不存在"); } // 调用statObject()来判断对象是否存在。 // 如果不存在, statObject()抛出异常, // 否则则代表对象存在。 minioClient.statObject(bucketName, objectName); in = new BufferedInputStream(minioClient.getObject(bucketName, objectName)); out = new ByteArrayOutputStream(); byte[] bs = new byte[1024]; int n; while ((n = in.read(bs)) != -1) { out.write(bs, 0, n); } } catch (Exception e) { throw new MinioClientApiException(objectName + "对象下载异常", e); } finally { if (null != out) { try { out.close(); } catch (IOException e1) { e1.printStackTrace(); } } if (null != in) { try { in.close(); } catch (IOException e1) { e1.printStackTrace(); } } } return out.toByteArray(); } }
    MinioClientApi自定义异常 public class MinioClientApiException extends RuntimeException { private static final long serialVersionUID = 1L; private final String message; public MinioClientApiException(String message) { this.message = message; } public MinioClientApiException(String message, Throwable e) { super(message, e); this.message = message; } @Override public String getMessage() { return message; } }
    软件开发
    2020-06-17 10:45:00
    大家知道,考研很大一部分也是考信息收集能力。每年往往有很多人就是在这上面栽跟头了,不能正确分析各大院校往年的录取信息,进而没能选择合适的报考院校。
    至于很多院校的录取信息是以 PDF 形式发布,例如我手上的深大电通录取结果,这就需要我们先把 PDF 转化为 Excel 啦。
    (1)PDF
    (2)Excel
    有了 Excel,那我们就可以为所欲为了!
    开始
    1. 载入 Excel 表格 #coding=utf8 import xlrd import numpy as np from pyecharts.charts import Bar from pyecharts.charts import Pie, Grid from pyecharts import options as opts #==================== 准备数据 ==================== # 导入Excel 文件 data = xlrd.open_workbook("C:/深圳大学电子与信息工程学院2020年电子信息硕士生拟录取名单.xlsx") # 载入第一个表格 table = data.sheets[0]
    2. 提取 Excel 表格数据 tables = def Read_Excel(excel): # 从第4行开始读取数据,因为这个Excel文件里面从第四行开始才是考生信息 for rows in range(3, excel.nrows-1): dict_ = {"id":"", "name":"", "status":"", "preliminary_score":"", "retest_score":"", "total_score":"", "ranking":""} dict_["id"] = table.cell_value(rows, 1) dict_["name"] = table.cell_value(rows, 2) dict_["status"] = table.cell_value(rows, 3) dict_["remarks"] = table.cell_value(rows, 4) dict_["preliminary_score"] = table.cell_value(rows, 5) dict_["retest_score"] = table.cell_value(rows, 6) dict_["total_score"] = table.cell_value(rows, 7) dict_["ranking"] = table.cell_value(rows, 8) # 将未被录取或者非普通计划录取的考生滤除 if dict_["status"] == str("拟录取") and dict_["remarks"] == str("普通计划"): tables.append(dict_)
    我们打印一下看看是否正确取出数据: # 执行上面方法 Read_Excel(table) for i in tables: print(i)
    可以看到一切顺利。
    3. 数据分段统计
    这步因人而异,我只是想把各个分数段进行单独统计而已,大家也可以根据自己的喜好做其它的处理。 num_score_300_310 = 0 num_score_310_320 = 0 num_score_320_330 = 0 num_score_330_340 = 0 num_score_340_350 = 0 num_score_350_360 = 0 num_score_360_370 = 0 num_score_370_380 = 0 num_score_380_390 = 0 num_score_390_400 = 0 num_score_400_410 = 0 min_score = 999 max_score = 0 # 将各个分段的数量统计 for i in tables: score = i["preliminary_score"] if score > max_score: max_score = score if score < min_score: min_score = score if score in range(300, 310): num_score_300_310 = num_score_300_310 + 1 elif score in range(310, 320): num_score_310_320 = num_score_310_320 + 1 elif score in range(320, 330): num_score_320_330 = num_score_320_330 + 1 elif score in range(330, 340): num_score_330_340 = num_score_330_340 + 1 elif score in range(340, 350): num_score_340_350 = num_score_340_350 + 1 elif score in range(350, 360): num_score_350_360 = num_score_350_360 + 1 elif score in range(360, 370): num_score_360_370 = num_score_360_370 + 1 elif score in range(370, 380): num_score_370_380 = num_score_370_380 + 1 elif score in range(380, 390): num_score_380_390 = num_score_380_390 + 1 elif score in range(390, 400): num_score_390_400 = num_score_390_400 + 1 elif score in range(400, 410): num_score_400_410 = num_score_400_410 + 1 # 构建两个元组用以后期建表方便 bar_x_axis_data = ("300-310", "310-320", "320-330", "330-340", "340-350", "350-360", "360-370", "370-380", "380-390", "390-400", "400-410") bar_y_axis_data = (num_score_300_310, num_score_310_320, num_score_320_330,\ num_score_330_340, num_score_340_350, num_score_350_360,\ num_score_360_370, num_score_370_380, num_score_380_390,\ num_score_390_400, num_score_400_410)
    绘制可视化图形
    1、柱状图: #===================== 柱状图 ===================== # 构建柱状图 c = ( Bar .add_xaxis(bar_x_axis_data) .add_yaxis("录取考生", bar_y_axis_data, color="#af00ff") .set_global_opts(title_opts=opts.TitleOpts(title="数量")) .render("C:/录取数据图.html") )
    2、饼图: #====================== 饼图 ====================== c = ( Pie(init_opts=opts.InitOpts(height="800px", width="1200px")) .add("录取分数概览", [list(z) for z in zip(bar_x_axis_data, bar_y_axis_data)], center=["35%", "38%"], radius="40%", label_opts=opts.LabelOpts( formatter="{b|{b}: }{c} {per|{d}%} ", rich={ "b": {"fontSize": 16, "lineHeight": 33}, "per": { "color": "#eee", "backgroundColor": "#334455", "padding": [2, 4], "borderRadius": 2, }, } )) .set_global_opts(title_opts=opts.TitleOpts(title="录取", subtitle='Made by 王昊'), legend_opts=opts.LegendOpts(pos_left="0%", pos_top="65%")) .render("C:/录取饼图.html") )

    大功告成!!是不是超级直观哈哈!
    作者 | Waao666
    原文 | https://blog.csdn.net/weixin_40973138/article/details/106190092 文源网络,仅供学习之用,如有侵权请联系删除。
    在学习Python的道路上肯定会遇见困难,别慌,我这里有一套学习资料,包含40+本电子书,800+个教学视频,涉及Python基础、爬虫、框架、数据分析、机器学习等,不怕你学不会! https://shimo.im/docs/JWCghr8prjCVCxxK/ 《Python学习资料》
    关注公众号【Python圈子】,优质文章每日送达。
    软件开发
    2020-06-17 10:39:00
    0. 前言
    通过前两篇,我们创建了一个项目,并规定了一个基本的数据层访问接口。这一篇,我们将以EF Core为例演示一下数据层访问接口如何实现,以及实现中需要注意的地方。
    1. 添加EF Core
    先在数据层实现层引入 EF Core: cd Domain.Implements dotnet add package Microsoft.EntityFrameworkCore
    当前项目以SqlLite为例,所以再添加一个SqlLite数据库驱动: dotnet add package Microsoft.EntityFrameworkCore.SQLite
    删除 Domain.Implements 里默认的Class1.cs 文件,然后添加Insfrastructure目录,创建一个 DefaultContext: using Microsoft.EntityFrameworkCore; namespace Domain.Implements.Insfrastructure { public class DefaultContext : DbContext { private string ConnectStr { get; } public DefaultContext(string connectStr) { ConnectStr = connectStr; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite(ConnectStr);//如果需要别的数据库,在这里进行修改 } } }
    2. EF Core 批量加载模型
    通常情况下,在使用ORM的时候,我们不希望过度的使用特性来标注实体类。因为如果后期需要变更ORM或者出现其他变动的时候,使用特性来标注实体类的话,会导致迁移变得复杂。而且大部分ORM框架的特性都依赖于框架本身,并非是统一的特性结构,这样就会造成一个后果:本来应该是对调用方隐藏的实现就会被公开,而且在项目引用关系中容易出现循环引用。
    所以,我在开发中会寻找是否支持配置类,如果使用配置类或者在ORM框架中设置映射关系,那么就可以保证数据层的纯净,也能实现对调用方隐藏实现。
    EF Core的配置类我们在《C# 数据访问系列》中关于EF的文章中介绍过,这里就不做过多介绍了(没来得及看的小伙伴们不着急,后续会有一个简单版的介绍)。
    通常情况下,配置类我也会放在Domain.Implements项目中。现在我给大家介绍一下如何快速批量加载配置类: protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetAssembly(this.GetType()), t => t.GetInterfaces().Any(i => t.Name.Contains("IEntityTypeConfiguration"))); }
    现在版本的EF Core支持通过Assembly加载配置类,可以指定加载当前上下文类所在的Assembly,然后筛选实现接口中包含IEntityTypeConfiguration的类即可。
    3. 使用EF Core实现数据操作
    我们已经创建好了一个EF Context,那么现在就带领大家一起看一下,如何使用EF来实现 上一篇《「asp.net core」7 实战之 数据访问层定义》中介绍的数据访问接口:
    新建一个BaseRepository类,在Domain.Implements项目的Insfrastructure 目录下: using Domain.Infrastructure; using Microsoft.EntityFrameworkCore; namespace Domain.Implements.Insfrastructure { public abstract class BaseRepository : ISearchRepository, IModifyRepository where T : class { public DbContext Context { get; } protected BaseRepository(DbContext context) { Context = context; } } }
    先创建以上内容,这里给Repository传参的时候,使用的是EFCore的默认Context类不是我们自己定义的。这是我个人习惯,实际上并没有其他影响。主要是为了对实现类隐藏具体的EF 上下文实现类。
    在实现各接口方法之前,创建如下属性: public DbSet Set { get => Context.Set(); }
    这是EF操作数据的核心所在。
    3.1 实现IModifyRepository接口
    先实现修改接口: public T Insert(T entity) { return Set.Add(entity).Entity; } public void Insert(params T[] entities) { Set.AddRange(entities); } public void Insert(IEnumerable entities) { Set.AddRange(entities); } public void Update(T entity) { Set.Update(entity); } public void Update(params T[] entities) { Set.UpdateRange(entities); } public void Delete(T entity) { Set.Remove(entity); } public void Delete(params T[] entities) { Set.RemoveRange(entities); }
    在修改接口里,我预留了几个方法没有实现,因为这几个方法使用EF Core自身可以实现,但实现会比较麻烦,所以这里借助一个EF Core的插件: dotnet add package Z.EntityFramework.Plus.EFCore
    这是一个免费开源的插件,可以直接使用。在Domain.Implements 中添加后,在BaseRepository 中添加如下引用: using System.Linq; using System.Linq.Expressions;
    实现方法: public void Update(Expression> predicate, Expression> updator) { Set.Where(predicate).UpdateFromQuery(updator); } public void Delete(Expression> predicate) { Set.Where(predicate).DeleteFromQuery(); } public void DeleteByKey(object key) { Delete(Set.Find(key)); } public void DeleteByKeys(params object[] keys) { foreach (var k in keys) { DeleteByKey(k); } }
    这里根据主键删除的方法有个问题,我们无法根据条件进行删除,实际上如果约定泛型T是BaseEntity的子类,我们可以获取到主键,但是这样又会引入另一个泛型,为了避免引入多个泛型根据主键的删除就采用了这种方式。
    3.2 实现ISearchRepository 接口
    获取数据以及基础统计接口: public T Get(object key) { return Set.Find(key); } public T Get(Expression> predicate) { return Set.SingleOrDefault(predicate); } public int Count() { return Set.Count(); } public long LongCount() { return Set.LongCount(); } public int Count(Expression> predicate) { return Set.Count(predicate); } public long LongCount(Expression> predicate) { return Set.LongCount(predicate); } public bool IsExists(Expression> predicate) { return Set.Any(predicate); }
    这里有一个需要关注的地方,在使用条件查询单个数据的时候,我使用了SingleOrDefault而不是FirstOrDefault。这是因为我在这里做了规定,如果使用条件查询,调用方应该能预期所使用条件是能查询出最多一条数据的。不过,这里可以根据实际业务需要修改方法: Single 返回单个数据,如果数据大于1或者等于0,则抛出异常 SingleOrDefault 返回单个数据,如果结果集没有数据,则返回null,如果多于1,则抛出异常 First 返回结果集的第一个元素,如果结果集没有数据,则抛出异常 FirstOrDefault 返回结果集的第一个元素,如果没有元素则返回null
    实现查询方法: public List Search() { return Query().ToList(); } public List Search(Expression> predicate) { return Query(predicate).ToList(); } public IEnumerable Query() { return Set; } public IEnumerable Query(Expression> predicate) { return Set.Where(predicate); } public List Search

    (Expression> predicate, Expression> order) { return Search(predicate, order, false); } public List Search

    (Expression> predicate, Expression> order, bool isDesc) { var source = Set.Where(predicate); if (isDesc) { source = source.OrderByDescending(order); } else { source = source.OrderBy(order); } return source.ToList(); }
    这里我尽量通过调用了参数最多的方法来实现查询功能,这样有一个好处,小伙伴们可以想一下哈。当然了,这是我自己觉得这样会好一点。
    实现分页:
    在实现分页之前,我们知道当时我们定义的分页参数类的排序字段用的是字符串,而不是lambda表达式,而Linq To EF需要一个Lambda表示才可以进行排序。这里就有两种方案,可以自己写一个方法,实现字符串到Lambda表达式的转换;第二种就是借用三方库来实现,正好我们之前引用的EF Core增强插件里有这个功能: var list = context.Customers.OrderByDescendingDynamic(x => "x.Name").ToList();
    这是它给出的示例。
    我们可以先依此来写一份实现方法: public PageModel Search(PageCondition condition) { var result = new PageModel { TotalCount = LongCount(condition.Predicate), CurrentPage = condition.CurrentPage, PerpageSize = condition.PerpageSize, }; var source = Query(condition.Predicate); if (condition.Sort.ToUpper().StartsWith("a")) // asc { source = source.OrderByDynamic(t => $"t.{condition.OrderProperty}"); } else // desc { source = source.OrderByDescendingDynamic(t => $"t.{condition.OrderProperty}"); } var items = source.Skip((condition.CurrentPage -1)* condition.PerpageSize).Take(condition.PerpageSize); result.Items = items.ToList(); return result; }
    回到第一种方案:
    我们需要手动写一个字符串的处理方法,先在Utils项目创建以下目录:Extend>Lambda,并在目录中添加一个ExtLinq类,代码如下: using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; namespace Utils.Extend.Lambda { public static class ExtLinq { public static IQueryable CreateOrderExpression(this IQueryable source, string orderBy, string orderAsc) { if (string.IsNullOrEmpty(orderBy)|| string.IsNullOrEmpty(orderAsc)) return source; var isAsc = orderAsc.ToLower() == "asc"; var _order = orderBy.Split(','); MethodCallExpression resultExp = null; foreach (var item in _order) { var orderPart = item; orderPart = Regex.Replace(orderPart, @"\s+", " "); var orderArry = orderPart.Split(' '); var orderField = orderArry[0]; if (orderArry.Length == 2) { isAsc = orderArry[1].ToUpper() == "ASC"; } var parameter = Expression.Parameter(typeof(T), "t"); var property = typeof(T).GetProperty(orderField); var propertyAccess = Expression.MakeMemberAccess(parameter, property); var orderByExp = Expression.Lambda(propertyAccess, parameter); resultExp = Expression.Call(typeof(Queryable), isAsc ? "OrderBy" : "OrderByDescending", new[] {typeof(T), property.PropertyType}, source.Expression, Expression.Quote(orderByExp)); } return resultExp == null ? source : source.Provider.CreateQuery(resultExp); } } }
    暂时不用关心为什么这样写,后续会为大家分析的。
    然后回过头来再实现我们的分页,先添加Utils 到Domain.Implements项目中 cd ../Domain.Implements # 进入Domain.Implements 项目目录 dotnet add reference ../Utils public PageModel Search(PageCondition condition) { var result = new PageModel { TotalCount = LongCount(condition.Predicate), CurrentPage = condition.CurrentPage, PerpageSize = condition.PerpageSize, }; var source = Set.Where(condition.Predicate).CreateOrderExpression(condition.OrderProperty, condition.Sort); var items = source.Skip((condition.CurrentPage -1)* condition.PerpageSize).Take(condition.PerpageSize); result.Items = items.ToList(); return result; }
    记得添加引用: using Utils.Extend.Lambda;
    在做分页的时候,因为前台传入的参数大多都是字符串的排序字段,所以到后端需要进程字符串到字段的处理。这里的处理利用了C# Expression的一个技术,这里就不做过多介绍了。后续在.net core高级篇中会有介绍。
    4. 总结
    到目前为止,看起来我们已经成功实现了利用EF Core为我们达成 数据操作和查询的目的。但是,别忘了EF Core需要手动调用一个SaveChanges方法。下一篇,我们将为大家介绍如何优雅的执行SaveChanges方法。
    这一篇介绍到这里,虽然说明不是很多,但是这也是我在开发中总结的经验。 更多内容烦请关注 我的博客《高先生小屋》

    软件开发
    2020-06-17 10:38:00
    Hi,大家好,我是明哥。
    在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。 我的在线博客: http://golang.iswbm.com 我的 Github:github.com/iswbm/GolangCodingTime
    说到Go语言,很多没接触过它的人,对它的第一印象,一定是它从语言层面天生支持并发,非常方便,让开发者能快速写出高性能且易于理解的程序。
    在 Python (为Py为例,主要是我比较熟悉,其他主流编程语言也类似)中,并发编程的门槛并不低,你要学习多进程,多线程,还要掌握各种支持并发的库 asyncio,aiohttp 等,同时你还要清楚它们之间的区别及优缺点,懂得在不同的场景选择不同的并发模式。
    而 Golang 作为一门现代化的编程语言,它不需要你直面这些复杂的问题。在 Golang 里,你不需要学习如何创建进程池/线程池,也不需要知道什么情况下使用多线程,什么时候使用多进程。因为你没得选,也不需要选,它原生提供的 goroutine (也即协程)已经足够优秀,能够自动帮你处理好所有的事情,而你要做的只是执行它,就这么简单。
    一个 goroutine 本身就是一个函数,当你直接调用时,它就是一个普通函数,如果你在调用前加一个关键字 go ,那你就开启了一个 goroutine。 // 执行一个函数 func() // 开启一个协程执行这个函数 go func()
    1. 协程的初步使用
    一个 Go 程序的入口通常是 main 函数,程序启动后,main 函数最先运行,我们称之为 main goroutine 。
    在 main 中或者其下调用的代码中才可以使用 go + func() 的方法来启动协程。
    main 的地位相当于主线程,当 main 函数执行完成后,这个线程也就终结了,其下的运行着的所有协程也不管代码是不是还在跑,也得乖乖退出。
    因此如下这段代码运行完,只会输出 hello, world ,而不会输出 hello, go (因为协程的创建需要时间,当 hello, world 打印后,协程还没来得及并执行) import "fmt" func mytest() { fmt.Println("hello, go") } func main() { // 启动一个协程 go mytest() fmt.Println("hello, world") }
    对于刚学习Go的协程同学来说,可以使用 time.Sleep 来使 main 阻塞,使其他协程能够有机会运行完全,但你要注意的是,这并不是推荐的方式(后续我们会学习其他更优雅的方式)。
    当我在代码中加入一行 time.Sleep 输出就符合预期了。 import ( "fmt" "time" ) func mytest() { fmt.Println("hello, go") } func main() { go mytest() fmt.Println("hello, world") time.Sleep(time.Second) }
    输出如下 hello, world hello, go
    2. 多个协程的效果
    为了让你看到并发的效果,这里举个最简单的例子 import ( "fmt" "time" ) func mygo(name string) { for i := 0; i < 10; i++ { fmt.Printf("In goroutine %sn", name) // 为了避免第一个协程执行过快,观察不到并发的效果,加个休眠 time.Sleep(10 * time.Millisecond) } } func main() { go mygo("协程1号") // 第一个协程 go mygo("协程2号") // 第二个协程 time.Sleep(time.Second) }
    输出如下,可以观察到两个协程就如两个线程一样,并发执行 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程2号 In goroutine 协程1号 In goroutine 协程2号
    通过以上简单的例子,是不是折服于Go的这种强大的并发特性,将同步代码转为异步代码,真的只要一个关键字就可以了,也不需要使用其他库,简单方便。
    本篇只介绍了协程的简单使用,真正的并发程序还是要结合 信道 (channel)来实现。关于信道的内容,将在下一篇文章中介绍。
    系列导读
    01. 开发环境的搭建(Goland & VS Code)
    02. 学习五种变量创建的方法
    **03. 详解数据类型:** 整形与浮点型
    04. 详解数据类型:byte、rune与string
    05. 详解数据类型:数组与切片
    06. 详解数据类型:字典与布尔类型
    07. 详解数据类型:指针
    08. 面向对象编程:结构体与继承
    09. 一篇文章理解 Go 里的函数
    10. Go语言流程控制:if-else 条件语句
    11. Go语言流程控制:switch-case 选择语句
    12. Go语言流程控制:for 循环语句
    13. Go语言流程控制:goto 无条件跳转
    14. Go语言流程控制:defer 延迟调用
    15. 面向对象编程:接口与多态
    16. 关键字:make 和 new 的区别?
    17. 一篇文章理解 Go 里的语句块与作用域
    18. 学习 Go 协程:goroutine
    19. 学习 Go 协程:详解信道/通道
    20. 几个信道死锁经典错误案例详解
    21. 学习 Go 协程:WaitGroup
    22. 学习 Go 协程:互斥锁和读写锁
    23. Go 里的异常处理:panic 和 recover
    24. 超详细解读 Go Modules 前世今生及入门使用
    25. Go 语言中关于包导入必学的 8 个知识点
    26. 如何开源自己写的模块给别人用?
    27. 说说 Go 语言中的类型断言?
    28. 这五点带你理解Go语言的select用法
    软件开发
    2020-06-01 08:13:00
    需求
    看一个披萨项目:要便于披萨种类扩展,要便于维护
    1、披萨种类有很多(比如:GreekPizza、CheesePizza等)
    2、披萨的制作有prepare,bake,cut,box
    3、完成披萨店订购功能
    基本介绍
    1)简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式
    2)简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)
    3)在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式.
    代码:
    pizza抽象类 /** * @ProjectName: DesignPattern * @Package: simplefactory * @Author: huat * @Date: 2020/6/16 16:57 * @Version: 1.0 */ public abstract class Pizza { //披萨名称 private String name; //因为每个披萨原材料不同,所以此处使用抽象方法 //准备原材料 public abstract void perpare(); //烤披萨 public void bake(){ System.out.println("bake...."+name); } //切披萨 public void cut(){ System.out.println("cut...."+name); } //打包披萨 public void box(){ System.out.println("box...."+name); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
    pizza对象类 /** * @ProjectName: DesignPattern * @Package: simplefactory * @Author: huat * @Date: 2020/6/16 17:45 * @Version: 1.0 */ public class GreekPizza extends Pizza { @Override public void perpare() { System.out.println("给希腊披萨准备原材料"); } } /** * @ProjectName: DesignPattern * @Package: simplefactory * @Author: huat * @Date: 2020/6/16 17:01 * @Version: 1.0 */ public class CheesePizza extends Pizza { @Override public void perpare() { System.out.println("给奶酪披萨准备原材料"); } }
    pizza工厂类 import simplefactory.pizza.CheesePizza; import simplefactory.pizza.GreekPizza; import simplefactory.pizza.Pizza; /** * @ProjectName: DesignPattern * @Package: simplefactory * @Author: huat * @Date: 2020/6/17 8:46 * @Version: 1.0 */ //简单工厂类 public class SimpleFactory { public Pizza createPizza(String type){ System.out.println("使用简单工厂模式"); Pizza pizza=null; if("cheesePizza".equalsIgnoreCase(type)){ pizza=new CheesePizza(); pizza.setName("奶酪披萨"); }else if("greekPizza".equalsIgnoreCase(type)){ pizza=new GreekPizza(); pizza.setName("希腊披萨"); } return pizza; } }
    订购披萨 import simplefactory.SimpleFactory; import simplefactory.pizza.Pizza; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * @ProjectName: DesignPattern * @Package: simplefactory.order * @Author: huat * @Date: 2020/6/16 17:55 * @Version: 1.0 */ public class OrderPizza { //初始化工厂类 SimpleFactory simpleFactory; //披萨 Pizza pizza=null; public void setSimpleFactory(SimpleFactory simpleFactory) throws IOException { String pizzaType=""; this.simpleFactory = simpleFactory; //循环订购披萨 do { pizzaType=getType(); pizza=this.simpleFactory.createPizza(pizzaType); if(null!=pizza){ pizza.perpare(); pizza.bake(); pizza.cut(); pizza.box(); }else{ System.out.println("暂无此pizza"); break; } }while (true); } //选择订购的披萨类型 private String getType() throws IOException { BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(System.in)); System.out.println("请输入你要订购的披萨"); String str=bufferedReader.readLine(); return str; } }
    测试类 import simplefactory.SimpleFactory; import simplefactory.order.OrderPizza; import java.io.IOException; /** * @ProjectName: DesignPattern * @Package: simplefactory.test * @Author: huat * @Date: 2020/6/17 14:46 * @Version: 1.0 */ public class TestPizza { public static void main(String[] args) throws IOException { OrderPizza orderPizza=new OrderPizza(); orderPizza.setSimpleFactory(new SimpleFactory()); } }
    软件开发
    2020-06-16 18:30:00
    最近发布的 Elasticsearch 6.3 包含了大家期待已久的 SQL 特性,今天给大家介绍一下具体的使用方法。
    首先看看接口的支持情况
    目前支持的 SQL 只能进行数据的查询只读操作,不能进行数据的修改,所以我们的数据插入还是要走之前的常规索引接口。
    目前 Elasticsearch 的支持 SQL 命令只有以下几个:
    命令 说明 DESC table 用来描述索引的字段属性
    SHOW COLUMNS 功能同上,只是别名
    SHOW FUNCTIONS 列出支持的函数列表,支持通配符?过滤
    SHOW TABLES
    SELECT .. FROM table_name WHERE .. GROUP BY .. HAVING .. ORDER BY .. LIMIT ..
    返回索引列表
    用来执行查询的命令
    我们分别来看一下各自怎么用,以及有什么效果吧,自己也可以动手试一下,看看。
    首先,我们创建一条数据: POST twitter /doc/ { "name" : "medcl" , "twitter" : "sql is awesome" , "date" : "2018-07-27" , "id" : 123 }
    RESTful下调用SQL
    在 ES 里面执行 SQL 语句,有三种方式,第一种是 RESTful 方式,第二种是 SQL-CLI 命令行工具,第三种是通过 JDBC 来连接 ES,执行的 SQL 语句其实都一样,我们先以 RESTful 方式来说明用法。
    RESTful 的语法如下: POST /_xpack/sql? format =txt { "query" : "SELECT * FROM twitter" }
    因为 SQL 特性是 xpack 的免费功能,所以是在 _xpack 这个路径下面,我们只需要把 SQL 语句传给 query 字段就行了,注意最后面不要加上 ; 结尾,注意是不要!
    我们执行上面的语句,查询返回的结果如下: date | id | name | twitter ------------------------+---------------+---------------+--------------- 2018-07-27T00:00:00.000Z| 123 |medcl | sql is awesome
    ES 俨然已经变成 SQL 数据库了,我们再看看如何获取所有的索引列表: POST /_xpack/sql? format =txt { "query" : "SHOW tables" }
    返回如下: name | type ---------------------------------+--------------- .kibana | BASE TABLE .monitoring-alerts- 6 |BASE TABLE .monitoring-es-6-2018.06.21 | BASE TABLE .monitoring-es- 6 - 2018.06 . 26 |BASE TABLE .monitoring-es-6-2018.06.27 | BASE TABLE .monitoring-kibana- 6 - 2018.06 . 21 |BASE TABLE .monitoring-kibana-6-2018.06.26 | BASE TABLE .monitoring-kibana- 6 - 2018.06 . 27 |BASE TABLE .monitoring-logstash-6-2018.06.20| BASE TABLE .reporting- 2018.06 . 24 |BASE TABLE .triggered_watches | BASE TABLE .watcher-history- 7 - 2018.06 . 20 |BASE TABLE .watcher-history-7-2018.06.21 | BASE TABLE .watcher-history- 7 - 2018.06 . 26 |BASE TABLE .watcher-history-7-2018.06.27 | BASE TABLE .watches |BASE TABLE apache_elastic_example | BASE TABLE forum-mysql |BASE TABLE twitter
    有点多,我们可以按名称过滤,如 twitt 开头的索引,注意通配符只支持 % 和 _ ,分别表示多个和单个字符(什么,不记得了,回去翻数据库的书去!): POST /_xpack/sql? format =txt { "query" : "SHOW TABLES 'twit%'" } POST /_xpack/sql? format =txt { "query" : "SHOW TABLES 'twitte_'" }
    上面返回的结果都是: name | type ---------------+--------------- twitter |BASE TABLE
    如果要查看该索引的字段和元数据,如下: POST /_xpack/sql? format =txt { "query" : "DESC twitter" }
    返回: column | type ---------------+--------------- date |TIMESTAMP id |BIGINT name |VARCHAR name.keyword |VARCHAR twitter |VARCHAR twitter.keyword|VARCHAR
    都是动态生成的字段,包含了 .keyword 字段。 还能使用下面的命令来查看,主要是兼容 SQL 语法。 POST /_xpack/sql? format =txt { "query" : "SHOW COLUMNS IN twitter" }
    另外,如果不记得 ES 支持哪些函数,只需要执行下面的命令,即可得到完整列表: SHOW FUNCTIONS
    返回结果如下,也就是当前6.3版本支持的所有函数,如下: name | type ----------------+--------------- AVG |AGGREGATE COUNT |AGGREGATE MAX |AGGREGATE MIN |AGGREGATE SUM |AGGREGATE STDDEV_POP |AGGREGATE VAR_POP |AGGREGATE PERCENTILE |AGGREGATE PERCENTILE_RANK |AGGREGATE SUM_OF_SQUARES |AGGREGATE SKEWNESS |AGGREGATE KURTOSIS |AGGREGATE DAY_OF_MONTH |SCALAR DAY |SCALAR DOM |SCALAR DAY_OF_WEEK |SCALAR DOW |SCALAR DAY_OF_YEAR |SCALAR DOY |SCALAR HOUR_OF_DAY |SCALAR HOUR |SCALAR MINUTE_OF_DAY |SCALAR MINUTE_OF_HOUR |SCALAR MINUTE |SCALAR SECOND_OF_MINUTE|SCALAR SECOND |SCALAR MONTH_OF_YEAR |SCALAR MONTH |SCALAR YEAR |SCALAR WEEK_OF_YEAR |SCALAR WEEK |SCALAR ABS |SCALAR ACOS |SCALAR ASIN |SCALAR ATAN |SCALAR ATAN2 |SCALAR CBRT |SCALAR CEIL |SCALAR CEILING |SCALAR COS |SCALAR COSH |SCALAR COT |SCALAR DEGREES |SCALAR E |SCALAR EXP |SCALAR EXPM1 |SCALAR FLOOR |SCALAR LOG |SCALAR LOG10 |SCALAR MOD |SCALAR PI |SCALAR POWER |SCALAR RADIANS |SCALAR RANDOM |SCALAR RAND |SCALAR ROUND |SCALAR SIGN |SCALAR SIGNUM |SCALAR SIN |SCALAR SINH |SCALAR SQRT |SCALAR TAN |SCALAR SCORE |SCORE
    同样支持通配符进行过滤: POST /_xpack/sql? format =txt { "query" : "SHOW FUNCTIONS 'S__'" }
    结果: name | type ---------------+--------------- SUM |AGGREGATE SIN |SCALAR
    那如果要进行模糊搜索呢,Elasticsearch 的搜索能力大家都知道,强!在 SQL 里面,可以用 match 关键字来写,如下: POST /_xpack/sql? format =txt { "query" : "SELECT SCORE(), * FROM twitter WHERE match(twitter, 'sql is') ORDER BY id DESC" }
    最后,还能试试 SELECT 里面的一些其他操作,如过滤,别名,如下: POST /_xpack/sql? format =txt { "query" : "SELECT SCORE() as score,name as myname FROM twitter as mytable where name = 'medcl' OR name ='elastic' limit 5" }
    结果如下: score | myname ---------------+--------------- 0.2876821 |medcl
    或是分组和函数计算: POST /_xpack/sql? format =txt { "query" : "SELECT name,max(id) as max_id FROM twitter as mytable group by name limit 5" }
    结果如下: name | max_id ---------------+--------------- medcl |123.0
    SQL-CLI下的使用
    上面的例子基本上把 SQL 的基本命令都介绍了一遍,很多情况下,用 RESTful 可能不是很方便,那么可以试试用 CLI 命令行工具来执行 SQL 语句,妥妥的 SQL 操作体验。
    切换到命令行下,启动 cli 程序即可进入命令行交互提示界面,如下: ➜ elasticsearch- 6.3 . 0 ./bin/elasticsearch-sql-cli .sssssss. ` .sssssss. .:sXXXXXXXXXXo` `ohXXXXXXXXXho. .yXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXX- .XXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXX. .XXXXXXXXXXXXXXXXXXXXo. .oXXXXXXXXXXXXXXXXXXXXh .XXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXy ` yXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX. `oXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXXXXXo` .XXXXXXXXXXXXXXXXXXXXXXXXXo ` .oXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXo` `odo` `oXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXo` `yXXXXXXXXXXXXXXXXXXXXXXXo` oXXXXXXXXXXXXXXXXX. .XXXXXXXXXXXXXXXXXXXXXXo ` ` oXXXXXXXXXXXXXXXXXXXy .XXXXXXXXXXXXXXXXXXXXo ` /XXXXXXXXXXXXXXXXXXXXX .XXXXXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXXXX- -XXXXXXXXXXXXXXXo` `oXXXXXXXXXXXXXXXo` .oXXXXXXXXXXXo ` ` oXXXXXXXXXXXo. `.sshXXyso` SQL `.sshXhss.` sql>
    当你看到一个硕大的创口贴,表示 SQL 命令行已经准备就绪了,查看一下索引列表,不,数据表的列表:
    [attach]2546[/attach]
    各种操作妥妥的,上面已经测试过的命令就不在这里重复了,只是体验不一样罢了。
    如果要连接远程的 ES 服务器,只需要启动命令行工具的时候,指定服务器地址,如果有加密,指定 keystone 文件,完整的帮助如下: ➜ elasticsearch-6.3.0 ./bin/elasticsearch-sql-cli --help Elasticsearch SQL CLI Non-option arguments: uri Option Description ------ ----------- -c, --check Enable initial connection check on startup (default: true) -d, --debug Enable debug logging -h, --help show help -k, --keystore_location Location of a keystore to use when setting up SSL. If specified then the CLI will prompt for a keystore password. If specified when the uri isn't https then an error is thrown. -s, --silent show minimal output -v, --verbose show verbose output
    JDBC 对接
    JDBC 对接的能力,让我们可以与各个 SQL 生态系统打通,利用众多现成的基于 SQL 之上的工具来使用 Elasticsearch,我们以两个工具来举例。
    和其他数据库一样,要使用 JDBC,要下载该数据库的 JDBC 的驱动,我们打开: https://www.elastic.co/downloads/jdbc-client
    只有一个 zip 包下载链接,下载即可。
    然后,我们这里使用 DbVisualizer 来连接 ES 进行操作,这是一个数据库的操作和分析工具,DbVisualizer 下载地址是: https://www.dbvis.com/ 。
    下载安装启动之后的程序主界面如下图:
    我们如果要使用 ES 作为数据源,我们第一件事需要把 ES 的 JDBC 驱动添加到 DbVisualizer 的已知驱动里面。我们打开 DbVisualizer 的菜单【Tools】-> 【Driver Manager】,打开如下设置窗口:
    点击绿色的加号按钮,新增一个名为 Elasticsearch-SQL 的驱动,url format 设置成 jdbc:es: ,如下图:
    然后点击上图黄色的文件夹按钮,添加我们刚刚下载好且解压之后的所有 jar 文件,如下:
    添加完成之后,如下图:
    就可以关闭这个 JDBC 驱动的管理窗口了。下面我们来连接到 ES 数据库。
    选择主程序左侧的新建连接图标,打开向导,如下:
    选择刚刚加入的 Elasticsearch-SQL 驱动:
    设置连接字符串,此处没有登录信息,如果有可以对应的填上:
    点击 Connect ,即可连接到 ES,左侧导航可以展开看到对应的 ES 索引信息:
    同样可以查看相应的库表结果和具体的数据:
    用他自带的工具执行 SQL 也是不在话下:
    同理,各种 ETL 工具和基于 SQL 的 BI 和可视化分析工具都能把 Elasticsearch 当做 SQL 数据库来连接获取数据了。
    最后一个小贴士,如果你的索引名称包含横线,如 logstash-201811,只需要做一个用双引号包含,对双引号进行转义即可,如下:
    关于 SQL 操作的文档在这里:
    https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-jdbc.html
    软件开发
    2020-06-16 18:04:00
    本文列出53个Python面试问题,并且提供了答案,供数科学家和软件工程师们参考。
    不久前,我作为“数据科学家”开始担任一个新的角色,实际上就是一位“Python工程师”。
    如果我在面试前提前了解一下Python的线程生命周期,而不是它的Recommender System(推荐系统)的话,我可能会在面试中表现得更好。
    为了帮助大家通过面试,下面我整理了我为Python面试/工作准备的问题,并提供了答案。大多数数据科学家都会编写大量的代码,所以这些问题/答案对科学家和工程师都同样适用。
    无论你是一位面试官、还是准备应聘一份工作、或者只是想提高你的Python技能,这份清单对你来说都将是无价之宝。
    问题是无序的。我们开始吧。
    1. 列表(list)和元组(tuple)有什么区别?
    在我每一次应聘Python数据科学家的面试中,这个问题都会被问到。所以对这个问题的答案,我可以说是了如指掌。
    列表是可变的。创建后可以对其进行修改。
    元组是不可变的。元组一旦创建,就不能对其进行更改。
    列表表示的是顺序。它们是有序序列,通常是同一类型的对象。比如说按创建日期排序的所有用户名,如 ["Seth", "Ema", "Eli"] 。
    元组表示的是结构。可以用来存储不同数据类型的元素。比如内存中的数据库记录,如 (2, "Ema", "2020–04–16")(#id, 名称,创建日期) 。
    2. 如何进行字符串插值?
    在不导入Template类的情况下,有3种方法进行字符串插值。 name = 'Chris'# 1. f stringsprint(f'Hello {name}')# 2. % operatorprint('Hey %s %s' % (name, name))# 3. formatprint( "My name is {}".format((name)))
    3. “is”和“==”有什么区别?
    在我的Python职业生涯的早期,我认为它们是相同的,因而制造了一些bug。所以请大家听好了,“is”用来检查对象的标识(id),而“==”用来检查两个对象是否相等。
    我们将通过一个例子说明。创建一些列表并将其分配给不同的名字。请注意,下面的b指向与a相同的对象。 a = [1,2,3]b = ac = [1,2,3]
    下面来检查是否相等,你会注意到结果显示它们都是相等的。 print(a == b)print(a == c)#=> True#=> True
    但是它们具有相同的标识(id)吗?答案是不。 print(a is b)print(a is c)#=> True#=> False
    我们可以通过打印他们的对象标识(id)来验证这一点。 print(id(a))print(id(b))print(id(c))#=> 4369567560#=> 4369567560#=> 4369567624
    你可以看到:c和a和b具有不同的标识(id)。
    4. 什么是装饰器(decorator)?
    这是每次面试我都会被问到的另一个问题。它本身就值得写一篇文章。如果你能自己用它编写一个例子,那么说明你已经做好了准备。
    装饰器允许通过将现有函数传递给装饰器,从而向现有函数添加一些额外的功能,该装饰器将执行现有函数的功能和添加的额外功能。
    我们将编写一个装饰器,该装饰器会在调用另一个函数时记录日志。
    编写装饰器函数logging。它接受一个函数func作为参数。它还定义了一个名为log_function_called的函数,它先执行打印出一些“函数func被调用”的信息(print(f'{func} called.')),然后调用函数func。最后返回定义的函数。 def logging(func): def log_function_called: print(f'{func} called.') func return log_function_called
    让我们编写其他两个函数,我们最终会将装饰器添加到其中(但还没有)。 def my_name: print('chris')def friends_name: print('naruto')my_namefriends_name#=> chris#=> naruto
    现在将装饰器添加到上面编写的两个函数之中。 @loggingdef my_name: print('chris')@loggingdef friends_name: print('naruto')my_namefriends_name#=> called.#=> chris#=> called.#=> naruto
    现在,你了解了如何仅仅通过在其上面添加@logging(装饰器),就能够轻松地将日志添加到我们编写的任何函数中。
    5. 解释Range函数
    Range函数可以用来创建一个整数列表,一般用在for循环中。它有3种使用方法。
    Range函数可以接受1到3个参数,参数必须是整数。
    请注意:我已经将range的每种用法包装在一个递推式构造列表(list comprehension)中,以便我们可以看到生成的值。
    用法1 - range(stop):生成从0到参数“stop”之间的整数。 [i for i in range(10)]#=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    用法2 - range(start, stop) : 生成从参数“start”到“stop”之间的整数 [i for i in range(2,10)]#=> [2, 3, 4, 5, 6, 7, 8, 9]
    用法3 - range(start, stop, step):以参数“step”为步长,生成从“start”到“stop”之间的整数。 [i for i in range(2,10,2)]#=> [2, 4, 6, 8]
    6. 定义一个名为car的类,它有两个属性:“color”和“speed”。然后创建一个实例并返回“speed”。 class Car : def __init__(self, color, speed): self.color = color self.speed = speedcar = Car('red','100mph')car.speed#=> '100mph'
    7. Python中的实例方法、静态方法和类方法有什么区别?
    实例方法:接受self参数,并且与类的特定实例相关。
    静态方法:使用装饰器 @staticmethod ,与特定实例无关,并且是自包含的(不能修改类或实例的属性)。
    类方法:接受cls参数,并且可以修改类本身。
    我们将通过一个虚构的CoffeeShop类来说明它们之间的区别。 class CoffeeShop: specialty = 'espresso' def __init__(self, coffee_price): self.coffee_price = coffee_price # instance method def make_coffee(self): print(f'Making {self.specialty} for ${self.coffee_price}') # static method @staticmethod def check_weather: print('Its sunny') # class method @classmethod def change_specialty(cls, specialty): cls.specialty = specialty print(f'Specialty changed to {specialty}')
    CoffeeShop类有一个属性specialty,默认值设为“espresso”。CoffeeShop类的每个实例初始化时都使用了coffee_price这个属性。同时,它还有3个方法,一个实例方法,一个静态方法和一个类方法。
    让我们将coffee_price的值设为5,来初始化CoffeeShop的一个实例。然后调用实例方法make_coffee。 coffee_shop = CoffeeShop('5')coffee_shop.make_coffee#=> Making espresso for $5
    现在我们来调用静态方法。静态方法无法修改类或实例状态,因此通常用于工具函数,例如,把2个数字相加。我们这里用它来检查天气。天气晴朗。太好了! coffee_shop.check_weather#=> Its sunny
    现在让我们使用类方法修改CoffeeShop的属性specialty,然后调用make_coffee方法来制作咖啡。 coffee_shop.change_specialty('drip coffee')#=> Specialty changed to drip coffeecoffee_shop.make_coffee#=> Making drip coffee for $5
    注意,make_coffee过去是用来做意式浓缩咖啡(espresso)的,但现在用来做滴滤咖啡(drip coffee)了!
    8. “func”和“ func”有什么区别?
    这个问题的目的是想看看你是否理解所有函数也是Python中的对象。 def func: print('Im a function') func#=> function __main__.func>func #=> Im a function
    func是表示函数的对象,它可以被分配给变量或传递给另一个函数。带圆括号的func调用该函数并返回其输出。
    9. 解释map函数的工作原理。
    Map函数返回一个列表,该列表由对序列中的每个元素应用一个函数时返回的值组成。 def add_three(x): return x + 3li = [1,2,3][i for i in map(add_three, li)]#=> [4, 5, 6]
    上面,我对列表中的每个元素的值加了3。
    10. 解释reduce函数的工作原理。
    这个问题很棘手,在你使用过它几次之前,你得努力尝试自己能够理解它。
    reduce接受一个函数和一个序列,然后对序列进行迭代。在每次迭代中,当前元素和前一个元素的输出都传递给函数。最后,返回一个值。 from functools import reducedef add_three(x,y): return x + y li = [1,2,3,5] reduce(add_three, li)#=> 11
    返回11,它是1 + 2 + 3 + 5的总和。
    11.解释filter函数的工作原理
    Filter函数顾名思义,是用来按顺序过滤元素。
    每个元素都被传递给一个函数,如果函数返回True,则在输出序列中返回该元素;如果函数返回False,则将其丢弃。 def add_three(x): if x % 2 == 0: return True else: return Falseli = [1,2,3,4,5,6,7,8][i for i in filter(add_three, li)]#=> [2, 4, 6, 8]
    注意上面所有不能被2整除的元素如何被删除的。
    12. Python是按引用调用还是按值调用?
    如果你在谷歌上搜索这个问题并阅读前几页,你就要准备好进入语义的迷宫了。你最好只是了解它的工作原理。
    不可变对象(如字符串、数字和元组等)是按值调用的。请注意下面的例子,当在函数内部修改时,name的值在函数外部不会发生变化。name的值已分配给内存中该函数作用域的新块。 name = 'chr'def add_chars(s): s += 'is' print(s)add_chars(name) print(name)#=> chris#=> chr
    可变对象(如列表等)是通过引用调用的。注意下面的例子中,函数外部定义的列表在函数内部的修改是如何影响到函数外部的。函数中的参数指向内存中存储li值的原始块。 li = [1,2]def add_element(seq): seq.append(3) print(seq)add_element(li) print(li)#=> [1, 2, 3]#=> [1, 2, 3]
    13. 如何使用reverse函数反转一个列表?
    下面的代码对一个列表调用reverse函数,对其进行修改。该方法没有返回值,但是会对列表的元素进行反向排序。 li = ['a','b','c']print(li)li.reverseprint(li)#=> ['a', 'b', 'c']#=> ['c', 'b', 'a']
    14. 字符串乘法是如何工作的?
    让我们看看将字符串" cat"乘以3的结果。 'cat' * 3#=> 'catcatcat'
    该字符串将自身连接3次。
    15. 列表乘法是如何工作的?
    我们来看看将列表[1,2,3]乘以2的结果。 [1,2,3] * 2#=> [1, 2, 3, 1, 2, 3]
    输出的列表包含了重复两次的列表[1,2,3]的内容。
    16. 类中的“self”指的是什么?
    “self”引用类本身的实例。这就是我们赋予方法访问权限并且能够更新方法所属对象的能力。
    下面,将self传递给__init__,使我们能够在初始化时设置实例的颜色。 class Shirt: def __init__(self, color): self.color = color s = Shirt('yellow')s.color#=> 'yellow'
    17. 如何在Python中连接列表?
    将2个列表相加,就是将它们连接在一起。但请注意,数组的工作方式不是这样的。 a = [1,2]b = [3,4,5] a + b#=> [1, 2, 3, 4, 5]
    18. 浅拷贝和深拷贝之间有什么区别?
    我们将在一个可变对象(列表)的上下文中讨论这个问题,对于不可变的对象,浅拷贝和深拷贝的区别并不重要。
    我们将介绍三种情况。
    1、引用原始对象。这将新对象li2指向li1所指向的内存中的同一位置。因此,我们对li1所做的任何更改也会在li2中发生。 li1 = [['a'],['b'],['c']]li2 = li1li1.append(['d'])print(li2)#=> [['a'], ['b'], ['c'], ['d']]
    2、创建原始对象的浅拷贝副本。我们可以使用list构造函数来实现这一点。浅拷贝创建一个新对象,但是用对原始对象的引用填充它。因此,向原始列表li3中添加新对象不会传播到li4中,但是修改li3中的一个对象将传播到li4中。 li3 = [['a'],['b'],['c']]li4 = list(li3)li3.append([4])print(li4)#=> [['a'], ['b'], ['c']]li3[0][0] = ['X']print(li4)#=> [[['X']], ['b'], ['c']]
    3、创建一个深拷贝副本。这是用copy.deepcopy完成的。现在,这两个对象是完全独立的,并且对其中一个对象所做的更改不会对另外一个对象产生影响。 import copyli5 = [['a'],['b'],['c']]li6 = copy.deepcopy(li5)li5.append([4])li5[0][0] = ['X']print(li6)#=> [['a'], ['b'], ['c']]
    19. 列表和数组有什么区别?
    注意:Python的标准库有一个array(数组)对象,但在这里,我特指常用的Numpy数组。
    列表存在于python的标准库中。数组由Numpy定义。
    列表可以在每个索引处填充不同类型的数据。数组需要同构元素。
    列表上的算术运算可从列表中添加或删除元素。数组上的算术运算按照线性代数方式工作。
    列表还使用更少的内存,并显著具有更多的功能。
    20. 如何连接两个数组?
    记住,数组不是列表。数组来自Numpy和算术函数,例如线性代数。
    我们需要使用Numpy的连接函数concatenate来实现。 import numpy as npa = np.array([1,2,3])b = np.array([4,5,6])np.concatenate((a,b))#=> array([1, 2, 3, 4, 5, 6])
    21. 你喜欢Python的什么?
    Python可读性很强,并且有一种Python方式可以处理几乎所有事情,这意味着它有一种简洁明了的首选方法。
    我将Python与Ruby进行对比,Ruby通常有很多种方法来做某事,但是没有指南说哪种方法是首选。
    22. 你最喜欢Python的哪个库?
    在处理大量数据时,没有什么比Pandas(熊猫)更有帮助了,因为Pandas让操作和可视化数据变得轻而易举。
    23. 举出几个可变和不可变对象的例子?
    不可变意味着创建后不能修改状态。例如:int、float、bool、string和tuple。
    可变意味着可以在创建后修改状态。例如列表(list)、字典(dict)和集合(set)。
    24. 如何将一个数字四舍五入到小数点后三位?
    使用round(value, decimal_places)函数。 a = 5.12345round(a,3)#=> 5.123
    25. 如何分割一个列表?
    分割语法使用3个参数, list[start:stop:step] ,其中step是返回元素的间隔。 a = [0,1,2,3,4,5,6,7,8,9]print(a[:2])#=> [0, 1]print(a[8:])#=> [8, 9]print(a[2:8])#=> [2, 3, 4, 5, 6, 7]print(a[2:8:2])#=> [2, 4, 6]
    26. 什么是pickling?
    Pickling是Python中序列化和反序列化对象的常用方法。
    在下面的示例中,我们对一个字典列表进行序列化和反序列化。 import pickleobj = [ {'id':1, 'name':'Stuffy'}, {'id':2, 'name': 'Fluffy'}]with open('file.p', 'wb') as f: pickle.dump(obj, f)with open('file.p', 'rb') as f: loaded_obj = pickle.load(f)print(loaded_obj)#=> [{'id': 1, 'name': 'Stuffy'}, {'id': 2, 'name': 'Fluffy'}]
    27. 字典和JSON有什么区别?
    Dict是Python的一种数据类型,是经过索引但无序的键和值的集合。
    JSON只是一个遵循指定格式的字符串,用于传输数据。
    28. 你在Python中使用了哪些ORM?
    ORM(对象关系映射)将数据模型(通常在应用程序中)映射到数据库表,并简化了数据库事务。
    SQLAlchemy通常用于Flask的上下文中,而Django拥有自己的ORM。
    29. any和all如何工作?
    Any接受一个序列,如果序列中的任何元素为true,则返回true。
    All只有当序列中的所有元素都为true时,才返回true。 a = [False, False, False]b = [True, False, False]c = [True, True, True]print( any(a) )print( any(b) )print( any(c) )#=> False#=> True#=> Trueprint( all(a) )print( all(b) )print( all(c) )#=> False#=> False#=> True
    30. 字典和列表的查找速度哪个更快?
    在列表中查找一个值需要O(n)时间,因为需要遍历整个列表,直到找到值为止。
    在字典中查找一个值只需要O(1)时间,因为它是一个哈希表。
    如果有很多值,这会造成很大的时间差异,因此通常建议使用字典来提高速度。但字典也有其他限制,比如需要唯一键。
    31. 模块(module)和包(package)有什么区别?
    模块是可以一起导入的文件(或文件集合)。 import sklearn
    包是模块的目录。 from sklearn import cross_validation
    因此,包是模块,但并非所有模块都是包。
    32. 如何在Python中递增和递减一个整数?
    可以使用“+=”和“-=”对整数进行递增和递减。 value = 5value += 1print(value)#=> 6value -= 1value -= 1print(value)#=> 4
    33. 如何返回一个整数的二进制值?
    使用bin函数。 bin(5)#=> '0b101'
    34. 如何从列表中删除重复的元素?
    可以通过将一个列表先转化为集合,然后再转化回列表来完成。 a = [1,1,1,2,3]a = list(set(a))print(a)#=> [1, 2, 3]
    35. 如何检查一个值是不是在列表中存在?
    使用“in”。 'a' in ['a','b','c'] #=> True'a' in [1,2,3]#=> False
    36. append和extend有什么区别?
    Append将一个值添加到一个列表中,而extend将另一个列表的值添加到一个列表中。 a = [1,2,3]b = [1,2,3]a.append(6)print(a)#=> [1, 2, 3, 6]b.extend([4,5])print(b)#=> [1, 2, 3, 4, 5]
    37. 如何取一个整数的绝对值?
    这可以通过abs函数来实现。 abs(2#=> 2 abs(-2)#=> 2
    38. 如何将两个列表组合成一个元组列表?
    可以使用zip函数将列表组合成一个元组列表。这不仅仅限于使用两个列表。也适合3个或更多列表的情况。 a = ['a','b','c']b = [1,2,3] [(k,v) for k,v in zip(a,b)]#=> [('a', 1), ('b', 2), ('c', 3)]
    39. 如何按字母顺序对字典进行排序?
    你不能对字典进行排序,因为字典没有顺序,但是你可以返回一个已排序的元组列表,其中包含字典中的键和值。 d = {'c':3, 'd':4, 'b':2, 'a':1} sorted(d.items)#=> [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
    40. 一个类如何继承Python的另一个类?
    在下面的示例中,Audi继承自Car。继承带来了父类的实例方法。 class Car: def drive(self): print('vroom')class Audi(Car): passaudi = Audiaudi.drive
    41. 如何删除字符串中的所有空白?
    最简单的方法是使用空白拆分字符串,然后将拆分成的字符串重新连接在一起。 s = 'A string with white space'''.join(s.split)#=> 'Astringwithwhitespace'
    42. 在迭代序列时,为什么要使用enumerate?
    enumerate允许在序列上迭代时跟踪索引。它比定义和递增一个表示索引的整数更具Python感。 li = ['a','b','c','d','e']for idx,val in enumerate(li): print(idx, val)#=> 0 a#=> 1 b#=> 2 c#=> 3 d#=> 4 e
    43. pass、continue和break之间有什么区别?
    pass意味着什么都不做。我们之所以通常使用它,是因为Python不允许在没有代码的情况下创建类、函数或if语句。
    在下面的例子中,如果在i>3中没有代码的话,就会抛出一个错误,因此我们使用pass。 a = [1,2,3,4,5]for i in a: if i > 3: pass print(i)#=> 1#=> 2#=> 3#=> 4#=> 5 Continue会继续到下一个元素并停止当前元素的执行。所以当i<3时,永远不会达到print(i)。 for i in a: if i < 3: continue print(i)#=> 3#=> 4#=> 5
    break会中断循环,序列不再重复下去。所以不会被打印3以后的元素。 for i in a: if i == 3: break print(i) #=> 1#=> 2
    44. 如何将for循环转换为使用递推式构造列表(list comprehension)?
    For循环如下: a = [1,2,3,4,5] a2 = for i in a: a2.append(i + 1)print(a2)#=> [2, 3, 4, 5, 6]
    用递推式构造列表来修改这个for循环,代码如下: a a3 = [i+1 for i in a] print(a3)#=> [2, 3, 4, 5, 6]
    递推式构造列表通常被认为更具Python风格,同时仍易于阅读。
    45. 举一个使用三元运算符的例子。
    三元运算符是一个单行的if/else语句。
    语法看起来像“if 条件 else b”。 x = 5y = 10'greater' if x > 6 else 'less'#=> 'less''greater' if y > 6 else 'less'#=> 'greater'
    46. 检查一个字符串是否仅仅包含数字?
    可以使用isnumeric方法。 '123abc...'.isalnum#=> False' 123abc'.isalnum#=> True
    47. 检查一个字符串是否仅仅包含字母?
    你可以使用isalpha。 '123a'.isalpha#=> False'a'.isalpha#=> True
    48. 检查字符串是否只包含数字和字母?
    你可以使用isalnum。 '123abc...'.isalnum#=> False'123abc'.isalnum#=> True
    49. 从字典返回键列表
    这可以通过将字典传递给Python的list构造函数list来完成。 d = {'id':7, 'name':'Shiba', 'color':'brown', 'speed':'very slow'}list(d)#=> ['id', 'name', 'color', 'speed']
    50. 如何将一个字符串转化为全大写和全小写?
    你可以使用upper和lower字符串方法。 small_word = 'potatocake'big_word = 'FISHCAKE'small_word.upper#=> 'POTATOCAKE'big_word.lower#=> 'fishcake'
    51. remove、del和pop有什么区别?
    remove 删除第一个匹配的值。 li = ['a','b','c','d'] li.remove('b')li#=> ['a', 'c', 'd']
    del按索引删除元素。 li = ['a','b','c','d'] del li[0]li#=> ['b', 'c', 'd']
    pop 按索引删除一个元素并返回该元素。 li = ['a','b','c','d'] li.pop(2)#=> 'c' li#=> ['a', 'b', 'd']
    52. 举一个递推式构造字典(dictionary comprehension)的例子
    下面我们将创建一个字典,其中字母表中的字母作为键,并以字母索引作为值。 # creating a list of lettersimport stringlist(string.ascii_lowercase)alphabet = list(string.ascii_lowercase)# list comprehensiond = {val:idx for idx,val in enumerate(alphabet)}d#=> {'a': 0,#=> 'b': 1,#=> 'c': 2,#=> ...#=> 'x': 23,#=> 'y': 24,#=> 'z': 25}
    53. Python中的异常处理是如何进行的?
    Python提供了3个关键字来处理异常,try、except和finally。
    语法如下: try: # try to do thisexcept: # if try block fails then do thisfinally: # always do this
    在下面的简单示例中,try块失败,因为我们不能将字符串添加到整数中。except块设置val=10,然后finally块打印出“complete”。 try: val = 1 + 'A'except: val = 10finally: print('complete') print(val)#=> complete#=> 10
    你永远不知道面试中会出现什么问题,最好的准备方法是拥有很多编写代码的经验。
    也就是说,这个列表应该涵盖Python所要求的数据科学家或初级/中级Python开发人员角色的大部分内容。
    我希望这对你一样有帮助。
    如果我漏掉了什么好问题,请让我知道。
    作者 | Chris
    原文 | https://towardsdatascience.com/53-python-interview-questions-and-answers-91fa311eec3f 文源网络,仅供学习之用,如有侵权请联系删除。
    在学习Python的道路上肯定会遇见困难,别慌,我这里有一套学习资料,包含40+本电子书,800+个教学视频,涉及Python基础、爬虫、框架、数据分析、机器学习等,不怕你学不会! https://shimo.im/docs/JWCghr8prjCVCxxK/ 《Python学习资料》
    关注公众号【Python圈子】,优质文章每日送达。
    软件开发
    2020-06-17 10:23:06
    >作者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文: https://www.jianshu.com/p/bbce8f04faf1
    > 《C语言探索之旅》 全系列
    内容简介 前言 变量的大小 内存的动态分配 动态分配一个数组 总结 第二部分第九课预告
    1. 前言
    上一课是 C语言探索之旅 | 第二部分第七课:文件读写 。
    经历了第二部分的一些难点课程,我们终于来到了这一课,一个听起来有点酷酷的名字: 动态分配 。
    >“万水千山总是情,分配也由系统定”。
    到目前为止,我们创建的变量都是系统的编译器为我们自动构建的,这是简单的方式。
    其实还有一种更偏手动的创建变量的方式,我们称为“动态分配”(Dynamic Allocation)。dynamic 表示“动态的”,allocation 表示“分配”。
    动态分配的一个主要好处就是可以在内存中“预置”一定空间大小,在编译时还不知道到底会用多少。
    使用这个技术,我们可以创建大小可变的数组。到目前为止我们所创建的数组都是大小固定不可变的。而学完这一课后我们就会创建所谓“动态数组”了。
    学习这一章需要对指针有一定了解,如果指针的概念你还没掌握好,可以回去复习 C语言探索之旅 | 第二部分第二课:进击的指针,C语言的王牌! 那一课。
    我们知道当我们创建一个变量时,在内存中要为其分配一定大小的空间。例如: int number = 2;
    当程序运行到这一行代码时,会发生几件事情: 应用程序询问 操作系统 (Operating System,简称 OS。例如Windows,Linux,macOS,Android,iOS,等)是否可以使用一小块内存空间。 操作系统回复我们的程序,告诉它可以将这个变量存储在内存中哪个地方(给出分配的内存地址)。 当函数结束后,你的变量会自动从内存中被删除。你的程序对操作系统说:“我已经不需要内存中的这块地址了,谢谢!” (当然,实际上你的程序不可能对操作系统说一声“谢谢”,但是确实是操作系统在掌管一切,包括内存,所以对它还是客气一点比较好...)。
    可以看到,以上的过程都是自动的。当我们创建一个变量,操作系统就会自动被程序这样调用。
    那么什么是手动的方式呢?说实在的,没人喜欢把事情复杂化,如果自动方式可行,何必要大费周章来使用什么手动方式呢?但是要知道,很多时候我们是不得不使用手动方式。
    这一课中,我们将会: 探究内存的机制(是的,虽然以前的课研究过,但是还是要继续深入),了解不同变量类型所占用的内存大小。 接着,探究这一课的主题,来学习如何向操作系统动态请求内存。也就是所谓的“动态内存分配”。 最后,通过学习如何创建一个在编译时还不知道其大小(只有在程序运行时才知道)的数组来了解动态内存分配的好处。
    准备好了吗?Let's Go !
    2. 变量的大小
    根据我们所要创建的变量的类型(char,int,double,等等),其所占的内存空间大小是不一样的。
    事实上,为了存储一个大小在 -128 至 127 之间的数(char 类型),只需要占用一个字节(8 个二进制位)的内存空间,是很小的。
    然而,一个 int 类型的变量就要占据 4 个字节了;一个 double 类型要占据 8 个字节。
    问题是:并不总是这样。
    什么意思呢?
    因为类型所占内存的大小还与操作系统有关系。不同的操作系统可能就不一样,32 位和 64 位的操作系统的类型大小一般会有区别。
    这一节中我们的目的是学习如何获知变量所占用的内存大小。
    有一个很简单的方法:使用 sizeof() 。
    虽然看着有点像函数,但其实 sizeof 不是一个函数,而是一个 C语言的关键字,也算是一个运算符吧。
    我们只需要在 sizeof 的括号里填入想要检测的变量类型,sizeof 就会返回所占用的字节数了。
    例如,我们要检测 int 类型的大小,就可以这样写: sizeof(int)
    在编译时, sizeof(int) 就会被替换为 int 类型所占用的字节数了。
    在我的电脑上, sizeof(int) 是 4,也就是说 int 类型在我的电脑的内存中占据 4 个字节。在你的电脑上,也许是 4,但也可能是其他的值。
    我们用一个例子来测试一下吧: // octet 是英语“字节”的意思,和 byte 类似 printf("char : %d octets\n", sizeof(char)); printf("int : %d octets\n", sizeof(int)); printf("long : %d octets\n", sizeof(long)); printf("double : %d octets\n", sizeof(double));
    在我的电脑(64 位)运行,输出: char : 1 octets int : 4 octets long : 8 octets double : 8 octets
    我们并没有测试所有已知的变量类型,你也可以课后自己去测试一下其他的类型,例如:short,float。
    曾几何时,当电脑的内存很小的年代,有这么多不同大小的变量类型可供选择是一件很好的事,因为我们可以选“够用的最小的”那种变量类型,以节约内存。
    现在,电脑的内存一般都很大,“有钱任性”么。所以我们在编程时也没必要太“拘谨”。不过在嵌入式领域,内存大小一般是有限的,我们就得斟酌着使用变量类型了。
    既然 sizeof 这么好用,我们可不可以用它来显示我们自定义的变量类型的大小呢?例如 struct,enum,union。
    是可以的。写一个程序测试一下: #include typedef struct Coordinate { int x; int y; } Coordinate; int main(int argc, char *argv[]) { printf("Coordinate 结构体的大小是 : %d 个字节\n", sizeof(Coordinate)); return 0; }
    运行输出: Coordinate 结构体的大小是 : 8 个字节
    对于内存的全新视角
    之前,我们在绘制内存图示时,还是比较不精准的。现在,我们知道了每个变量所占用的大小,我们的内存图示就可以变得更加精准了。
    假如我定义一个 int 类型的变量: int age = 17;
    我们用 sizeof 测试后得知 int 的大小为 4。假设我们的变量 age 被分配到的内存地址起始是 1700,那么我们的内存图示就如下所示:
    我们看到,我们的 int 型变量 age 在内存中占用 4 个字节,起始地址是 1700(它的内存地址),一直到 1703。
    如果我们对一个 char 型变量(大小是一个字节)同样赋值: char number = 17;
    那么,其内存图示是这样的:
    假如是一个 int 型的数组: int age[100];
    用 sizeof() 测试一下,就可以知道在内存中 age 数组占用 400 个字节。4 * 100 = 400。
    即使这个数组没有赋初值,但是在内存中仍然占据 400 个字节的空间。变量一声明,在内存中就为它分配一定大小的内存了。
    那么,如果我们创建一个类型是 Coordinate 的数组呢? Coordinate coordinate[100];
    其大小就是 8 * 100 = 800 个字节了。
    3. 内存的动态分配
    好了,现在我们就进入这一课的关键部分了,重提一次这一课的目的:学会如何手动申请内存空间。
    我们需要引入 stdlib.h 这个标准库头文件,因为接下来要使用的函数是定义在这个库里面。
    这两个函数是什么呢?就是: malloc:是 Memory Allocation 的缩写,表示“内存分配”。询问操作系统能否预支一块内存空间来使用。 free:表示“解放,释放,自由的”。意味着“释放那块内存空间”。告诉操作系统我们不再需要这块已经分配的空间了,这块内存空间会被释放,另一个程序就可以使用这块空间了。
    当我们手动分配内存时,须要按照以下三步顺序来: 调用 malloc 函数来申请内存空间。 检测 malloc 函数的返回值,以得知操作系统是否成功为我们的程序分配了这块内存空间。 一旦使用完这块内存,不再需要时,必须用 free 函数来释放占用的内存,不然可能会造成内存泄漏。
    以上三个步骤是不是让我们回忆起关于上一课“文件读写”的内容了?
    这三个步骤和文件指针的操作有点类似,也是先申请内存,检测是否成功,用完释放。
    malloc 函数:申请内存
    malloc 分配的内存是在堆上,一般的局部变量(自动分配的)大多是在栈上。
    关于堆和栈的区别,还有内存的其他区域,如静态区等,大家可以自己延伸阅读。
    之前“字符串”那一课里已经给出过一张图表了。再来回顾一下吧:
    名称 内容 代码段 可执行代码、字符串常量
    数据段 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据
    BSS段 未初始化全局变量,未初始化全局静态变量

    局部变量、函数参数
    动态内存分配
    给出 malloc 函数的原型,你会发现有点滑稽: void* malloc(size_t numOctetsToAllocate);
    可以看到,malloc 函数有一个参数 numOctetsToAllocate,就是需要申请的内存空间大小(用字节数表示),这里的 size_t(之前的课程有提到过)其实和 int 是类似的,就是一个 define 宏定义,实际上很多时候就是 int。
    对于我们目前的演示程序,可以将 sizeof(int) 置于 malloc 的括号中,表示要申请 int 类型的大小的空间。
    真正引起我们兴趣的是 malloc 函数的返回值: void*
    如果你还记得我们在函数那章所说的,void 表示“空”,我们用 void 来表示函数没有返回值。
    所以说,这里我们的函数 malloc 会返回一个指向 void 的指针,一个指向“空”(void 表示“虚无,空”)的指针,有什么意义呢?malloc 函数的作者不会搞错了吧?
    不要担心,这么做肯定是有理由的。
    >难道有人敢质疑老爷子 Dennis Ritchie(C语言的作者)的智商? 来人呐,拖出去... 罚写 100 个 C语言小游戏。
    事实上,这个函数返回一个指针,指向操作系统分配的内存的首地址。
    如果操作系统在 1700 这个地址为你开辟了一块内存的话,那么函数就会返回一个包含 1700 这个值的指针。
    但是,问题是:malloc 函数并不知道你要创建的变量是什么类型的。
    实际上,你只给它传递了一个参数: 在内存中你需要申请的字节数。
    如果你申请 4 个字节,那么有可能是 int 类型,也有可能是 long 类型。
    正因为 malloc 不知道自己应该返回什么变量类型(它也无所谓,只要分配了一块内存就可以了),所以它会返回 void* 这个类型。这是一个可以表示任意指针类型的指针。
    void* 与其他类型的指针之间可以通过强制转换来相互转换。例如: int *i = (int *)p; // p 是一个 void* 类型的指针 void *v = (void *)c; // c 是一个 char* 类型的指针
    实践
    如果我实际来用 malloc 函数分配一个 int 型指针: int *memoryAllocated = NULL; // 创建一个 int 型指针 memoryAllocated = malloc(sizeof(int)); // malloc 函数将分配的地址赋值给我们的指针 memoryAllocated
    经过上面的两行代码,我们的 int 型指针 memoryAllocated 就包含了操作系统分配的那块内存地址的首地址值。
    假如我们用之前我们的图示来举例,这个值就是 1700。
    检测指针
    既然上面我们用两行代码使得 memoryAllocated 这个指针包含了分配到的地址的首地址值,那么我们就可以通过检测 memoryAllocated 的值来判断申请内存是否成功了: 如果为 NULL,则说明 malloc 调用没有成功。 否则,就说明成功了。
    一般来说内存分配不会失败,但是也有极端情况: 你的内存(堆内存)已经不够了。 你申请的内存值大得离谱(比如你申请 64 GB 的内存空间,那我想大多数电脑都是不可能分配成功的)。
    希望大家每次用 malloc 函数时都要做指针的检测,万一真的出现返回值为 NULL 的情况,那我们需要立即停止程序,因为没有足够的内存,也不可能进行下面的操作了。
    为了中断程序的运行,我们来使用一个新的函数: exit()
    exit 函数定义在 stdlib.h 中,调用此函数会使程序立即停止。
    这个函数也只有一个参数,就是返回值,这和 return 函数的参数是一样原理的。实例: int main(int argc, char *argv[]) { int *memoryAllocated = NULL; memoryAllocated = malloc(sizeof(int)); if (memoryAllocated == NULL) // 如果分配内存失败 { exit(0); // 立即停止程序 } // 如果指针不为 NULL,那么可以继续进行接下来的操作 return 0; }
    另外一个问题:用 malloc 函数申请 0 字节内存会返回 NULL 指针吗?
    可以测试一下,也可以去查找关于 malloc 函数的说明文档。
    >申请 0 字节内存,函数并不返回 NULL,而是返回一个正常的内存地址。 但是你却无法使用这块大小为 0 的内存!
    这就好比尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。
    对于这一点一定要小心,因为这时候 if(NULL != p) 语句校验将不起作用。
    free函数:释放内存
    记得上一课我们使用 fclose 函数来关闭一个文件指针,也就是释放占用的内存。
    free 函数的原理和 fclose 是类似的,我们用它来释放一块我们不再需要的内存。原型: void free(void* pointer);
    free 函数只有一个目的:释放 pointer 指针所指向的那块内存。
    实例程序: int main(int argc, char *argv[]) { int* memoryAllocated = NULL; memoryAllocated = malloc(sizeof(int)); if (memoryAllocated == NULL) // 如果分配内存失败 { exit(0); // 立即停止程序 } // 此处添加使用这块内存的代码 free(memoryAllocated); // 我们不再需要这块内存了,释放之 return 0; }
    综合上面的三个步骤,我们来写一个完整的例子: #include #include int main(int argc, char *argv[]) { int* memoryAllocated = NULL; memoryAllocated = malloc(sizeof(int)); // 分配内存 if (memoryAllocated == NULL) // 检测是否分配成功 { exit(0); // 不成功,结束程序 } // 使用这块内存 printf("您几岁了 ? "); scanf("%d", memoryAllocated); printf("您已经 %d 岁了\n", *memoryAllocated); free(memoryAllocated); // 释放这块内存 return 0; }
    运行输出: 您几岁了 ? 32 您已经 32 岁了
    以上就是我们用动态分配的方式来创建了一个 int 型变量,使用它,释放它所占用的内存。
    但是,我们也完全可以用以前的方式来实现,如下: int main(int argc, char *argv[]) { int myAge = 0; // 分配内存 (自动) // 使用这块内存 printf("您几岁了 ? "); scanf("%d", &myAge); printf("你已经 %d 岁了\n", myAge); return 0; } // 释放内存 (在函数结束后自动释放)
    在这个简单使用场景下,两种方式(手动和自动)都是能完成任务的。
    总结说来,创建一个变量(说到底也就是分配一块内存空间)有两种方式:自动和手动。 自动:我们熟知并且一直使用到现在的方式。 手动(动态):这一课我们学习的内容。
    你可能会说:“我发现动态分配内存的方式既复杂又没什么用嘛!”
    复杂么?还行吧,确实相对自动的方式要考虑比较多的因素。
    没有用么?绝不!
    因为很多时候我们不得不使用手动的方式来分配内存。
    接下来我们就来看一下手动方式的必要性。
    4. 动态分配一个数组
    暂时我们只是用手动方式来创建了一个简单的变量。
    然而,一般说来,我们的动态分配可不是这样“大材小用”的。
    如果只是创建一个简单的变量,我们用自动的方式就够了。
    那你会问:“啥时候须要用动态分配啊?”
    问得好。动态分配最常被用来创建在运行时才知道大小的变量,例如动态数组。
    假设我们要存储一个用户的朋友的年龄列表,按照我们以前的方式(自动方式),我们可以创建一个 int 型的数组: int ageFriends[18];
    很简单对吗?那问题不就解决了?
    但是以上方式有两个缺陷: 你怎么知道这个用户只有 18 个朋友呢?可能他有更多朋友呢。 你说:“那好,我就创建一个数组: int ageFriends[10000];
    足够储存 1 万个朋友的年龄。”
    但是问题是:可能我们使用到的只是这个大数组的很小一部分,岂不是浪费内存嘛。
    最恰当的方式是询问用户他有多少朋友,然后创建对应大小的数组。
    而这样,我们的数组大小就只有在运行时才能知道了。
    Voila,这就是动态分配的优势了: 可以在运行时才确定申请的内存空间大小。 不多不少刚刚好,要多少就申请多少,不怕不够或过多。
    所以借着动态分配,我们就可以在运行时询问用户他到底有多少朋友。
    如果他说有 20 个,那我们就申请 20 个 int 型的空间;如果他说有 50 个,那就申请 50 个。经济又环保。
    我们之前说过,C语言中禁止用变量名来作为数组大小,例如不能这样: int ageFriends[numFriends]; // numFriends 是一个变量
    尽管有的 C编译器可能允许这样的声明,但是我们不推荐。
    我们来看看用动态分配的方式如何实现这个程序: #include #include int main(int argc, char *argv[]) { int numFriends = 0, i = 0; int *ageFriends= NULL; // 这个指针用来指示朋友年龄的数组 // 询问用户有多少个朋友 printf("请问您有多少朋友 ? "); scanf("%d", &numFriends); if (numFriends > 0) // 至少得有一个朋友吧,不然也太惨了 :P { ageFriends = malloc(numFriends * sizeof(int)); // 为数组分配内存 if (ageFriends== NULL) // 检测分配是否成功 { exit(0); // 分配不成功,退出程序 } // 逐个询问朋友年龄 for (i = 0 ; i < numFriends; i++) { printf("第%d位朋友的年龄是 ? ", i + 1); scanf("%d", &ageFriends[i]); } // 逐个输出朋友的年龄 printf("\n\n您的朋友的年龄如下 :\n"); for (i = 0 ; i < numFriends; i++) { printf("%d 岁\n", ageFriends[i]); } // 释放 malloc 分配的内存空间,因为我们不再需要了 free(ageFriends); } return 0; }
    运行输出: 请问您有多少朋友 ? 7 第1位朋友的年龄是 ? 25 第2位朋友的年龄是 ? 21 第3位朋友的年龄是 ? 27 第4位朋友的年龄是 ? 18 第5位朋友的年龄是 ? 14 第6位朋友的年龄是 ? 32 第7位朋友的年龄是 ? 30 您的朋友的年龄如下 : 25岁 21岁 27岁 18岁 14岁 32岁 30岁
    当然了,这个程序比较简单,但我向你保证以后的课程会使用动态分配来做更有趣的事。
    5. 总结 不同类型的变量在内存中所占的大小不尽相同。 借助 sizeof 这个关键字(也是运算符)可以知道一个类型所占的字节数。 动态分配就是在内存中手动地预留一块空间给一个变量或者数组。 动态分配的常用函数是 malloc(当然,还有 calloc,realloc,可以查阅使用方法,和 malloc 是类似的),但是在不需要这块内存之后,千万不要忘了使用 free 函数来释放。而且,malloc 和 free 要一一对应,不能一个 malloc 对应两个 free,会出错;或者两个 malloc 对应一个 free,会内存泄露! 动态分配使得我们可以创建动态数组,就是它的大小在运行时才能确定。
    6. 第二部分第九课预告
    今天的课就到这里,一起加油吧!
    下一课:  C语言探索之旅 | 第二部分第九课: 实战"悬挂小人"游戏
    >我是 谢恩铭 ,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师 ,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」
    软件开发
    2020-06-17 10:22:00
    在国外某社交网站上有一个关于迁移 Spring Boot 迁移 Maven 至 Gradle 的帖子:
    该贴子上也有很多人质疑:Maven 用的好好的,为什么要迁移至 Gradle?
    虽然该贴子只是说 Gradle 牛逼,但并没有说迁移至 Gradle 所带来的影响和价值。
    所以,Spring Boot 官方对此也发了博文作了解释: https://spring.io/blog/2020/06/08/migrating-spring-boot-s-build-to-gradle
    栈长简单概括一下。
    没错,Spring Boot 做了一个重大调整:
    在 Spring Boot 2.3.0.M1 中,将首次使用 Gradle 代替 Maven 来构建 Spring Boot 项目。
    为什么要迁移?
    Spring Boot 团队给出的主要原因是,迁移至 Gradle 可以 减少构建项目所花费的时间 。
    因为使用 Maven 构建,回归测试时间太长了,等待项目构建大大增加了修复 bug 和实现新特性的时间。
    而 Gradle 的宗旨是减少构建工作量,它可以根据需要构建任何有变化的地方或者并行构建。
    当然,Spring Boot 团队也花了很多时间来尝试用 Maven 进行 并行构建,但因为构建 Spring Boot 项目的复杂性,最终失败了。
    另外,Spring Boot 团队也看到了在其他 Spring 项目中使用 Gradle 以及并行构建所带来的提升,并且还可以使用 Gradle 在一些第三方项目上的构建缓存,这些优势都促使 Gradle 带到构建 Spring Boot 项目中来。
    迁移有什么好处?
    栈长使用 Maven,哪怕只改一个代码也是构建全部,构建项目确实要花不少时间。
    Spring Boot 官方也给出了数据,一次完整的 Maven 项目构建一般需要一个小时或者以上,而在过去的 4 周时间内,使用 Gradle 构建的平均时间只用了 9 分 22 秒!!!
    如下面截图所示:
    光从构建时间来看,效率真是倍数级的。 https://github.com/spring-projects/spring-boot/tree/v2.3.0.RELEASE
    栈长特意去看了下,在 Spring Boot 2.2.8 中使用的是 Maven:
    而最新发布的 Spring Boot 2.3.1 已经是切换到 Gradle 了:
    会带来什么影响?
    也许会有小伙伴质疑,Spring Boot 迁移到了 Gradle,会不会对公司现有的 Maven 项目或者后续的版本升级造成影响?
    如果你只是使用 Spring Boot 框架来搭建系统,那还是可以继续使用 Maven 来管理依赖的,Spring Boot 会继续在 Maven 中央仓库提交。
    如下面所示: org.springframework.boot spring-boot 2.3.1.RELEASE
    因为当版本确定之后,这个 Maven 构建只是一次性的,不会影响 Spring Boot 团队的日常迭代效率。
    但是,如果我们需要在本地构建 Spring Boot 源码,或者你正在学习最新 Spring Boot 源码,就需要掌握 Gradle 构建了。
    题外话,Gradle 肯定是未来的趋势,但也不一定非得迁移至 Gradle,只有适合自己的才是最好的,毕竟现在 Maven 和 Gradle 都是主流,但是 Maven 更占有市场,很多主流开源项目都是以 Maven 依赖来作为示例演示的。
    栈长也会陆续关注 Spring Boot 动态,后续也会给大家带来各方面的教程,获取历史教程可以在Java技术栈公众号后台回复:boot,掌握 Spring Boot 问题不大。
    学习、从不止步。
    推荐去我的博客阅读更多:
    1. Java JVM、集合、多线程、新特性系列教程
    2. Spring MVC、Spring Boot、Spring Cloud 系列教程
    3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程
    4. Java、后端、架构、阿里巴巴等大厂最新面试题
    觉得不错,别忘了点赞+转发哦!
    软件开发
    2020-06-17 10:19:00
    【Part1——理论篇】
    试想一个问题,如果我们要抓取某个微博大V微博的评论数据,应该怎么实现呢?最简单的做法就是找到微博评论数据接口,然后通过改变参数来获取最新数据并保存。首先从微博api寻找抓取评论的接口,如下图所示。
    但是很不幸,该接口频率受限,抓不了几次就被禁了,还没有开始起飞,就凉凉了。
    接下来小编又选择微博的移动端网站,先登录,然后找到我们想要抓取评论的微博,打开浏览器自带流量分析工具,一直下拉评论,找到评论数据接口,如下图所示。
    之后点击“参数”选项卡,可以看到参数为下图所示的内容:
    可以看到总共有4个参数,其中第1、2个参数为该条微博的id,就像人的身份证号一样,这个相当于该条微博的“身份证号”,max_id是变换页码的参数,每次都要变化,下次的max_id参数值在本次请求的返回数据中。
    【Part2——实战篇】
    有了上文的基础之后,下面我们开始撸代码,使用Python进行实现。
    1、首先区分url,第一次不需要max_id,第二次需要用第一次返回的max_id。
    2、请求的时候需要带上cookie数据,微博cookie的有效期比较长,足够抓一条微博的评论数据了,cookie数据可以从浏览器分析工具中找到。
    3、然后将返回数据转换成json格式,取出评论内容、评论者昵称和评论时间等数据,输出结果如下图所示。
    4、为了保存评论内容,我们要将评论中的表情去掉,使用正则表达式进行处理,如下图所示。
    5、之后接着把内容保存到txt文件中,使用简单的open函数进行实现,如下图所示。
    6、重点来了,通过此接口最多只能返回16页的数据(每页20条),网上也有说返回50页的,但是接口不同、返回的数据条数也不同,所以我加了个for循环,一步到位,遍历还是很给力的,如下图所示。
    7、这里把函数命名为job。为了能够一直取出最新的数据,我们可以用schedule给程序加个定时功能,每隔10分钟或者半个小时抓1次,如下图所示。
    8、对获取到的数据,做去重处理,如下图所示。如果评论已经在里边的话,就直接pass掉,如果没有的话,继续追加即可。
    这项工作到此就基本完成了。
    【Part3——总结篇】
    这种方法虽然抓不全数据,但在这种微博的限制条件下,也是一种比较有效的方法。
    最后如果您需要本文代码的话,请在后台回复“ 微博 ”二字,觉得不错,记得给个star噢~
    看完本文有收获?请转发分享给更多的人
    IT共享之家
    入群请在微信后台回复【入群】
    想学习更多Python网络爬虫与数据挖掘知识,可前往专业网站: http://pdcfighting.com/
    软件开发
    2020-06-17 10:09:00
    简单版 创建一张测试表 创建对应的JavaBean 创建mybatis配置文件,sql映射文件 测试
    MyBatis操作数据库 创建MyBatis全局配置文件 MyBatis的全局配置文件包含了影响MyBatis行为甚深的设置(settings)和属性(properties)信息、如数据库连接池信息等。指导着MyBatis进行工作。我们可以参照官方文件的配置示例。 创建SQL映射文件 映射文件的作用就是定义Dao接口的实现类如何工作。这是我们使用MyBatis时编写的最多的文件。 根据全局配置文件,利用 SqlSessionFactoryBuilder 创建 SqlSessionFactory 。 String resource="mybatis-config.xml"; InputStream is = Resources.getResourceAsStream(resource); SqlSessionFactory sf = new SqlSessionFactoryBuilder().build(is); 使用 SqlSessionFactory 获取 SqlSession 对象。一个 SqlSession 对象代表和数据库的一次会话。 SqlSession sqlSession = sf.openSession();
    使用SqlSession:根据Id查询 SqlSession sqlSession = sf.openSession(); try { Employee employee = sqlSession.selectOne("helloworld.EmployeeMapper.getEmployeeById", 1); System.out.println(employee); } finally { sqlSession.close(); }
    完整代码 加入相关的maven依赖 mysql mysql-connector-java 5.1.19 runtime org.mybatis mybatis 3.2.7 编写数据库脚本 create table t_employee( id int auto_increment, last_name varchar(255), email varchar(255), gender varchar(255), primary key(id) ); insert into t_employee (last_name,email,gender)values('tom','tom@qq.com','male'); select * from t_employee; 编写实体类 public class Employee { private Integer id; private String lastName; private String email; private String gender; // getter and setter // toString } 编写EmployeeMapper.xml 编写MyBatis的全局配置文件 mybatis-config.xml 编写主程序 public class Test { public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream is = Resources.getResourceAsStream(resource); SqlSessionFactory sf = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sf.openSession(); try { Employee employee = sqlSession.selectOne("helloworld.EmployeeMapper.getEmployeeById", 1); System.out.println(employee); } finally { sqlSession.close(); } } }
    接口式编程 创建一个Dao接口 public interface EmployeeMapper { Employee getEmployeeById(Integer id); } 测试 SqlSession sqlSession = sf.openSession(); try { // 使用SqlSession获取映射器进行操作 EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class); Employee employee = mapper.getEmployeeById(1); System.out.println(employee); } finally { sqlSession.close(); }
    SqlSession SqlSession的实例 是非线程安全的 ,因此是 不能被共享的 。 SqlSession每次使用完成后需要正确关闭,这个 关闭操作是必须的 。 SqlSession可以直接调用方法的id进行数据库操作。 但推荐使用SqlSession获取到Dao接口的代理类。执行代理对象的方法,可以更安全的进行类型检查操作。
    软件开发
    2020-06-17 10:07:00
    什么是条件语句
    JavaScript 语言中,条件语句(if 语句)常用于基于不同条件执行不同的动作。简单来讲就是判断给出的某个条件是否是正确的,如果条件正确要如何做,条件错误要如何做。举一个例子,例如现在有一个变量 age,给定一个条件语句为 “age是否大于18”,如果大于18 则可以玩游戏,否则不可以玩游戏。 var age = 20; if(age > 18){ console.log("你可以玩游戏哟"); }else{ console.log("未成年不可以玩游戏"); }
    在 JavaScript 中,我们可以使用的 if 条件语句有如下几种: if 语句:当指定条件为 true 时,使用该语句来执行代码。 if-else 语句:当指定条件为 true 时执行 if 后面的代码,为false执行 else 后面的代码。 else if 语句:当要指定多个条件时,可以在 if 语句后面加 else if 语句。
    if 语句
    if 语句是最基本的条件语句,规定假如条件为 true 时,则执行花括号 {} 中的代码块。
    语法如下所示: if (condition) { // 条件为 true 时要执行的代码块 }
    其中 condition 表示条件,并且 if 只能小写,后面必须接英文的花括号 {} ,如果不按照语言要求写代码会报错。
    示例:
    例如我们给定一个条件,当变量 num 大于10,输出“嘻嘻嘻”,我们可以这样写: var num = 15; if(num > 10){ console.log("嘻嘻嘻"); }
    在 VSCode 中执行上述代码,输出结果如下:
    执行代码时,我们直接在 .js 文件中编写好代码,在 VSCode 的终端中使用 node test.js 命令来执行这段代码,其中 test.js 是文件名。
    如果我们是在 HTML 中编写 JavaScript 代码,则需要将 JavaScript 代码写在
    直接在浏览器中打开这个文件,页面会显示如下内容:
    if-else语句
    if-else 语句规定假如条件为 true 时,执行 if 后面的花括号中的代码块,为 false 时则执行 else 后面花括号中的代码块。
    语法如下所示: if (condition) { // 条件为真时要执行的代码块 }else{ // 如果条件为false,则执行的代码块 }
    其中 condition 是条件语句, else 后面不需要接条件语法。这也好理解,例如打个比方说,我今年大于18岁,那么大于18岁就是一个条件 if 语句,只要不满足这个条件,不管是等于还是小于18岁,都是 else 。
    示例:
    同样是一个关于时间的例子,根据当前时间是否符合标准来打招呼,12点之前是”Good morning“,12点之后是”Good afternoon“: var hour = new Date().getHours(); // 获取当前时间(小时) var greet; if(hour < 12) { greet = "Good morning"; } else { greet = "Good afternoon"; } console.log("现在时间为:" + hour); console.log("打个招呼吧:" + greet);
    输出:
    else if 语句
    else if 语句和 if 语句类似,后面也要接一个条件,例如 if 后面接的是条件1, else if 后面就接条件2, else 后面就是既不满足条件1,又不满足条件2 的其他情况。
    语法如下: if (condition1) { // condition1 为 true 时,要执行的代码块 } else if (condition2) { // 当 condition1 为 false 而 condition2 为 true,则执行此代码块 } else { // 当 condition1 和 condition2 为 false,则执行这个代码块 }
    示例:
    例如学生食堂早上8点吃早饭,中午12点吃午饭,晚上18点吃晚饭,其他时间不吃饭: var hour = new Date().getHours(); // 获取当前时间(小时) if(hour == 8) { console.log("现在的时间为:" + hour + "点, 该吃早饭啦!"); } else if( hour == 12) { console.log("现在的时间为:" + hour + "点, 该吃午饭啦!"); } else if( hour == 18){ console.log("现在的时间为:" + hour + "点, 该吃晚饭啦!"); }else{ console.log("现在的时间为:" + hour + "点, 不是吃饭时间!"); }
    执行代码,输出: 现在的时间为:14点, 不是吃饭时间!
    动手小练习 一年级三班昨天考试,请根据他们的成绩,来给他们打等级,成绩为100的等级为S、成绩小于100大于80等级为A,成绩小于80大于60等级为B,成绩小于60等级为B。 现有一个变量height,当变量值大于100,输出“小姐姐,要注意控制饮食哟”,小于100大于90,输出”小姐姐,标准身材呢“,小于90,输出”小姐姐,太瘦了要多吃点!“。
    链接: https://www.9xkd.com/
    软件开发
    2020-06-17 09:59:00
    GitHub 15.2k Star 的Java工程师成神之路,不来了解一下吗!
    GitHub 15.2k Star 的Java工程师成神之路,不来了解一下吗!
    Perl 之父 Larry Wall 曾经在自己的《Programming Perl》一书中提到过:"程序员有3种美德: 懒惰、急躁和傲慢" 。懒惰,作为程序员美德的第一个要素。
    Larry Wall 所说程序员应该具备的懒惰,并不是安于现状、不思进取。而是一种为了达到同样甚至更好的目标,而付出最少的时间或者精力的行为。一个懒惰的程序员会尽量使自己的代码即实用又有很好的可读性,这样可以节省很多后面的维护的成本。一个懒惰的程序员会尽力完善代码中的注释及文档,以免别人问自己太过问题。一个懒惰的程序员会擅长使用各种工具,从方方面面提升自己的效率。
    懒惰是科技发展、人类进步的最大动力。从原始社会、农业时代、工业时代一直到如今的信息时代。因为懒惰,人们才会有动力去发明各种高效、便捷的工具,这些当初的工具,渐渐的就形成了如今的科技。所谓工欲善其事、必先利其器,说的就是这个道理。
    在一篇文章中,作者将介绍多种实用的工具,全方位的武装你,使我们的读者都可以当一个“懒惰”的程序员。
    搜索类在线工具
    1、SearchCode( https://searchcode.com/ )是一个源码搜索引擎,目前支持从 Github、Bitbucket、Google Code、CodePlex、SourceForge 和 Fedora Project 平台搜索公开的源码。
    2、mvnrepository( http://mvnrepository.com )这个不用详细解释了,就是查询maven的gav等信息。
    3、Iconfont( https://www.iconfont.cn )国内功能很强大且图标内容很丰富的矢量图标库,提供矢量图标下载、在线存储、格式转换等功能。阿里巴巴体验团队倾力打造,设计和前端开发的便捷工具。
    4、BinaryDoc for OpenJDK( https://openjdk.binarydoc.org/net.java/openjdk/)直接从OpenJDK二进制文件生成文档,二进制代码是最好的文档。
    5、Unsplash( https://unsplash.com )是一个免费的图片分享网站,可以在上面搜索无版权图片
    6、鸠摩搜书( https://www.jiumodiary.com/ )国内一款强大的电子书搜索引擎,整合了大部分电子书平台的资源,最重要的是他无需注册登录,可以直接下载。并且网站页面清新、且资源免费。
    7、MySlide( https://myslide.cn/ )是一个提供PPT分享服务的平台,在这里你可以找到你想要的PPT。专注技术领域的PPT共享,各种技术大会的演讲PPT这里都有。
    8、IT大咖说( https://www.itdks.com/ )是IT垂直领域的大咖知识分享平台,分享行业TOP大咖干货,技术大会在线直播录播,在线直播知识分享平台。
    生成类在线工具
    1、BeJSON( http://www.bejson.com/json2javapojo )是一个比较好用将Json转成Java对象的工具。json是目前JavaWeb中数据传输的主要格式,很多时候会有把json转成Java对象的需求。有时候合作方会提供一个json的样例,需要我们自己定义Java类,这时候这个工具就派上用场了。
    2、在线corn生成工具( https://cron.qqe2.com/ ),Cron 一般用于配置定时任务的执行。但是要想一次性的把一个corn表达式配置好确实很难的,需要程序员记住他的语法。有一些在线工具可以提供图形化的界面,只要输入想要定时执行的周期等,就可以自动生成corn表达式。
    3、正则表达式的生成工具( http://tool.chinaz.com/tools/regexgenerate )正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。在使用正则表达式进行字符转过滤的时候,需要用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。通常,这个规则字符串的定义是比较麻烦和复杂的。也需要经过大量的测试和验证才能被采用。
    4、 ASCII艺术生成工具( http://patorjk.com/software/taag/ )可以将输入的字符快速转换成ASCII艺术文字的形式。
    5、ProcessOn( https://www.processon.com/ )是一个在线协作绘图平台,为用户提供最强大、易用的作图工具!支持在线创作流程图、BPMN、UML图、UI界面原型设计、iOS界面原型设计。
    6、MarkDown编辑器,Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档,深受广大程序员们的喜爱,推荐几款在线md编辑器:MaHua( https://mahua.jser.me/ ) 马克飞象( https://maxiang.io/ ) Cmd( https://www.zybuluo.com/mdeditor )
    转换类在线工具
    1、站长工具的编码转换( http://tool.chinaz.com/tools/unicode.aspx )比较全面,提供了Unicode编码、UFT8编码、URL编码/解码等功能。编码问题一直困扰着开发人员,尤其在Java 中更加明显,因为Java 是跨平台语言,不同平台之间编码之间的切换较多。计算中提拱了多种编码方式,常见的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。有些时候开发人员需要通过编码转换的方式来查看不同编码下面的文件内容。
    2、时间戳转换工具( http://tool.chinaz.com/Tools/unixtime.aspx),时间戳(英语:Timestamp)是指在一连串的资料中加入辨识文字,如时间或日期,用以保障本地端(local)资料更新顺序与远端(remote)一致。
    3、Timebie( http://www.timebie.com/cn/easternbeijing.php )提供了世界时间相互转换的功能。世界各地时间转换在做国际业务的时候会经常用到,比如北京时间转纽约时间,北京时间转洛杉矶时间。
    4、加密解密也是JavaWeb可能会经常遇到的,有的时候我们需要验证加密算法是否正确,或者要解密等场景,就需要一个在线工具( http://tool.chinaz.com/tools/textencrypt.aspx )来快速验证。
    5、convertworld( https://www.convertworld.com/zh-hans/ )是一个比较全的单位换算的网站。我经常用它进行时间单位和货币单位的换算。
    6、Convertio( https://convertio.co/zh/flv-mp4/ )是一个在线视频格式转换工具,支持多种常见视频格式,如 FLV、MOV 和 AVI 等。上传的视频文件不能超过 100 MB。
    7、Docsmall( https://docsmall.com/image-compress )是一个在线图片压缩工具,可以批量压缩图片、Gif 图,一次最多上传 30 张图片,每张图片最大为 25 MB。
    检查类在线工具
    1、JSON格式化工具( https://www.json.cn/ )是我尝试过很多同类工具之后最经常使用的一个,不仅支持json格式的验证及格式化,还可以将json格式压缩成普通文本等好用功能。有时候我们不确定这个文本是否完全符合JSON格式,有时候我们也想可以更清晰的查看这个JSON文本的格式关系。就可以使用这个工具来进行JSON格式的验证和格式化。
    2、正则验证( http://tool.chinaz.com/regex ),Java开发对正则表达式肯定不陌生。站长工具提供的这个正则验真工具还不错。
    3、Diffchecker( https://www.diffchecker.com/ )是一个使用很不错代码差异对比工具。使过svn或者git的人对diffcheck肯定不陌生,但有时候我们修改的文本内容并没有被版本控制,那么就可以使用在线的网站查看文件的修改情况。
    对照类工具
    1、ASCII对照表 : http://tool.oschina.net/commons?type=4 2、HTTP状态码 : http://tool.oschina.net/commons?type=5 3、HTTP Content-type : http://tool.oschina.net/commons 4、TCP/UDP常见端口参考 : http://tool.oschina.net/commons?type=7 5、HTML转义字符 : http://tool.oschina.net/commons?type=2 6、RGB颜色参考 : http://tool.oschina.net/commons?type=3 7、网页字体参考 : http://tool.oschina.net/commons?type=8
    在线代码运行
    1、CodeRunner( https://tool.lu/coderunner/ )可以在线运行php、c、c++、go、python、java、groovy等代码。当我们在外面,没有IDE又想执行个小程序的时候是个不错的选择。
    一个实用小插件
    最后,再给大家推荐一个chrome插件,这个插件中囊括了很多上面介绍的在线工具的功能,如JSON格式化、时间戳转换、Markdown工具、编码解码、加密解密、正则验证等。
    FeHelper ,可以关注我的公众号,后台回复"在线工具",我已经把安装包给大家准备好了。
    软件开发
    2020-06-17 09:59:00
    近日,美国黑人乔治‧佛洛伊德(George Floyd)被警员制服期间死亡而触发的反种族主义已经持续了10多天,这场有关种族的示威浪潮蔓延至欧洲英国、法国、德国、西班牙和澳大利亚等国家。
    关于这个事件,最近也有很多互联网公司纷纷加入。几天内,微软、苹果以及谷歌等多家美国本土的互联网公司的CEO均通过不同形式表达了对于种族平等的支持。
    而在技术圈,最近也发生了一些支持这一运动的行动。
    谷歌摆脱"黑名单"
    近日,谷歌的Chrome浏览器的源码提交记录中,出现了几条关于种族歧视相关的提交。其中主要提交内容是废弃了"blacklist"的写法:
    下图是部分文件重名的提交内容:
    因为有开发者认为 “黑名单”和 “白名单”之类的术语强化了"黑 == 坏,白 == 好" 等意思,具有一定的种族歧视色彩。
    在提交记录中,Chrome的开发者将blacklist修改为blocklist。其实,这一修改早在去年10月份,就已经有了相关规范,
    自去年10月以来,Chrome 已在其官方代码样式指南中包含有关如何编写“种族中立”代码的指南。该文件明确指出,Chrome和Chromium开发人员应避免使用“黑名单”和“白名单”一词,而应使用中性术语“阻止名单”(blocklist)和“允许名单”(allowlist)。
    技术也要"政治正确"
    除了Chrome废弃了"黑名单"的表述以外,其实还有很多类似的事件。
    如开发者熟知的"Master/Slave",是分布式系统中一个比较常用的计算结构,这个名词由两个单词组合而成:Master和Slave。
    Master:主人、雇主。 Slave:奴,奴隶。
    很多开发者认为master-slave这一表述中的slave(奴隶)对于人权具有一定的侵犯性,所以有很多呼声要求修改这一词汇。
    早在2014 年,Drupal 项目就用 primary 和 replica 替换了 master 和 slave;Django 项目则用 leader 和 follower 替换之;CouchDB 项目也做了类似语言上的净化。
    在2018年,两个被我们熟知的软件,Redis和Python也为了"政治正确"而做出了相应妥协。
    2018年9月7日,Redis 5.0 RC5 发布了,该版本中仍然使用master-slave来表示主从模式,这引起了很多开发者的抗议。之后Redis的作者在推特上发起了一个投票,结果显示,超过半数的人希望修改这一描述。
    最后Redis的作者决定将 master-slave 描述改为 master-replica。
    同样是2018年9月7日,在 Red Hat 工作的 Python 开发者 Victor Stinner 公开提交了 4 个 PR,希望能将 Python 文档和代码中出现的 “master” 和 “slave” 修改为像 “parent” 和 “worker” 这样的术语,以及对其他类似的术语也进行修改。
    对于这个问题,Python的创始人,已经宣布退出Python核心开发组决策层的Guido van Rossum被请回参与了这一事件的讨论及仲裁。最终他做出了重要的决定:
    计划在Python 3.8中,将slave改为worker、helper、另外将master process改为parent process。
    近日,Golang也有开发者提了类似的commit,要求修改whitelist/blacklist、master/slave等表述:
    对于类似的修改,有一些是相对简单的,只要修改命名就行了。就怕有些软件修改之后产生各种兼容性问题。
    所以,很多软件都是持谨慎态度的,但是随着很多呼声越来越高,相信很多厂商也不得不最终选择"政治正确"。
    对了,美国还有个地方叫"白宫"...
    软件开发
    2020-06-17 09:58:00

    所有的框架处理业务请求时,都会处理URL的路径部分,分配到指定的代码中去处理。
    实现这一功能的关键就是获取$_SERVER全局变量中对于URL部分的数据
    当请求的路径为
    http:// test.com/article? id=1
    http:// test.com/article/update? id=1
    支持以上url模式,不需要配置传递PATH_INFO变量,也不需要配置伪静态去除index.php
    最简单的nginx配置如下:
    server { listen 80; server_name test.com; access_log /var/log/nginx/test.com.access.log main; root /home/test; index index.html index.htm index.php; location / { try_files $uri $uri/ /index.php?q=$uri &$args; } location ~ \.php { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
    此配置有几个重点要关注:
    1.try_files必须配置在location块中,这个可以用于除去index.php,如果不配置,则必须在路径中加上/index.php/
    2.location ~ \.php
    a. 这里是否以$结尾,有时会被困扰,重点看清是否存在try_files,如果不存在try_files指令,那么就一定不要以$结尾,这样在路径中使用带/index.php/的模式还是可以访问的
    b. 如果存在try_files指令,并且location ~ \.php$ 这里是以$结尾,那么/index.php/在php的location中就匹配不到,但是try_files又把参数重写到index.php?q=中了,因此这样也是可以访问到
    此时$_SERVER变量中,经常被各大框架或者自写程序用作路由处理使用的变量值如下:
    $_SERVER["PHP_SELF"]=>"/index.php",没有URL中的参数
    $_SERVER["PATH_INFO"]=>,根本不存在,因为Nginx没有传递这个变量
    $_SERVER["REQUEST_URI"]=>"/article/update?id=1",这个是实现路由的关键,参数都存在
    PHP中比较兼容的处理是: $uri=$_SERVER['REQUEST_URI']; $uri=str_replace("/index.php","",$uri); if(strpos($uri,"?")!==false){ $uri=substr($uri,0,strpos($uri,'?')); } $uri=trim($uri,'/'); var_dump($uri);//获取到 article/update

    软件开发
    2020-06-17 09:55:00
    一般常规办法:先使用POI或者HSSFWorkbook等第三方类库对其表格数据结构化,再用SQL语句写入数据库。由于Java并没有表格对象,总要利用集合加实体类去实现(硬编码),如果碰到格式复杂的表格,解析难度大,工作量会成倍增加,代码不仅冗长、且很难通用。
    比如要处理这么个场景:数据库表Logistics有3个字段:Shippers、Region、Quantity。解析如下Excel表格,并入库:



    入库后的效果:



    Java代码大概要写成这样子:
    ...
    File target = new File(filepath, filename);
    FileInputStream fi = new FileInputStream(target);
    HSSFWorkbook wb = new HSSFWorkbook(fi);
    HSSFSheet sheet = wb.getSheetAt(sheetnum);
    int rowNum = sheet.getLastRowNum() + 1;
    for (int i = startrow; i < rowNum; i++) {
    PageData varpd = new PageData();
    HSSFRow row = sheet.getRow(i);
    int cellNum = row.getLastCellNum();
    ...
    }
    ...
    List listPd = (List)ObjectExcelRead.readExcel(filePath, fileName, 3, 0, 0);
    for(int i=0;i pd.put("ET_ID", this.get32UUID());
    ...
    }
    /*The operation to import the database*/
    mv.addObject("msg","success");
    ...
    如果有了集算器,这样的问题则会简单很多,它是专业处理结构化数据的语言,能够轻松读取Excel数据,结构化成“序表”后导入数据库。以往需要编写数千行代码才能完成的Excel数据结构化入库工作,现在只需简单的几行就搞定了。比如上面的问题,集算器SPL仅3行:
    A
    1 =file("/workspace/crosstab.xls").xlsimport@t(;1,2).rename(#1:Shippers)
    2
    3
    =A1.pivot@r(Shippers;Region,Quantity)
    =Mysql.update(A2,Logistics)
    其实还有很多类似的结构化问题不太方便,但有集算器SPL的辅助却很简单,感兴趣可以参考: 复杂Excel表格导入导出的最简方法 、 10行代码提取复杂Excel数据
    集算器还很容易嵌入到Java应用程序中, Java如何调用SPL脚本 有使用和获得它的方法。
    关于集算器安装使用、获得免费授权和相关技术资料,可以参见 如何使用集算器 。
    软件开发
    2020-06-17 09:38:00
    BIRT本身不直接支持动态数据源,常见解决办法是在数据源的beforeopen事件中添加类似如下代码:
    ...
    importPackage( Packages.java.io );
    importPackage( Packages.java.util );
    fin = new java.io.FileInputStream(new String("d:/config.txt"));
    props = new java.util.Properties( );
    props.load(fin);
    extensionProperties.odaDriverClass = new String(props.getProperty("driver"));
    extensionProperties.odaURL = new String(props.getProperty("url"));
    extensionProperties.odaUser = new String(props.getProperty("username"));
    extensionProperties.odaPassword = new String(props.getProperty("password"));
    fin.close()
    ...
    这种通过硬编码的方式解决(要么需要大量修改报表文件,要么需要每个报表继承一个公共的库文件),过程还是比较复杂,在报表数量较多,开发人数较多时,需要注意的地方有些多,并不完美。
    如果有了集算器,这样的问题就简单多了,其独特的宏替换机制极大地提高代码复用程度,根据不同参数值,得到不同结果。
    比如要处理这么个场景: 数据源myDB和oraDB分别指向不同的数据库,两库中有相同结构的表ORDER,报表需要根据参数动态连接数据源,查询并展现ORDER中金额大于1000的订单。示意图如下:



    集算器SPL仅1行就搞定了:
    1
    A =${pSource}.query("select * from ORDER where Amount>1000")
    其中 pSource 为是报表参数,代表数据源名,${…} 表示将字符串解析为表达式。其实还有很多情况BIRT解决动态计算问题不太方便,但有集算器SPL的辅助却很简单,感兴趣可以参考: 解决 BIRT 动态数据源的若干示例
    集算器提供了 JDBC 驱动,可以很方便的与BIRT等报表工具集成, BIRT调用SPL脚本 有使用和获得它的方法。
    关于集算器安装使用、获得免费授权和相关技术资料,可以参见 如何使用集算器 。
    软件开发
    2020-06-17 09:33:00
    控制报表数据访问权限,是让不同的人访问同一张报表的时候所看到的数据是不同的或者说只能看到权限范围内的数据。
    报表工具通用的做法是控制数据集(报表所呈现的数据基本都是来自数据集)。以 sql 数据集为例,只要 sql 的条件不一样,返回的数据也就不同了。比如 sql 写成:
    Select … from T where ${w}
    当 w 定义为 if(role==’admin’,”1=1”,” status=1”) 时,角色为“管理员”可以看所有数据,否则只能看到 status 字段值为 1 的。
    这种做法是报表工具动态宏的功能,可能让 sql 动态拼接。具体例子参考: 润乾报表权限管理机制
    同理,如果是其他数据集类型,按照相应方法,根据不同人控制到仅返回权限内数据即可实现。
    软件开发
    2020-06-17 09:31:00
    翻车现场:
    在IDEA进行MongoDB进行自定义查询操作时,出现的Bug: Caused by: org.springframework.data.mapping.PropertyReferenceException: No property name found for type Teacher! Did you mean 'age'?
    报错:

    错误代码:
    测试类:
    TeacherRepository:



    原因:
    上面的代码,好像一眼看去,没什么问题, 但是在不知道的情况下,已经出问题了,
    其中: 接口的命名: findByNameAndAge 中的name和age 与实体类中的不一致,导致的问题
    在Spirng Data MongoDB中, 如果进行自定义方法查询,是有规则限制的, 在 MongoRepository下的接口命名是不能随意命名的, 其中的变量需要与实体类中的变量名一致


    解决:
    只要把原本 findByNameAndAge 改成 findByUsernameAndAge 即可

    注意: 除了字段名保持一致, 其他的也要根据 Spring Data mongodb提供自定义方法的规则: 如下:
    按照findByXXX,findByXXXAndYYY、countByXXXAndYYY等规则定义方法,实现查询操作。



    看完恭喜你,又知道了一点点!!!
    你知道的越多,不知道的越多!
    ~感谢志同道合的你阅读, 你的支持是我学习的最大动力 ! 加油 ,陌生人一起努力,共勉!!
    软件开发
    2020-06-16 20:16:01
    在Spring Boot配置文件中,配置文件的属性值还可以进行参数间的引用,也就是在后一个配置的属性值中直接引用先前已经定义过的属性,这样可以直接解析其中的属性值了。 使用参数间引用的好处就是,在多个具有相互关联的配置属性中,只需要对其中一处属性预先配置,其他地方都可以引用,省去了后续多处修改的麻烦 参数间引用的语法格式为${xx},xx表示先前在配置文件中已经配置过的属性名,示例代码如下 ```properties app.name=MyApp app.description=${app.name} is a Spring Boot application ``` 上述参数间引用设置示例中,先设置了“app.name=MyApp”,将app.name属性的属性值设置为了MyApp;接着,在app.description属性配置中,使用${app.name}对前一个属性值进行了引用 接下来,通过一个案例来演示使用随机值设置以及参数间引用的方式进行属性设置的具体使用和效果,具体步骤如下 (1)打开Spring Boot项目resources目录下的application.properties配置文件,在该配置文件中分别通过随机值设置和参数间引用来配置两个测试属性,示例代码如下 ```properties # 随机值设置以及参数间引用配置 tom.age=${random.int[10,20]} tom.description=tom的年龄可能是${tom.age} ``` 在上述application.properties配置文件中,先使用随机值设置了tom.age属性的属性值,该属性值设置在了[10,20]之间,随后使用参数间引用配置了tom.description属性 (2)打开\项目的测试类,在该测试类中新增字符串类型的description属性,并将配置文件中的tom.description属性进行注入,然后新增一个测试方法进行输出测试,示例代码如下 ```java @Value("${tom.description}") private String description;@Test public void placeholderTest() { System.out.println(description); } ``` 上述代码中,通过@Value("${tom.description}")注解将配置文件中的tom.description属性值注入到了对应的description属性中,在测试方法placeholderTest()中对该属性值进行了输出打印。 执行测试方法placeholderTest() ,查看控制台输出效果可以看出,测试方法placeholderTest()运行成功,并打印出了属性description的注入内容,该内容与配置文件中配置的属性值保持一致。接着,重复执行测试方法placeholderTest(),查看控制台输出语句中显示的年龄就会在[10,20]之间随机显示 这些内容,是从拉勾教育的《Java工程师高薪训练营》里学到的,课程内容非常全面,还有拉勾的内推大厂服务,推荐你也看看。
    软件开发
    2020-06-16 19:38:00
    在Spring Boot配置文件中设置属性时,除了可以像前面示例中显示的配置属性值外,还可以使用随机值和参数间引用对属性值进行设置。下面,针对配置文件中这两种属性值的设置方式进行讲解 #### 1.8.1随机值设置 在Spring Boot配置文件中,随机值设置使用到了Spring Boot内嵌的RandomValuePropertySource类,对一些隐秘属性值或者测试用例属性值进行随机值注入 随机值设置的语法格式为${random.xx},xx表示需要指定生成的随机数类型和范围,它可以生成随机的整数、uuid或字符串,示例代码如下 ```properties my.secret=${random.value} // 配置随机值 my.number=${random.int} // 配置随机整数 my.bignumber=${random.long} // 配置随机long类型数 my.uuid=${random.uuid} // 配置随机uuid类型数 my.number.less.than.ten=${random.int(10)} // 配置小于10的随机整数 my.number.in.range=${random.int[1024,65536]} // 配置范围在[1024,65536]之间的随机整数 ``` 上述代码中,使用RandomValuePropertySource类中random提供的随机数类型,分别展示了不同类型随机值的设置示例 这些内容,是从拉勾教育的《Java工程师高薪训练营》里学到的,课程内容非常全面,还有拉勾的内推大厂服务,推荐你也看看。
    软件开发
    2020-06-16 19:32:00
    MyBatis介绍 MyBatis是支持 定制化SQL 、存储过程以及高级映射的优秀的 持久层框架 。 MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。 MyBatis可以使用简单的 XML 或 注解 用于配置和原始映射,将 接口和Java的POJO 映射成数据库中的记录。 推荐使用XML而不是注解。
    MyBatis历史 原是Apache的一个开源项目iBatis。 2010年6月这个项目由Apache Software Foundation迁移到了Google Code,随着开发团队转投Google Code 旗下,iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github。 iBatis一词来源于“internet”和“abatis”的组合,是一个 基于Java的持久层框架 。iBatis提供的持久层框架包括SQL Maps和DAO。
    为何要用MyBatis MyBatis是一个 半自动化 的持久化层框架。 JDBC SQL夹在Java代码块里,耦合度高导致硬编码内伤。 维护不易且实际开发需求中SQL是有变化,频繁修改的情况很常见。 Hibernate和JPA 长难复杂SQL,对于Hibernate而言处理也不容易。 内部自动生产的SQL,不容易做特殊优化。 基于全映射的全自动框架,大量字段的POJO进行部分映射时比较困难。 导致数据库性能下降。 对开发人员而言, 核心SQL还是需要自己优化 。 SQL和Java编码分开,功能边界清晰。一个专注业务、一个专注数据。
    下载MyBatis
    下载地址: https://github.com/mybatis/mybatis-3/

    软件开发
    2020-06-16 18:35:00
    恶意刷新
    恶意刷新就是不停的去刷新提交页面,导致出现大量无效数据,这类问题在实际应用中我们经常遇到,比如一个活动的分享得积分,刷票,刷红包等等,遇到这些问题,你是如何去防止的。
    当你在做一个刷红包的活动,或者一个分享得积分的活动时,频繁的被刷新会导致数据库吃紧,严重时会导致系统死机。遇到这方面你是如何防止恶意刷新页面的,说白了也就是恶意刷新你创建的链接。

    下面我们来看看防止恶意刷页面的原理:
    1 要求在页面间传递一个验证字符串;
    2 在生成页面的时候 随机产生一个字符串;
    3 做为一个必须参数在所有连接中传递。同时将这个字符串保存在session中;
    点连接或者表单进入页面后,判断session中的验证码是不是与用户提交的相同,如果相同,则处理,不相同则认为是重复刷新;
    4 在处理完成后将重新生成一个验证码,用于新页面的生成。
    我们可以从session方面防止用户恶意刷新。
    代码如下:

    方案一:
    $allowTime ){ $refresh = true ; $_SESSION [ $allowT ] = time (); } else { $refresh = false ; } ?>

    方案二:


    方案三:
    < form method = "post" name = "magic" action = "f5.php" > < input type = "hidden" name = "tag" value = " " > < input type = text name = "name" > < input type = "submit" value = "submit" >
    上面的代码是基于 session的验证,假设你在2秒内刷新了页面,那么他会执行exit() 函数输出一条消息,并退出当前脚本,于是就不会加载下面的内容,所以这段代码最好放在header中,先让代码执行,再加载其他的东西。
    如果把代码放在了footer里,结果整个页面都加载了只在最后一行输出了"请不要频繁刷新",放在header中,效果比较好,想看效果的话按两下F5 吧。当然最好的是采用的是新建一个php文件,然后在header调用。

    这样做的好处有两个:
    一个是修改功能代码方便,不用每次都打开header文件,也不怕误改了其他地方的代码,二是一旦出错,可以快速修改并检查,甚至可以直接删除文件。

    代码如下:

    你也可以结合cookie与session一起用,代码如下:利用文件存储数据

    这里读取数据
    alert(" 您不可以刷新本页 !! "); history.back();" ; } ?>
    其中counter.txt 文件为同目录下的记录登录数文件。
    $counter=fgets($fp,1024); 为读取文件中数值型值的方法(可包含小数点数值)

    软件开发
    2020-06-10 16:23:00
    原文: PyCoder's Weekly - Issue #423
    200527 Zoom.Quiet (大妈) 用时 42 分钟 完成快译 200527 Zoom.Quiet (大妈) 用时 37 分钟 完成格式转抄. 从终端向 Python 传递代码的多种方法 BRETT CANNON
    You might know about pointing Python to a file path, or using -m to execute a module. But did you know that Python can execute a directory? Or a .zip file?
    ( 是也乎:
    甚至于还有通过 FaaS 直接远程嗯哼的...
    ) Python 3.9 的 PEP 们 JAKE EDGE
    The first Python 3.9 beta release is upon us! Learn what to expect in the final October release by taking a tour of the Python Enhancement Proposals (PEPs) that were accepted for Python 3.9.
    ( 是也乎:
    老爹离开后, Py 没了缰绳儿, 越来越欢跳了...
    ) Python 依赖关系管理工具概述 MARIO KOSTELAC
    While pip is often considered the de facto Python package manager, the dependency management ecosystem has really grown over that last few years. Learn about the different tools available and how they fit into this ecosystem.
    ( 是也乎:
    可见 pyenv 小出一和了一个多精巧的空间, 夯实了地位
    ) 在 Raspberry Pi 上用 Python 构建物理项目 REAL PYTHON
    In this tutorial, you’ll learn to use Python on the Raspberry Pi. The Raspberry Pi is one of the leading physical computing boards on the market and a great way to get started using Python to interact with the physical world.
    ( 是也乎:
    ) CPython Internals Book (抢先体验折扣 ~ 6折) REAL PYTHON sponsor
    Unlock the inner workings of the Python language, compile the Python interpreter from source code, and participate in the development of CPython. The new “CPython Internals” book shows you exactly how. 下载示例章节并获得您的早鸟折扣 →
    ( 是也乎:
    真蟒版"CPython 源代码鉴赏", 距离陈儒当年针对 Python 2.4 写的已经有12年了...
    ) PSF 董事会提名期延长至6月3日 PYTHON.ORG Qt for Python 5.15.0 发布了! QT.IO
    ( 是也乎:
    Qt 是开源商业成功典范了..
    嗯哼? 能抢到 QT.IO 这个域名也不容易哪...
    )
    讨论 Discussions 你用 Python 的 pdb 来调试有多频繁? RAYMOND HETTINGER
    ( 是也乎:
    NEVER
    )
    文章,教程和嗯哼 Articles, Tutorials and Talks 关于 Python 测试入门的建议 REAL PYTHON podcast
    Have you wanted to get started with testing in Python? Maybe you feel a little nervous about diving in deeper than just confirming your code runs. What are the tools needed and what would be the next steps to level up your Python testing?
    ( 是也乎:
    TDD 难的从来不是工具, 而是转变思路,在写功能前, 就得设计好容易放屁的裤子出来...
    ) 立即停止使用 datetime.now !(有依赖注入) HAKI BENITA
    How do you test a function that relies on datetime.now() or date.today()? You could use libraries like FreezeGun or libfaketime, but not every project can afford the luxury of reaching for third-party solutions. Learn how dependency injection can help you write code that is more testable, maintainable, and practical.
    ( 是也乎:
    还是推荐用更加友好的第三方嗯哼了..
    ) 如何编写可安装的 Django 应用 REAL PYTHON
    In this step-by-step tutorial, you’ll learn how to create an installable Django app. You’ll cover everything you need to know, from extracting your app from a Django project to turning it into a package that’s available on PyPI and installable through pip.
    ( 是也乎:
    太南了, Django 当年设计时绝对没想到有这种应用场景的
    tox 立功了...
    ) Python 语言峰会的一些议题 JAKE EDGE
    The Python Language Summit is an annual gathering for the developers of various Python implementations. This year, the gathering was conducted via videoconference. Here are summaries of some of the sessions from this year’s summit. 用Python 的 zip() 函数进行并行迭代 REAL PYTHON video
    How to use the Python zip() function to solve common programming problems. You’ll learn how to traverse multiple iterables in parallel and create dictionaries with just a few lines of code.
    ( 是也乎:
    ) 构建一个 OpenCV 社交距离探测器 ADRIAN ROSEBROCK
    In this tutorial, you will learn how to implement a COVID-19 social distancing detector using OpenCV, Deep Learning, and Computer Vision.
    ( 是也乎:
    其实 OpenCV 只是一个视觉库, 真正功能还是要靠现成模块的...
    ) 如何为您的博客设置 RSS/Atom 聚合 FLORIAN DAHLITZ • Shared by Florian Dahlitz
    ( 是也乎:
    这不应该是 SSG 引擎内置的功能?
    ) SOLID 设计的 Python 指南 DEREK DRUMMOND • Shared by Derek Drummond
    好物 Interesting Projects, Tools and Libraries, Projects & Code httpx: 适用于 Python 的下一代HTTP客户端 GITHUB.COM/ENCODE
    ( 是也乎:
    叕叕叒又一个下一代 HTTP 客户端..
    ) python-skyfield: Python 优雅天文学 GITHUB.COM/SKYFIELDERS PySyft: 用于 加密/隐私保护机器学习 GITHUB.COM/OPENMINED pybridge-ios: 在本机 iOS 应用程序中复用 Python 代码 GITHUB.COM/JOAOVENTURA
    ( 是也乎:
    iOS 版 QPython
    ) returns: 使函数返回有意义/有类型且安全的东西... GITHUB.COM/DRY-PYTHON
    ( 是也乎:
    C++化 Python 叕一种努力. ) snakeware: 具有完整 Python 用户空间的免费 Linux 发行版 GITHUB.COM/JOSHIEMOORE
    ( 是也乎:
    这怎么说呢, 就是 MacroPython 的完备版本?
    ) python-testing-crawler: 用于Web应用程序自动功能测试的爬虫 GITHUB.COM/PYTHON-TESTING-CRAWLER
    ( 是也乎:
    可自检的爬虫...
    )
    📆🐍 活动/大会 Events, MeetUp 真的是全球线下活动组织中心 ⋅ Python Web Conf 2020 (Virtual) June 17–19, 2020 ⋅ FlaskCon 2020 (Virtual) July 4–5, 2020.
    The call for speakers is open until June 7th, 2020.
    ( 是也乎:
    中国也已经接到有关通知, 允许线下集会申报了...
    )
    DAMA ❤️ Happy Pythonic ;-( 大妈私人无责任播报 )
    101camp9py 即将报名(能开发票 ;-)
    课程规划: 开始报名 2020.5.31 报名截止 2020.6.21 正式开课 2020.6.28 课程结束 2020.8.09
    详情 => 蟒营™ Python 入门班第9期
    PS: 首发: Issue 423 ~蠎周刊 ~汇集全球蠎事儿 ;-) 修订: issue-423.md NN 4033
    好文笔,感叹号年度配额: 1/3
    投稿/反馈邮箱: askdama@googlegroups.com
    (邮件列表地址, 当成正常邮件发送邮件就好, 不用注册, 不用翻越...)
    ZoomQuiet/ 大妈
    就是四处 是也乎,( ̄▽ ̄) 的那个 大妈 : 私自嗯哼: ZoomQuiet (订阅号: ZoomQuiet42) 公开课程: 蟒营 (订阅号: Mainium) 历史吐糟: Chaos42 (订阅号 PythoniCamp) as 创始组织者: PyChina (订阅号: PyChinaOrg) 本地社区: GDG珠海 (订阅号: GDG-ZhuHai) TFUG珠海 (订阅号: ZH_TFUG)
    蟒营®编程思维提高班Python版 第9期 报名: 6.1~6.7第一周报名, 统惠500元 报名截止 2020.6.21 正式开课 2020.6.28 课程结束 2020.8.09
    蟒营®原创课程 服务: 伴你重享学习乐趣
    蟒营®: 编程思维提高班 Python版 自怼圈 蟒周刊
    Powered by: Zoom.Quiet / 昧因科技® 本文由博客一文多发平台 OpenWrite 发布;
    软件开发
    2020-06-10 16:07:00
    0. 前言
    在前一篇中我们讲到了Dapper的应用,但是给我们的感觉Dapper不像个ORM更像一个IDbConnection的扩展。是的,没错。在实际开发中我们经常用Dapper作为对EF Core的补充。当然了Dapper并不仅仅只有这些,就让我们通过这一篇文章去让Dapper更像一个ORM吧。
    1. Dapper Contrib
    Dapper Contrib 扩展了Dapper对于实体类的CRUD方法:
    安装方法:
    命令行: dotnet add package Dapper.Contrib
    NuGet: Install-Package Dapper.Contrib
    使用: using Dapper.Contrib.Extensions;
    这个是一个使得Dapper功能更强大的扩展包,因为支持了CRUD,所以需要对实体类添加配置,该扩展包使用Attribute作为依据进行相关映射配置: [Table("Model")] public class Model { [Key] [ExplicitKey] public int Id{get;set;} [Computed] public int Count {get;set;} [Write] public String Name{get;set;} }
    这是所有的配置,Table用来声明是一个表,必须指定表名,Key表示该属性是数据库主键,ExplicitKey表示这个属性是数据库中显示设置的主键,Computed表示该字段是一个计算字段,Write表示该字段可以设置值进去。需要注意的是: Key和ExplicitKey这两个不能同时标注在一个属性上。
    那么接下来,我们看看它扩展了哪些方法:
    插入单个对象: public static long Insert(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class;
    其中 transcation表示事务,如果指定事务,数据的提交将由事务控制,该方法会返回插入对象的主键(如果对象主键是数字类型)或者返回一个待插入列表中已插入的行数。
    获取单个对象: public static T Get(this IDbConnection connection, [Dynamic] dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class;
    通过传入主键,获取一个数据
    获取所有数据: public static IEnumerable GetAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class;
    更新数据:
    Dapper Contrib 提供了一个用来更新的方法: public static bool Update(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class;
    这个方法比较有意思的是 var entity = connection.Get(1); entity.Name = "测试1"; connection.Update(entity);
    和 var models = connection.GetAll(); foreach(var m in models) { Console.WriteLine(m); m.StringLength ++; } connection.Update(models.AsList());
    都可以,并不会报错。
    不过需要注意的是,如果需要更新的实例没有指定主键值(主减属性没有赋值),则不会有任何行发生更新。而且在更新的时候,会更新所有列,不会因为不赋值就不更新。
    删除方法有两个: public static bool Delete(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class; public static bool DeleteAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class;
    删除也是传入一个实体类,一样也只是需要主键有值,如果没有找到主键对应的数据,则不会有任何变化。Delete与Update一样,如果传入一个List集合也是可以的。
    2. Dapper Transaction
    这个包扩展了Dapper的事务处理能力。虽然是Dapper的扩展包,但是是给IConnection添加了一个扩展方法。使用示例如下: dotnet add package Dapper.Transaction
    老规矩,记得先把包加进来。
    然后代码是这样的: using Dapper.Transaction; using(var connection = new SqliteConnection("Data Source=./demo.db")) { connection.Open(); var transcation = connection.BeginTransaction(); // 编写业务代码 transcation.Commit(); }
    如果使用Dapper Transaction,需要先调用 connection.Open()来确保连接是开启状态。
    transcation这个对象可以当做普通的DbTranscation对象,传给Dapper的方法来使用,也可以当做一个开启了事务的Dapper客户端来使用。也就是说,Dapper对IDbConnection扩展的方法,在这个包对IDbTranscation也扩展了响应的方法:
    3. Dapper Plus
    这个插件是Dapper上用来处理巨量数据的插件, 但这是个收费版的插件 ,不过每个月都有一定的试用期限。想试试的可以下一下: dotnet add package Z.Dapper.Plus
    使用: using Z.Dapper.Plus;
    这个插件在使用之前需要先配置实体类与数据库之间的映射关系: DapperPlusManager.Entity().Table("Customers"); DapperPlusManager.Entity().Table("Suppliers").Identity(x => x.SupplierID);
    该插件支持四组大批量处理方式: Bulk Insert Bulk Update Bulk Merge Bulk Delete // STEP MAPPING DapperPlusManager.Entity().Table("Suppliers").Identity(x => x.SupplierID); DapperPlusManager.Entity().Table("Products").Identity(x => x.ProductID); // STEP BULKINSERT using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools())) { connection.BulkInsert(suppliers).ThenForEach(x => x.Products.ForEach(y => y.SupplierID = x.SupplierID)).ThenBulkInsert(x => x.Products); } // STEP BULKUPDATE using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools())) { connection.BulkUpdate(suppliers, x => x.Products); } // STEP BULKMERGE using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools())) { connection.BulkMerge(suppliers).ThenForEach(x => x.Products.ForEach(y => y.SupplierID = x.SupplierID)).ThenBulkMerge(x => x.Products); } // STEP BULKDELETE using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools())) { connection.BulkDelete(suppliers.SelectMany(x => x.Products)).BulkDelete(suppliers); }
    4. 总结
    这些插件让Dapper更强,也更具备一个完整的ORM的方法,当然实际开发中需要结合实际需求使用。可能并不是所有的都合适。
    Dapper的内容就到此为止了。本来预计下一篇开始 asp.net core的内容,不过有个小伙伴推荐了FreeSql,我看了下感觉挺不错的,就给小伙伴们介绍一下~这一个介绍完成之后,就进入了我期待已久的asp.net core系列了。 更多内容烦请关注 我的博客《高先生小屋》
    软件开发
    2020-06-10 16:04:00
    一、表级锁和行级锁
    表级锁:锁定整张表。
    行级锁:锁定某行。
    又分为:
    Record lock(记录锁):锁定某个索引行;
    gap lock(间隙锁):锁范围,锁行与行之间的间隙(不包括该行);
    Next-key lock(临键锁):上两者结合,先对该行加上记录锁,再对该索引两边加上间隙锁
    二、(行级锁)共享锁和排它锁
    共享锁:读锁。事务A为数据a加上该锁后,事务A只能对a进行读,其他事务可以为a加上共享锁(不能加上排它锁)同样只能读。
    用法:lock in share mode 排它锁:写锁。事务B为数据b加上该锁后,事务B可以对b进行读写,但其他事务不能加任何锁了,只能等待B释放锁。
    用法:for update
    三、(表级锁)意向共享锁和意向排它锁
    意义在于是行锁和表锁共存。用来说明事务稍后会对表中数据加上哪种锁。
    软件开发
    2020-06-16 17:25:00
    你有没有试过代码里等待几秒再继续做下一件事,但是控制台日志啥都没打,一直傻傻的等?
    今天教大家显示实时打印等了多少秒的进度条,希望大家喜欢。
    1、工具
    今天跟大家分享的Python库就是Tqdm,它是 Python 进度条库,可以在 Python 长循环中添加一个进度提示信息。用户只需要封装任意的迭代器,是一个快速、扩展性强的进度条工具库。
    效果图
    2、安装 $ pip install tqdm
    3、tqdm的用法
    主要有3种: 自动控制 手动控制 脚本或命令行
    4、例子
    使用方法一:传入可迭代对象 import time from tqdm import * for i in tqdm(range(10 * 60)): time.sleep(0.1) #进度条每0.1s前进一次,总时间为60 * 10 *0.1=60s
    效果图
    使用方法二:trange
    trange(i) 是 tqdm(range(i)) 的简单写法 import time from tqdm import trange for i in trange(10 * 60): #do something time.sleep(0.1)
    以上例子,如果把60当成变量,这样就可以指定秒数显示进度条。
    效果图
    个人觉得上面的例子已经够满足我的需求了,如果还要继续深入,其他例子可以参考GitHub地址: https://github.com/tqdm/tqdm
    原文链接: https://www.toutiao.com/i6838738761249980931/ 文源网络,仅供学习之用,如有侵权请联系删除。
    在学习Python的道路上肯定会遇见困难,别慌,我这里有一套学习资料,包含40+本电子书,800+个教学视频,涉及Python基础、爬虫、框架、数据分析、机器学习等,不怕你学不会! https://shimo.im/docs/JWCghr8prjCVCxxK/ 《Python学习资料》
    关注公众号【Python圈子】,优质文章每日送达。
    软件开发
    2020-06-16 17:03:00
    在读技术博客的过程中,我们会发现那些能够把知识、成果讲透的博主很多都会做动态图表。他们的图是怎么做的?难度大吗?
    这篇文章就介绍了 Python 中一种简单的动态图表制作方法。
    数据暴增的年代,数据科学家、分析师在被要求对数据有更深的理解与分析的同时,还需要将结果有效地传递给他人。如何让目标听众更直观地理解?当然是将数据可视化啊,而且最好是动态可视化。
    本文将以线型图、条形图和饼图为例,系统地讲解如何让你的数据图表动起来。
    这些动态图表是用什么做的?
    接触过数据可视化的同学应该对 Python 里的 Matplotlib 库并不陌生。它是一个基于 Python 的开源数据绘图包,仅需几行代码就可以帮助开发者生成直方图、功率谱、条形图、散点图等。这个库里有个非常实用的扩展包——FuncAnimation,可以让我们的静态图表动起来。
    FuncAnimation 是 Matplotlib 库中 Animation 类的一部分,后续会展示多个示例。如果是首次接触,你可以将这个函数简单地理解为一个 While 循环,不停地在 “画布” 上重新绘制目标数据图。
    如何使用 FuncAnimation?
    这个过程始于以下两行代码: import matplotlib.animation as ani animator = ani.FuncAnimation(fig, chartfunc, interval = 100)
    从中我们可以看到 FuncAnimation 的几个输入: fig 是用来 「绘制图表」的 figure 对象; chartfunc 是一个以数字为输入的函数,其含义为时间序列上的时间; interval 这个更好理解,是帧之间的间隔延迟,以毫秒为单位,默认值为 200。
    这是三个关键输入,当然还有更多可选输入,感兴趣的读者可查看原文档,这里不再赘述。
    下一步要做的就是将数据图表参数化,从而转换为一个函数,然后将该函数时间序列中的点作为输入,设置完成后就可以正式开始了。
    在开始之前依旧需要确认你是否对基本的数据可视化有所了解。也就是说,我们先要将数据进行可视化处理,再进行动态处理。
    按照以下代码进行基本调用。另外,这里将采用大型流行病的传播数据作为案例数据(包括每天的死亡人数)。 import matplotlib.animation as ani import matplotlib.pyplot as plt import numpy as np import pandas as pdurl = 'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_deaths_global.csv' df = pd.read_csv(url, delimiter=',', header='infer')df_interest = df.loc[ df['Country/Region'].isin(['United Kingdom', 'US', 'Italy', 'Germany']) & df['Province/State'].isna]df_interest.rename( index=lambda x: df_interest.at[x, 'Country/Region'], inplace=True) df1 = df_interest.transposedf1 = df1.drop(['Province/State', 'Country/Region', 'Lat', 'Long']) df1 = df1.loc[(df1 != 0).any(1)] df1.index = pd.to_datetime(df1.index)
    绘制三种常见动态图表
    1、绘制动态线型图
    如下所示,首先需要做的第一件事是定义图的各项,这些基础项设定之后就会保持不变。它们包括:创建 figure 对象,x 标和 y 标,设置线条颜色和 figure 边距等: import numpy as np import matplotlib.pyplot as pltcolor = ['red', 'green', 'blue', 'orange'] fig = plt.figure plt.xticks(rotation=45, ha="right", rotation_mode="anchor") #rotate the x-axis values plt.subplots_adjust(bottom = 0.2, top = 0.9) #ensuring the dates (on the x-axis) fit in the screen plt.ylabel('No of Deaths') plt.xlabel('Dates')
    接下来设置 curve 函数,进而使用 .FuncAnimation 让它动起来: def buildmebarchart(i=int): plt.legend(df1.columns) p = plt.plot(df1[:i].index, df1[:i].values) #note it only returns the dataset, up to the point i for i in range(0,4): p[i].set_color(color[i]) #set the colour of each curveimport matplotlib.animation as ani animator = ani.FuncAnimation(fig, buildmebarchart, interval = 100) plt.show
    2、动态饼状图
    可以观察到,其代码结构看起来与线型图并无太大差异,但依旧有细小的差别。 import numpy as np import matplotlib.pyplot as pltfig,ax = plt.subplots explode=[0.01,0.01,0.01,0.01] #pop out each slice from the piedef getmepie(i): def absolute_value(val): #turn % back to a number a = np.round(val/100.*df1.head(i).max.sum, 0) return int(a) ax.clear plot = df1.head(i).max.plot.pie(y=df1.columns,autopct=absolute_value, label='',explode = explode, shadow = True) plot.set_title('Total Number of Deaths\n' + str(df1.index[min( i, len(df1.index)-1 )].strftime('%y-%m-%d')), fontsize=12)import matplotlib.animation as ani animator = ani.FuncAnimation(fig, getmepie, interval = 200) plt.show
    主要区别在于,动态饼状图的代码每次循环都会返回一组数值,但在线型图中返回的是我们所在点之前的整个时间序列。
    返回时间序列通过 df1.head(i) 来实现,而. max则保证了我们仅获得最新的数据,因为流行病导致死亡的总数只有两种变化:维持现有数量或持续上升。 df1.head(i).max
    3、动态条形图
    创建动态条形图的难度与上述两个案例并无太大差别。在这个案例中,作者定义了水平和垂直两种条形图,读者可以根据自己的实际需求来选择图表类型并定义变量栏。 fig = plt.figure bar = ''def buildmebarchart(i=int): iv = min(i, len(df1.index)-1) #the loop iterates an extra one time, which causes the dataframes to go out of bounds. This was the easiest (most lazy) way to solve this :) objects = df1.max.index y_pos = np.arange(len(objects)) performance = df1.iloc[[iv]].values.tolist[0] if bar == 'vertical': plt.bar(y_pos, performance, align='center', color=['red', 'green', 'blue', 'orange']) plt.xticks(y_pos, objects) plt.ylabel('Deaths') plt.xlabel('Countries') plt.title('Deaths per Country \n' + str(df1.index[iv].strftime('%y-%m-%d'))) else: plt.barh(y_pos, performance, align='center', color=['red', 'green', 'blue', 'orange']) plt.yticks(y_pos, objects) plt.xlabel('Deaths') plt.ylabel('Countries')animator = ani.FuncAnimation(fig, buildmebarchart, interval=100)plt.show
    在制作完成后,存储这些动态图就非常简单了,可直接使用以下代码: animator.save(r'C:\temp\myfirstAnimation.gif')
    感兴趣的读者如想获得详细信息可参考: https://matplotlib.org/3.1.1/api/animation_api.html。
    作者:Costas Andreou
    原文链接: https://towardsdatascience.com/learn-how-to-create-animated-graphs-in-python-fce780421afe 文源网络,仅供学习之用,如有侵权请联系删除。
    在学习Python的道路上肯定会遇见困难,别慌,我这里有一套学习资料,包含40+本电子书,800+个教学视频,涉及Python基础、爬虫、框架、数据分析、机器学习等,不怕你学不会! https://shimo.im/docs/JWCghr8prjCVCxxK/ 《Python学习资料》
    关注公众号【Python圈子】,优质文章每日送达。
    软件开发
    2020-06-16 16:44:00
    作者:一号线 https://segmentfault.com/a/1190000022387211
    RabbitMQ是基于AMQP协议的,通过使用通用协议就可以做到在不同语言之间传递。
    AMQP协议
    核心概念 server:又称broker,接受客户端连接,实现AMQP实体服务。 connection:连接和具体broker网络连接。 channel:网络信道,几乎所有操作都在channel中进行,channel是消息读写的通道。客户端可以建立多个channel,每个channel表示一个会话任务。 message:消息,服务器和应用程序之间传递的数据,由properties和body组成。properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性;body是消息实体内容。 Virtual host:虚拟主机,用于逻辑隔离,最上层消息的路由。一个Virtual host可以若干个Exchange和Queue,同一个Virtual host不能有同名的Exchange或Queue。 Exchange:交换机,接受消息,根据路由键转发消息到绑定的队列上。 banding:Exchange和Queue之间的虚拟连接,binding中可以包括routing key routing key:一个路由规则,虚拟机根据他来确定如何路由 一条消息。 Queue:消息队列,用来存放消息的队列。
    Exchange
    交换机的类型,direct、topic、fanout、headers,durability(是否需要持久化true需要)auto delete当最后一个绑定Exchange上的队列被删除Exchange也删除。 Direct Exchange,所有发送到Direct Exchange的消息被转发到RouteKey 中指定的Queue,Direct Exchange可以使用默认的默认的Exchange (default Exchange),默认的Exchange会绑定所有的队列,所以Direct可以直接使用Queue名(作为routing key )绑定。或者消费者和生产者的routing key完全匹配。 Toptic Exchange,是指发送到Topic Exchange的消息被转发到所有关心的Routing key中指定topic的Queue上。Exchange 将routing key和某Topic进行模糊匹配,此时队列需要绑定一个topic。所谓模糊匹配就是可以使用通配符,“#”可以匹配一个或多个词,“”只匹配一个词比如“log.#”可以匹配“log.info.test” "log. "就只能匹配log.error。 Fanout Exchange:不处理路由键,只需简单的将队列绑定到交换机上。发送到改交换机上的消息都会被发送到与该交换机绑定的队列上。Fanout转发是最快的。
    消息如何保证100%投递
    什么是生产端的可靠性投递? 保证消息的成功发出 保证MQ节点节点的成功接收 发送端MQ节点(broker)收到消息确认应答 完善消息进行补偿机制
    可靠性投递保障方案
    消息落库,对消息进行打标
    消息的延迟投递
    在高并发场景下,每次进行db的操作都是每场消耗性能的。我们使用延迟队列来减少一次数据库的操作。
    消息幂等性 幂等性是什么?点击 这篇文章 看下。
    我对一个动作进行操作,我们肯能要执行100次1000次,对于这1000次执行的结果都必须一样的。比如单线程方式下执行update count-1的操作执行一千次结果都是一样的,所以这个更新操作就是一个幂等的,如果是在并发不做线程安全的处理的情况下update一千次操作结果可能就不是一样的,所以并发情况下的update操作就不是一个幂等的操作。对应到消息队列上来,就是我们即使受到了多条一样的消息,也和消费一条消息效果是一样的。
    高并发的情况下如何避免消息重复消费 唯一id+加指纹码,利用数据库主键去重。
    优点:实现简单
    缺点:高并发下有数据写入瓶颈。 利用Redis的原子性来实习。
    使用Redis进行幂等是需要考虑的问题 是否进行数据库落库,落库后数据和缓存如何做到保证幂等(Redis   和数据库如何同时成功同时失败)? 如果不进行落库,都放在Redis中如何这是Redis和数据库的同步策略?还有放在缓存中就能百分之百的成功吗?
    confirm 确认消息、Return返回消息
    理解confirm消息确认机制 消息的确认,指生产者收到投递消息后,如果Broker收到消息就会给我们  的生产者一个应答,生产者接受应答来确认broker是否收到消息。
    如何实现confirm确认消息。 在Channel上开启确认模式:channel.confirmSelect() 在channel上添加监听:addConfirmListener,监听成功和失败的结果,具体结果对消息进行重新发送或者记录日志。
    return消息机制
    Return消息机制处理一些不可路由的消息,我们的生产者通过指定一个Exchange和Routinkey,把消息送达到某一个队列中去,然后我们消费者监听队列进行消费处理!
    在某些情况下,如果我们在发送消息的时候当Exchange不存在或者指定的路由key路由找不到,这个时候如果我们需要监听这种不可到达的消息,就要使用Return Listener!
    Mandatory 设置为true则会监听器会接受到路由不可达的消息,然后处理。如果设置为false,broker将会自动删除该消息。
    消费端自定义监听
    消费端限流 什么是消费端的限流?限流算法 点击这里 阅读。
    假设我们有个场景,首先,我们有个rabbitMQ服务器上有上万条消息未消费,然后我们随便打开一个消费者客户端,会出现:巨量的消息瞬间推送过来,但是我们的消费端无法同时处理这么多数据。
    这时就会导致你的服务崩溃。其他情况也会出现问题,比如你的生产者与消费者能力不匹配,在高并发的情况下生产端产生大量消息,消费端无法消费那么多消息。 rabbitMQ提供了一种qos(服务质量保证)的功能,即非自动确认消息的前提下,如果有一定数目的消息(通过consumer或者Channel设置qos)未被确认,不进行新的消费。 void basicQOS(unit prefetchSize,ushort prefetchCount,Boolean global)方法。 prefetchSize:0 单条消息的大小限制。0就是不限制,一般都是不限制。 prefetchCount: 设置一个固定的值,告诉rabbitMQ不要同时给一个消费者推送多余N个消息,即一旦有N个消息还没有ack,则consumer将block掉,直到有消息ack global:truefalse 是否将上面的设置用于channel,也是就是说上面设置的限制是用于channel级别的还是consumer的级别的。
    消费端ack与重回队列 消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!(也可以加上最大努力次数的尝试) 如果由于服务器宕机等严重问题,那我们就需要手动进行ack保证消费端的消费成功!
    消息重回队列 重回队列就是为了对没有处理成功的消息,把消息重新投递给broker! 实际应用中一般都不开启重回队列。
    TTL队列/消息 TTL time to live 生存时间。 支持消息的过期时间,在消息发送时可以指定。 支持队列过期时间,在消息入队列开始计算时间,只要超过了队列的超时时间配置,那么消息就会自动的清除。
    死信队列 死信队列:DLX,Dead-Letter-Exchange
    利用DLX,当消息在一个队列中变成死信(dead message,就是没有任何消费者消费)之后,他能被重新publish到另一个Exchange,这个Exchange就是DLX。
    消息变为死信的几种情况: 消息被拒绝(basic.reject/basic.nack)同时requeue=false(不重回队列) TTL过期 队列达到最大长度 DLX也是一个正常的Exchange,和一般的Exchange没有任何的区别,他能在任何的队列上被指定,实际上就是设置某个队列的属性。
    当这个队列出现死信的时候,RabbitMQ就会自动将这条消息重新发布到Exchange上去,进而被路由到另一个队列。可以监听这个队列中的消息作相应的处理,这个特性可以弥补rabbitMQ以前支持的immediate参数的功能。
    死信队列的设置 设置Exchange和Queue,然后进行绑定
    Exchange: dlx.exchange(自定义的名字)
    queue: dlx.queue(自定义的名字)
    routingkey: #(#表示任何routingkey出现死信都会被路由过来)
    然后正常的声明交换机、队列、绑定,只是我们在队列上加上一个参数:
    arguments.put("x-dead-letter-exchange","dlx.exchange");
    rabbitMQ集群模式 主备模式:实现rabbitMQ高可用集群,一般在并发量和数据不大的情况下,这种模式好用简单。又称warren模式。(区别于主从模式,主从模式主节点提供写操作,从节点提供读操作,主备模式从节点不提供任何读写操作,只做备份)如果主节点宕机备份从节点会自动切换成主节点,提供服务。 集群模式:经典方式就是Mirror模式,保证100%数据不丢失,实现起来也是比较简单。 镜像队列,是rabbitMQ数据高可用的解决方案,主要是实现数据同步,一般来说是由2-3节点实现数据同步,(对于100%消息可靠性解决方案一般是3个节点)
    多活模式:这种模式也是实现异地数据复制的主流模式,因为shovel模式配置相对复杂,所以一般来说实现异地集群都是使用这种双活,多活的模式,这种模式需要依赖rabbitMQ的federation插件,可以实现持续可靠的AMQP数据。
    rabbitMQ部署架构采用双中心模式(多中心)在两套(或多套)数据中心个部署一套rabbitMQ集群,各中心的rabbitMQ服务需要为提供正常的消息业务外,中心之间还需要实现部分队列消息共享。
    大家可以关注微信公众号:Java技术栈,可以获取我整理的 N 篇消息队列教程,都是干货,第一时间更新。
    多活架构如下:
    federation插件是一个不需要构建Cluster,而在Brokers之间传输消息的高性能插件,federation可以在brokers或者cluster之间传输消息,连接的双方可以使用不同的users或者virtual host双方也可以使用不同版本的erlang或者rabbitMQ版本。federation插件可以使用AMQP协议作为通讯协议,可以接受不连续的传输。
    Federation Exchanges,可以看成Downstream从Upstream主动拉取消息,但 并不是拉取所有消息,必须是在Downstream上已经明确定义Bindings关系的 Exchange,也就是有实际的物理Queue来接收消息,才会从Upstream拉取消息 到Downstream。
    使用AMQP协议实施代理间通信,Downstream 会将绑定关系组合在一起, 绑定/解除绑定命令将发送到Upstream交换机。
    因此,Federation Exchange只接收具有订阅的消息。 HAProxy是一款提供高可用性、负载均衡以及基于TCP (第四层)和HTTP (第七层)应用的代理软件,支持虚拟主机,它是免费、快速并且可靠的一种解决 方案。
    HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会 话保持或七层处理。HAProxy运行在时下的硬件上,完全可以支持数以万计的 并发连接。
    并且它的运行模式使得它可以很简单安全的整合进您当前的架构中 同时可以保护你的web服务器不被暴露到网络上。
    HAProxy性能为何这么好? 单进程、事件驱动模型显著降低了.上下文切换的开销及内存占用. 在任何可用的情况下,单缓冲(single buffering)机制能以不复制任何数据的方式完成读写操作,这会节约大量的CPU时钟周期及内存带宽 借助于Linux 2.6 (>= 2.6.27.19). 上的splice()系统调用,HAProxy可以实现零复制转发(Zero-copy forwarding),在Linux 3.5及以上的OS中还可以实现心零复制启动(zero-starting) 内存分配器在固定大小的内存池中可实现即时内存分配,这能够显著减少创建一个会话的时长 树型存储:侧重于使用作者多年前开发的弹性二叉树,实现了以O(log(N))的低开销来保持计时器命令、保持运行队列命令及管理轮询及最少连接队列
    keepAlive KeepAlived软件主要是通过VRRP协议实现高可用功能的。VRRP是 Virtual Router RedundancyProtocol(虚拟路由器冗余协议)的缩写, VRRP出现的目的就是为了解决静态路由单点故障问题的,它能够保证当 个别节点宕机时,整个网络可以不间断地运行所以,Keepalived - -方面 具有配置管理LVS的功能,同时还具有对LVS下面节点进行健康检查的功 能,另一方面也可实现系统网络服务的高可用功能
    keepAlive的作用 管理LVS负载均衡软件 实现LVS集群节点的健康检查中 作为系统网络服务的高可用性(failover)
    Keepalived如何实现高可用
    Keepalived高可用服务对之间的故障切换转移,是通过VRRP (Virtual Router Redundancy Protocol ,虚拟路由器冗余协议)来实现的。
    在Keepalived服务正常工作时,主Master节点会不断地向备节点发送( 多播的方式)心跳消息,用以告诉备Backup节点自己还活看,当主Master节点发生故障时,就无法发送心跳消息,备节点也就因此无法继续检测到来自主Master节点的心跳了,于是调用自身的接管程序,接管主Master节点的IP资源及服务。
    而当主Master节点恢复时备Backup节点又会释放主节点故障时自身接管的IP资源及服务,恢复到原来的备用角色。
    推荐去我的博客阅读更多:
    1. Java JVM、集合、多线程、新特性系列教程
    2. Spring MVC、Spring Boot、Spring Cloud 系列教程
    3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程
    4. Java、后端、架构、阿里巴巴等大厂最新面试题
    觉得不错,别忘了点赞+转发哦!
    软件开发
    2020-06-16 16:39:00