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

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


  • 首页

  • 归档

  • 搜索

如何将服务器http地址转换成https地址 如何将服务器h

发表于 2021-07-05

如何将服务器http地址转换成https地址

引言:最近在做微信小程序,然后需要将http地址转换成https地址。这是我参与新手入门的第1篇文章

一、http与https的区别

  • HTTP:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML页面的方法。它可以使浏览器更加高效。HTTP 协议是以明文方式发送信息的,如果黑客截取了 Web 浏览器和服务器之间的传输报文,就可以直接获得其中的信息。
  • HTTP的连接是通过网络与服务器建立连接的该连接采用TCP连接,默认端口为80
  • HTTPS:是以安全为目标的 HTTP 通道,是 HTTP 的安全版。HTTPS 的安全基础是 SSL。SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。SSL 协议可分为两层:SSL 记录协议(SSL Record Protocol),它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL 握手协议(SSL Handshake Protocol),它建立在 SSL 记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

二、申请SSL证书

既然提到了SSL证书那么接下来就是申请SSL证书
本文以阿里云服务器为例子

  • 申请阿里云CA证书
    进入阿里云平台,找到SSL证书界面,点击免费证书并点击立即购买,在选择界面选择DV单域名证书,最后点击购买即可(可以看见价格为0)。
    在这里插入图片描述

接下来回到刚才的界面点击创建证书,并点击证书申请。
在这里插入图片描述

在这里插入图片描述

可以看见证书绑定域名而且是必填的,看来还需要申请一个域名。

三、域名申请

关于域名申请,可以自行去阿里云或者腾讯云等平台注册一个域名。
域名申请完成后记得进行网站备份,阿里云上网站备份得好久….我记得我备份网站用了近半个月。

申请完域名后便可以做之前的操作了。填写完域名,点击下一步并点击验证即可。
在这里插入图片描述

最后申请完成的证书详情如下
在这里插入图片描述

完成后下载该证书并部署到服务器上即可。

例如:在python flask框架中具体为:

1
2
3
4
5
6
7
8
python复制代码from flask import Flask, flash, get_flashed_messages

app = Flask(__name__,static_url_path='/')


if __name__ == '__main__':

app.run(host='0.0.0.0',port=443,ssl_context=('域名.pem','域名.key'))

本文转载自: 掘金

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

基于Spring Security实现自定义认证-以短信登录

发表于 2021-07-05

基于Spring Security实现自定义认证-以短信登录为例

《Spring Security实战》、慕课网《Spring Security技术栈开发企业级认证与授权》笔记

实现基于Spring Security的认证有两种方式

  1. 增加一个过滤器继承OncePerRequestFilter,将这个Filter放到HttpSecurity的合适的位置。(继承OncePerRequestFilter的目的是确保一次请求只通过一次该过滤器)
  2. 基于Spring Security的自定义认证

方法1的那种过滤器的方式大家应该很熟悉,就不展开记录了,下面详细说一下基于Spring Security的自定义认证。

先以UsernamePassword认证为例,先捋一下认证流程。

基本概念

  • Authentication:Spring Security验证的封装类,包括权限、确定身份正确的凭据、身份详细信息、是否被验证。常见的实现类有RememberMeAuthenticationToken、UsernamePasswordAuthenticationToken
  • AuthenticationProvider:Spring Security的一个验证过程,一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。Authentication在AuthenticationProvider中流动。用大白话说就是不同的AuthenticationProvider提供不同的token。
  • AuthenticationManager:处理Authentication的请求,整个系统只有一个。ProviderManager是AuthenticationManager的实现类

UsernamePassword认证流程

  1. 进入UsernamePasswordAuthenticationFilter类:attemptAuthentication()方法中将前端传过来的username、password封装到UsernamePasswordAuthenticationToken中并标记为未认证。最后调用this.getAuthenticationManager().authenticate(authRequest)交给AuthenticationManager处理。
  2. ProviderManager根据传入的token类从众多的AuthenticationProvider中找出合适的AuthenticationProvider来处理改认证。
  3. 在具体的XXXAuthenticationProvider中认证用户返回带有认证通过和详细信息的token。

所以我们基于Spring Security自定义一个认证要新建一个XXXAuthenticationToken和XXXAuthenticationProvider,最后将自己的逻辑加入到HttpSecurity的过滤器链中。

下边以短信登录为例实践上边的知识点。

短信登录逻辑:

  1. 前端输入手机号,然后获取短信验证码,最后带着手机号和验证码一起登录
  2. 服务端要监听这个登录地址,校验验证码是否正确
  3. 正确使用Spring Security自定义认证颁发一个token给前端。

下面贴代码:

SmsValidateCodeFilter

短信验证码过滤器,验证短信登录验证码是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
java复制代码package com.zchi.customizeAuthentication.security.smsCode;

import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @Description 短信验证码过滤器,验证短信登录验证码是否正确
* @Author 张弛
* @Datee 2021/7/4
* @Version 1.0
**/
@Setter
public class SmsValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

private AntPathMatcher pathMatcher = new AntPathMatcher();

private AuthenticationFailureHandler authenticationFailureHandler;

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
String url = "/authentication/smsLogin";
if (pathMatcher.match(url, httpServletRequest.getRequestURI())) {
action = true;
}

if (action) {

try {
validate(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}

}

filterChain.doFilter(httpServletRequest, httpServletResponse);
}

private void validate(ServletWebRequest request) throws ServletRequestBindingException {

// todo 获取验证码,现在先写死,之后改成从session或者redis中获取
String codeInSession = "f123";

String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");

if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}

if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}

// todo 验证码是否过期

if(!StringUtils.equals(codeInSession, codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}

// todo 使用完这个验证码就删除
}

public void setFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
}

SmsCodeAuthenticationToken

直接照着UsernamePasswordAuthenticationToken写就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码package com.zchi.customizeAuthentication.security.smsCode;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import javax.security.auth.Subject;
import java.util.Collection;

/**
* @Description 直接照着UsernamePasswordAuthenticationToken写就行
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
**/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

private final Object principal;

public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}

public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}

@Override
public Object getCredentials() {
return this.principal;
}

@Override
public Object getPrincipal() {
return null;
}

@Override
public boolean implies(Subject subject) {
return false;
}
}

SmsCodeAuthenticationProvider

Authentication的提供者,根据之前SmsCodeAuthenticationFilter中存入token中的信息获取当前用户信息。声明支持的token类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码package com.zchi.customizeAuthentication.security.smsCode;

import lombok.Data;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
* @Description Authentication的提供者,根据之前SmsCodeAuthenticationFilter中存入token中的信息获取当前用户信息。声明支持的token类型
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
* @see SmsCodeAuthenticationFilter,SmsCodeAuthenticationToken
**/
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

private UserDetailsService userDetailsService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
// 需要把未认证中的一些信息copy到已认证的token中
authenticationResult.setDetails(token);
return authenticationResult;
}

// 该provider支持的token
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}

SmsCodeAuthenticationSecurityConfig

将自己写的这些类配置到过滤器链上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码package com.zchi.customizeAuthentication.security.smsCode;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
* @Description 将自己写的这些类配置到过滤器链上
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
**/
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private SmsCodeAuthenticationSuccessHandler authenticationSuccessHandler;

@Autowired
private SmsCodeAuthenctiationFailureHandler authenticationFailureHandler;

@Autowired
private UserDetailsService userDetailsService;

@Override
public void configure(HttpSecurity httpSecurity) {
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);

SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

SmsValidateCodeFilter smsValidateCodeFilter = new SmsValidateCodeFilter();
smsValidateCodeFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
httpSecurity.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
}
}

SecurityConfig

项目的安全配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码package com.zchi.customizeAuthentication.security;

import com.zchi.customizeAuthentication.security.smsCode.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
* @Description 项目的安全配置
* @Author 张弛
* @Datee 2021/7/3
* @Version 1.0
**/
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/authentication/require",
"/login",
"/code/*",
"/error",
"/authentication/smsLogin"
)
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
// 将我们自己的短信登录配置到项目的过滤器链上
.apply(smsCodeAuthenticationSecurityConfigs);
}

@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> {
return new UserDetails() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return null;
}

@Override
public boolean isAccountNonExpired() {
return false;
}

@Override
public boolean isAccountNonLocked() {
return false;
}

@Override
public boolean isCredentialsNonExpired() {
return false;
}

@Override
public boolean isEnabled() {
return false;
}
};
};
}
}

参考:

《Spring Security实战》

《Spring Security技术栈开发企业级认证与授权》

代码连接securityDemo: 《基于Spring Security实现自定义认证-以短信登录为例》代码 (gitee.com)

本文转载自: 掘金

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

CSAPP第三章——程序的机器级表示:学习笔记总结

发表于 2021-07-05

花了半个多月,补完王爽老师的汇编语言后,跟着CMU的视频课+课本,学完了第三章的知识,最深的感触就是CSAPP无论是视频还是书的质量都非常的硬,不愧它的盛名。(lab6和课后的家庭作业还没做,之后再补)
现在来对前面所学做一个总结。(大致按照CMU视频的顺序进行)

一、Basics 基础

1.使用指令新建、编辑、汇编、链接汇编语言程序
①新建并编辑源代码
命令:getdit sum.c
说明:gedit是一个GNOME桌面环境下兼容UTF-8的文本编辑器。使用vi或者vim同样可以实现新建与编辑。
②预处理【sum.c -> sum.i】
命令:gcc -E sum.c -o sum.i 【sum.c -> sum.i】
说明:预处理时,编译器会将C源代码中包含的的头文件编译进来
③编译 【sum.i -> sum.s】
命令:gcc -S sum.i -o sum.s
说明:gcc首先检查代码的规范性,是否有语法错误,确定代码实际要做的工作,让后将代码翻译成汇编语言
④汇编【sum.s -> sum.o】
命令:gcc -c sum.s -o sum.o
说明:gcc进行汇编阶段,将编译阶段生成的”.s”文件转成二进制目标代码(可重定位目标文件)
⑤链接【sum.o -> sum】
命令:gcc sum.o -o sum
说明:链接过程将有关的目标文件彼此连接起来,使得所有目标文件成为一个能够执行的统一整体。
⑥执行
命令:./sum
说明:执行可执行文件,输出结果

2.数据格式
b(byte):字节
w(word):1字=2字节
l(double words):双字=4字节
q(quad words):四字=8字节
对应的mov指令为:
movb movw movl movq

al:1字节
ax:2字节
eax:4字节
rax:8字节

3.数据传送
传内存数据的格式:在这里插入图片描述
传地址的格式:
在这里插入图片描述
扩展:
扩展分为零扩展和符号扩展,若要扩展后保持相同的数,对于有符号数,符号扩展(即高位补原来的最高位)可以保证扩展前后相同,对于无符号数,零扩展可以保证扩展前后相同。指令,以字节->字为例,其他同样格式。
零扩展:movzbw dl,ax
符号扩展:movsbw dl,ax
另,符号扩展多一个cltq指令,表示将%(eax)符号扩展->rax
另,当给32位寄存器赋值时,总会将高位自动改为0

4.寄存器作用

0-63 0-31 0-15 8-15 0-7 使用惯例
%rax %eax %ax %ah %al 保存返回值
%rbx %ebx %bx %bh %bl 被调用者保存
%rcx %ecx %cx %ch %cl 第4个参数
%rdx %edx %dx %dh %dl 第3个参数
%rsi %esi %si 无 %sil 第2个参数
%rdi %edi %di 无 %dil 第1个参数
%rbp %ebp %bp 无 %bpl 被调用者保存
%rsp %esp %sp 无 %spl 栈指针
%r8 %r8d %r8w 无 %r8b 第5个参数
%r9 %r9d %r9w 无 %r9b 第6个参数
%r10 %r10d %r10w 无 %r10b 调用者保存
%r11 %r11d %r11w 无 %r11b 调用者保存
%r12 %r12d %r12w 无 %r12b 被调用者保存
%r13 %r13d %r13w 无 %r13b 被调用者保存
%r14 %r14d %r14w 无 %r14b 被调用者保存
%r15 %r15d %r15w 无 %r15b 被调用者保存
前六个参数分别存在 rdi rsi rdx rcx r8 r9中,若有更多的参数,则放在栈上,且栈顶为第7个参数,其次第8个参数…
rbx rdx r12~r15是被调用者保存,在函数体中如果要调用别的函数,可以把有用的数据放在这些被调用者保存的寄存器中,这样在被调用的函数中,如果要改变这些寄存器的值,总是会事先入栈保存并在return前弹出。
而调用者保存的寄存器,在调用函数前,不希望被修改的有用数据总是要先保存在栈中,在调用函数后再弹出使用。

5.算术和逻辑操作
在这里插入图片描述
注意算术右移(SAR)和逻辑右移(SHR),算术右移是有符号数的右移,高位补1,逻辑右移是无符号数的右移,高位补0.

imulq:有符号全乘法
mulq:无符号全乘法
【上面两条指令都需要一个参数在%rax中】
idivq:有符号除法
divq:无符号除法
乘积存在%rdx(高64位)和%rax(低64位)
商存在%rax中,余数存在%rdx中。

question:为什么家庭作业1中的imulq指令乘积存在rdx中...??

二、Control 控制

1.Condition code 条件码

条件码 英文 含义
CF Carry Flag (for unsigned) 无符号数相加时的进位标记
ZF Zero Flag 结果是0
SF Sign Flag (for signed) 符号标记,运算结果最高有效位为1(负数),则SF置为1
OF Overflow Flag (for signed) 溢出标记,表示有符号数的溢出(两个同符号数相加结果为不同符号,就会有这个溢出)
在这里插入图片描述
lea不会设置这四个标志位。
cmpq用于比较大小,testq用于将某个数和0比较

CF和OF的区别:(参考了大佬的blog)
①首先需要知道,计算机对数值的存储采用补码形式存储,一来避免了+0和-0的尴尬,二来数值的加法和减法可以统一为补码的加法。在汇编语言层面,定义变量的时候,没有 signed和unsignde 之分,汇编器统统将你输入的整数字面量当作有符号数(最高位的符号位根据输入的数值符号决定)处理成二进制补码存入到计算机中,只有这一个标准!汇编器不会区分有符号还是无符号然后用两个标准来处理,它统统当作有符号的!并且统统汇编成补码!


②那么,有符号和无符号数在计算机中是怎么区分并对他们的运算采用不同的策略呢?有一个重要的点是,补码是一个强大的设计,其统一了无符号数和有符号数的加法运算是相同的,即从0位到高位一个个相加,且相加的时候再加上从前面的进位。【所以用同一个加法器即可】那么,无符号数和有符号数在加法运算的时候并不区分,用同一个加法器即可,只是对结果拥有不同的解释权罢了【但是乘法运算用不了同一套了,能力有限】


③ OF、CF、SF标志。
先看CF标志位,书上说CF标志位只对无符号数有意义,首先明白一点,即使是两个有符号数相加,也会导致CF的变动,并不是说有符号数,编译器不设置CF位。因为CF的标志位的变动是由于最高有效位(如果对于8位数,就是第8位)向更高位(第9位)产生了进位或者借位而产生,而对于有符号数来说,最高位是符号位,它的变动和数值位的变动意义不一样。所以对于有符号数,CF也可能发生变动,但是它的变动是没意义的。而如果是无符号数,它的变动就意味中8位的内存或寄存器不足以保存数据,因为数据产生了进位或借位。
再看OF标志位,它只对有符号数有意义,因为两个标准的8位有符号数据(标准指的是赋值的时候不要赋超过有符号数范围的数字,由于截断,即是8位能保存,保存进来的数据数值大小早就产生了变化),这2个数据只有同号(都为正或为负)相加才会溢出,也就是结果超过有符号数的范围。例如2个正数,符号位(第8位)都为0,相加后发生溢出,符号位由于第7位的进位变成了1,两个正数相加变为了负数?由此对OF产生了作用,如此来说OF的作用是由于符号位发生变化,如果是两个无符号数,最高位代表的并不是符号意义,产生了变动也是无意义的,所以说OF只对有符号数有意义。
最后SF标志,有了上面的介绍,就能理解SF看的是最高位的符号位意义,对于无符号数来说,最高位代表的是数值意义,并不是符号意义。


④可爱又可怕的c语言。
为什么又扯到 c 了?因为大多数遇到有符号还是无符号问题的朋友,都是c里面的 signed 和 unsigned 声明引起的,那为什么开头是从汇编讲起呢?因为我们现在用的c编译器,都是将c语言代码编译成汇编语言代码,然后再用汇编器汇编成机器码的。搞清楚了汇编,就相当于从根本上明白了c,而且,用机器的思维去考虑问题,必须用汇编。(我一般遇到什么奇怪的c语言的问题都是把它编译成汇编来看。)
C 是可爱的,因为c符合kiss 原则,对机器的抽象程度刚刚好,让我们即提高了思维层面(比汇编的机器层面人性化多了),又不至于离机器太远 (像c# ,Java之类就太远了)。当初K&R 版的c就是高级一点的汇编……:-)
C又是可怕的,因为它把机器层面的所有的东西都反应了出来,像这个有没有符号的问题就是一例(java就不存在这个问题,因为它被设计成所有的整数都是有符号的)。为了说明c的可怕特举一例:

1
2
3
4
5
6
7
8
9
10
c复制代码#include <stdio.h> 
#include <string.h>
int main()
{
int x = 2;
char * str = "abcd";
int y = (x - strlen(str) ) / 2;
//注:原作者这样写,编译器可能会对其优化,直接使用右移移位指令而不是采用除法指令,改成3即可看到
printf("%d\n",y);
}

结果应该是 -1 但是却得到:2147483647 。为什么?因为strlen的返回值,类型是size_t,也就是unsigned int ,与 int 混合计算时,int类型被自动转换为unsigned int了,结果自然出乎意料。。。
观察编译后的代码,除法指令为 div ,意味无符号除法。解决办法就是强制转换,变成 int y = (int)(x - strlen(str) ) / 2; 强制向有符号方向转换(编译器默认正好相反),这样一来,除法指令编译成 idiv 了。我们知道,就是同样状态的两个内存单位,用有符号处理指令 imul ,idiv 等得到的结果,与用 无符号处理指令mul,div等得到的结果,是截然不同的!所以牵扯到有符号无符号计算的问题,特别是存在讨厌的自动转换时,要倍加小心!(这里自动转换时,无论gcc还是cl都不提示!!!)
为了避免这些错误,建议,凡是在运算的时候,确保你的变量都是 signed 的。


2.Conditional branches 条件分支
在这里插入图片描述注:greater和less是有符号的,above和below是无符号的。
在这里插入图片描述
注:原则:总是先判断!test然后决定是否转向else(上面这个是条件控制转移)


Conditional move条件传送
在这里插入图片描述

注:这是一种分支预测优化技术,基本思想是把then代码和else都执行得到两个结果,然后才会选择使用哪一个结果,看起来似乎浪费时间但事实是如果是简单的计算,会更有效率,学到性能优化时会明白原因。【这是一种流水线技术,当代码运行时到达一个分支,他们会试着猜测分支结果,这被称为分支预测技术,并且他们非常擅于预测,98%的时候他们都能猜对,所以他们可以在路上预测suta曲线,并开始朝这个方向前进,只要猜测正确,就会非常有效率,但是如果分支猜错了,必须要阻止它并转向另一个方向重新开始】

3.Loops 循环
do while:
在这里插入图片描述


while:
在这里插入图片描述


for:
在这里插入图片描述

4.switch语句
Switch语句利用了Jump Table跳转表机制,跳转表机制避免了需要顺序地判断各个case(O[n])【或者二分算法O(logn)】,而可以根据偏移直接跳转到那个代码块case(O(1))
在这里插入图片描述)在这里插入图片描述
注:switch语句总是用ja (max of x) 先判断是否为默认,若是默认的话直接跳转默认,不是默认的话才利用跳转表机制,以x的值为索引去查跳转表,然后跳到那个跳转表中存着的分支代码的地址。
如果不是从0开始的,那么会给一个偏移量【所以总变成从0开始所以】
如果case值很稀疏,那么编译器会优化成if-else语句
在这里插入图片描述

三、Procedures 过程

栈比较特别,他是自高地址往下生长的。
callq:
1.下条指令的地址入栈
2.rip转到函数调用的入口处
retq:(假定栈顶是想要跳转的地址):
push rip
传递数据:
rdi rsi rdx rcx r8 r9传递数据,多余的在栈中传递(第7个在栈顶)。
这里的参数要求是整形或指针浮点类型的参数是由另外一组单独的寄存器来传递的.
运行方式:
如果是单线程的运行模型Single threaded model,在函数嵌套调用中,会不断地往压栈,在任何时刻只有一个函数在运行,在函数返回时,直接可以根据栈顶的地址返回。所以可以不断地调用函数和返回函数来实现复杂的工作。
栈帧:(不确定对错)
栈帧是从刚进入这个函数后,return前栈多出的那部分,包括了在该函数中的局部变量,函数再调用其他函数的返回地址及参数超过6时的传参 以及其他。<也就是控制权在该函数时所分配的所有栈内存>
需知但不需要理解:
我们通常可以看到,程序经常在栈上分配比实际需求多的空间,这是因为,有一些约定要求内存地址保持对齐,对齐的方式可以有很多种,这有点含糊不清,但不用担心是否会有未使用的空间和函数(目前不需要理解).<或许其中一个原因是canary金丝雀>
rsp与rbp:

在栈中,我们需要的只是在栈中为每个被调用且未返回的过程保留一个栈帧。通常一个栈帧由两个指针分隔,一是栈指针RSP,一是基指针RBP,但基指针是一个可选项,一般不会使用除了非常特殊的情况,所以这个寄存器实际上并不会在你的程序中以帧指针的形式出现,它将被用作常规寄存器。
如果是分配固定栈帧的,如这里分配16,那么在结尾编译器会释放掉这个空间。但是如果是可变长的数组或内存缓冲区时,编译器不知道分配多少空间,那么就会采用基指针来记录一开始的栈帧,然后在结束后返回时令rps=基指针

被调用者保存:(rbp,rbx,r12~r13),例如A调用B,如果B可能会改变A中的这些寄存器的话,那么在B中会先入栈。【帮调用者擦屁股】
在这里插入图片描述
这里有一点困惑,之所以常函数或者常量参数尽量加const的原因,应该就是为了能让编译器知道是不会改变的,就免去了保存的过程,吗???

调用者保存:(除了上面那些寄存器)例如A调用B,如果调用B可能会改变这些寄存器的值,那就先在A调用B前先存起来【自己的东西自己保护好】

今天先到这里,明天再续。

四、Data 数据

内容:

五、Advanced topics

内容:

本文转载自: 掘金

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

Nacos配置中心交互模型是 push 还是 pull ?你

发表于 2021-07-05

本文案例收录在 github.com/chengxy-nds…

大家好,我是小富~

对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现、配置管理,非常好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏。

比如我们今天要讨论的话题,Nacos在做配置中心的时候,配置数据的交互模式是服务端推过来还是客户端主动拉的?

这里我先抛出答案:客户端主动拉的!

接下来咱们扒一扒Nacos的源码,来看看它具体是如何实现的?

配置中心

聊Nacos之前简单回顾下配置中心的由来。

简单理解配置中心的作用就是对配置统一管理,修改配置后应用可以动态感知,而无需重启。

因为在传统项目中,大多都采用静态配置的方式,也就是把配置信息都写在应用内的yml或properties这类文件中,如果要想修改某个配置,通常要重启应用才可以生效。

但有些场景下,比如我们想要在应用运行时,通过修改某个配置项,实时的控制某一个功能的开闭,频繁的重启应用肯定是不能接受的。

尤其是在微服务架构下,我们的应用服务拆分的粒度很细,少则几十多则上百个服务,每个服务都会有一些自己特有或通用的配置。假如此时要改变通用配置,难道要我挨个改几百个服务配置?很显然这不可能。所以为了解决此类问题配置中心应运而生。

配置中心

推与拉模型

客户端与配置中心的数据交互方式其实无非就两种,要么推push,要么拉pull。

推模型

客户端与服务端建立TCP长连接,当服务端配置数据有变动,立刻通过建立的长连接将数据推送给客户端。

优势:长链接的优点是实时性,一旦数据变动,立即推送变更数据给客户端,而且对于客户端而言,这种方式更为简单,只建立连接接收数据,并不需要关心是否有数据变更这类逻辑的处理。

弊端:长连接可能会因为网络问题,导致不可用,也就是俗称的假死。连接状态正常,但实际上已无法通信,所以要有的心跳机制KeepAlive来保证连接的可用性,才可以保证配置数据的成功推送。

拉模型

客户端主动的向服务端发请求拉配置数据,常见的方式就是轮询,比如每3s向服务端请求一次配置数据。

轮询的优点是实现比较简单。但弊端也显而易见,轮询无法保证数据的实时性,什么时候请求?间隔多长时间请求一次?都是不得不考虑的问题,而且轮询方式对服务端还会产生不小的压力。

长轮询

开篇我们就给出了答案,nacos采用的是客户端主动拉pull模型,应用长轮询(Long Polling)的方式来获取配置数据。

额?以前只听过轮询,长轮询又是什么鬼?它和传统意义上的轮询(暂且叫短轮询吧,方便比较)有什么不同呢?

短轮询

不管服务端配置数据是否有变化,不停的发起请求获取配置,比如支付场景中前段JS轮询订单支付状态。

这样的坏处显而易见,由于配置数据并不会频繁变更,若是一直发请求,势必会对服务端造成很大压力。还会造成推送数据的延迟,比如:每10s请求一次配置,如果在第11s时配置更新了,那么推送将会延迟9s,等待下一次请求。

为了解决短轮询的问题,有了长轮询方案。

长轮询

长轮询可不是什么新技术,它不过是由服务端控制响应客户端请求的返回时间,来减少客户端无效请求的一种优化手段,其实对于客户端来说与短轮询的使用并没有本质上的区别。

客户端发起请求后,服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。

Nacos初识

为了后续演示操作方便我在本地搭了个Nacos。注意: 运行时遇到个小坑,由于Nacos默认是以cluster集群的方式启动,而本地搭建通常是单机模式standalone,这里需手动改一下启动脚本startup.X中的启动模式。

直接执行/bin/startup.X就可以了,默认用户密码均是nacos。

几个概念

Nacos配置中心的几个核心概念:dataId、group、namespace,它们的层级关系如下图:

dataId:是配置中心里最基础的单元,它是一种key-value结构,key通常是我们的配置文件名称,比如:application.yml、mybatis.xml,而value是整个文件下的内容。

目前支持JSON、XML、YAML等多种配置格式。

group:dataId配置的分组管理,比如同在dev环境下开发,但同环境不同分支需要不同的配置数据,这时就可以用分组隔离,默认分组DEFAULT_GROUP。

namespace:项目开发过程中肯定会有dev、test、pro等多个不同环境,namespace则是对不同环境进行隔离,默认所有配置都在public里。

架构设计

下图简要描述了nacos配置中心的架构流程。

客户端、控制台通过发送Http请求将配置数据注册到服务端,服务端持久化数据到Mysql。

客户端拉取配置数据,并批量设置对dataId的监听发起长轮询请求,如服务端配置项变更立即响应请求,如无数据变更则将请求挂起一段时间,直到达到超时时间。为减少对服务端压力以及保证配置中心可用性,拉取到配置数据客户端会保存一份快照在本地文件中,优先读取。

这里我省略了比较多的细节,如鉴权、负载均衡、高可用方面的设计(其实这部分才是真正值得学的,后边另出文讲吧),主要弄清客户端与服务端的数据交互模式。

下边我们以Nacos 2.0.1版本源码分析,2.0以后的版本改动较多,和网上的很多资料略有些不同
地址:github.com/alibaba/nac…

客户端源码分析

Nacos配置中心的客户端源码在nacos-client项目,其中NacosConfigService实现类是所有操作的核心入口。

说之前先了解个客户端数据结构cacheMap,这里大家重点记住它,因为它几乎贯穿了Nacos客户端的所有操作,由于存在多线程场景为保证数据一致性,cacheMap采用了AtomicReference原子变量实现。

1
2
3
4
java复制代码/**
* groupKey -> cacheData.
*/
private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(new HashMap<>());

cacheMap是个Map结构,key为groupKey,是由dataId, group, tenant(租户)拼接的字符串;value为CacheData对象,每个dataId都会持有一个CacheData对象。

获取配置

Nacos获取配置数据的逻辑比较简单,先取本地快照文件中的配置,如果本地文件不存在或者内容为空,则再通过HTTP请求从远端拉取对应dataId配置数据,并保存到本地快照中,请求默认重试3次,超时时间3s。

获取配置有getConfig()和getConfigAndSignListener()这两个接口,但getConfig()只是发送普通的HTTP请求,而getConfigAndSignListener()则多了发起长轮询和对dataId数据变更注册监听的操作addTenantListenersWithContent()。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}

@Override
public String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener)
throws NacosException {
String content = getConfig(dataId, group, timeoutMs);
worker.addTenantListenersWithContent(dataId, group, content, Arrays.asList(listener));
return content;
}

注册监听

客户端注册监听,先从cacheMap中拿到dataId对应的CacheData对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JAVA复制代码public void addTenantListenersWithContent(String dataId, String group, String content,
List<? extends Listener> listeners) throws NacosException {
group = blank2defaultGroup(group);
String tenant = agent.getTenant();
// 1、获取dataId对应的CacheData,如没有则向服务端发起长轮询请求获取配置
CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
synchronized (cache) {
// 2、注册对dataId的数据变更监听
cache.setContent(content);
for (Listener listener : listeners) {
cache.addListener(listener);
}
cache.setSyncWithServer(false);
agent.notifyListenConfig();
}
}

如没有则向服务端发起长轮询请求获取配置,默认的Timeout时间为30s,并把返回的配置数据回填至CacheData对象的content字段,同时用content生成MD5值;再通过addListener()注册监听器。

CacheData也是个出场频率非常高的一个类,我们看到除了dataId、group、tenant、content这些相关的基础属性,还有几个比较重要的属性如:listeners、md5(content真实配置数据计算出来的md5值),以及注册监听、数据比对、服务端数据变更通知操作都在这里。

其中listeners是对dataId所注册的所有监听器集合,其中的ManagerListenerWrap对象除了持有Listener监听类,还有一个lastCallMd5字段,这个属性很关键,它是判断服务端数据是否更变的重要条件。

在添加监听的同时会将CacheData对象当前最新的md5值赋值给ManagerListenerWrap对象的lastCallMd5属性。

1
2
3
4
5
java复制代码public void addListener(Listener listener) {
ManagerListenerWrap wrap =
(listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
: new ManagerListenerWrap(listener, md5);
}

看到这对dataId监听设置就完事了?我们发现所有操作都围着cacheMap结构中的CacheData对象,那么大胆猜测下一定会有专门的任务来处理这个数据结构。

变更通知

客户端又是如何感知服务端数据已变更呢?

我们还是从头看,NacosConfigService类的构造器中初始化了一个ClientWorker,而在ClientWorker类的构造器中又启动了一个线程池来轮询cacheMap。

而在executeConfigListen()方法中有这么一段逻辑,检查cacheMap中dataId的CacheData对象内,MD5字段与注册的监听listener内的lastCallMd5值,不相同表示配置数据变更则触发safeNotifyListener方法,发送数据变更通知。

1
2
3
4
5
6
7
java复制代码void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
}
}
}

safeNotifyListener()方法单独起线程,向所有对dataId注册过监听的客户端推送变更后的数据内容。

客户端接收通知,直接实现receiveConfigInfo()方法接收回调数据,处理自身业务就可以了。

1
2
3
4
5
6
7
8
9
10
11
java复制代码configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("receive:" + configInfo);
}

@Override
public Executor getExecutor() {
return null;
}
});

为了理解更直观我用测试demo演示下,获取服务端配置并设置监听,每当服务端配置数据变化,客户端监听都会收到通知,一起看下效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "localhost";
String dataId = "test";
String group = "DEFAULT_GROUP";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("数据变更 receive:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});

boolean isPublishOk = configService.publishConfig(dataId, group, "我是新配置内容~");
System.out.println(isPublishOk);

Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
}

结果和预想的一样,当向服务端publishConfig数据变化后,客户端可以立即感知,愣是用主动拉pull模式做出了服务端实时推送的效果。

1
2
3
java复制代码数据变更 receive:我是新配置内容~
true
我是新配置内容~

服务端源码分析

Nacos配置中心的服务端源码主要在nacos-config项目的ConfigController类,服务端的逻辑要比客户端稍复杂一些,这里我们重点看下。

处理长轮询

服务端对外提供的监听接口地址/v1/cs/configs/listener,这个方法内容不多,顺着doPollingConfig往下看。

服务端根据请求header中的Long-Pulling-Timeout属性来区分请求是长轮询还是短轮询,这里咱们只关注长轮询部分,接着看LongPollingService(记住这个service很关键)类中的addLongPollingClient()方法是如何处理客户端的长轮询请求的。

正常客户端默认设置的请求超时时间是30s,但这里我们发现服务端“偷偷”的给减掉了500ms,现在超时时间只剩下了29.5s,那为什么要这样做呢?

用官方的解释之所以要提前500ms响应请求,为了最大程度上保证客户端不会因为网络延时造成超时,考虑到请求可能在负载均衡时会耗费一些时间,毕竟Nacos最初就是按照阿里自身业务体量设计的嘛!

此时对客户端提交上来的groupkey的MD5与服务端当前的MD5比对,如md5值不同,则说明服务端的配置项发生过变更,直接将该groupkey放入changedGroupKeys集合并返回给客户端。

1
Java复制代码MD5Util.compareMd5(req, rsp, clientMd5Map)

如未发生变更,则将客户端请求挂起,这个过程先创建一个名为ClientLongPolling的调度任务Runnable,并提交给scheduler定时线程池延后29.5s执行。

1
2
Java复制代码ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

这里每个长轮询任务携带了一个asyncContext对象,使得每个请求可以延迟响应,等延时到达或者配置有变更之后,调用asyncContext.complete()响应完成。

asyncContext 为 Servlet 3.0新增的特性,异步处理,使Servlet线程不再需要一直阻塞,等待业务处理完毕才输出响应;可以先释放容器分配给请求的线程与相关资源,减轻系统负担,其响应将被延后,在处理完业务或者运算后再对客户端进行响应。

ClientLongPolling任务被提交进入延迟线程池执行的同时,服务端会通过一个allSubs队列保存所有正在被挂起的客户端长轮询请求任务,这个是客户端注册监听的过程。

如延时期间客户端据数一直未变化,延时时间到达后将本次长轮询任务从allSubs队列剔除,并响应请求response,这是取消监听。收到响应后客户端再次发起长轮询,循环往复。

处理长轮询

到这我们知道服务端是如何挂起客户端长轮询请求的,一旦请求在挂起期间,用户通过管理平台操作了配置项,或者服务端收到了来自其他客户端节点修改配置的请求。

怎么能让对应已挂起的任务立即取消,并且及时通知客户端数据发生了变更呢?

数据变更

管理平台或者客户端更改配置项接位置ConfigController中的publishConfig方法。

值得注意得是,在publishConfig接口中有这么一段逻辑,某个dataId配置数据被修改时会触发一个数据变更事件Event。

1
java复制代码ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));

仔细看LongPollingService会发现在它的构造方法中,正好订阅了数据变更事件,并在事件触发时执行一个数据变更调度任务DataChangeTask。

订阅数据变更事件

DataChangeTask内的主要逻辑就是遍历allSubs队列,上边我们知道,这个队列中维护的是所有客户端的长轮询请求任务,从这些任务中找到包含当前发生变更的groupkey的ClientLongPolling任务,以此实现数据更变推送给客户端,并从allSubs队列中剔除此长轮询任务。

DataChangeTask

而我们在看给客户端响应response时,调用asyncContext.complete()结束了异步请求。

结束语

上边只揭开了nacos配置中心的冰山一角,实际上还有非常多重要的技术细节都没提及到,建议大家没事看看源码,源码不需要通篇的看,只要抓住核心部分就够了。就比如今天这个题目以前我真没太在意,突然被问一下子吃不准了,果断看下源码,而且这样记忆比较深刻(别人嚼碎了喂你的知识总是比自己咀嚼的差那么点意思)。

nacos的源码我个人觉得还是比较朴素的,代码并没有过多炫技,看起来相对轻松。大家不要对看源码有什么抵触,它也不过是别人写的业务代码而已,just so so!


我是小富~,如果对你有用在看、关注支持下,咱们下期见~

整理了几百本各类技术电子书,有需要的同学自取。技术群快满了,想进的同学可以加我好友,和大佬们一起吹吹技术。

电子书地址

个人公众号: 程序员内点事,欢迎交流

本文转载自: 掘金

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

基于阿里网盘api二次开发实现类似onedrive双端同步功

发表于 2021-07-05

基于阿里网盘api二次开发实现类似onedrive双端同步功能

为啥选择阿里网盘二次开发?

  • 百度网盘横行网速慢的要死,仗着手持大家资源无情限速,不适合做同步盘,之前百度出了一款不限速的图片,视频同步软件,但是由于格式的限制也不想做。
  • 阿里网盘并没有过多限制,由于之前的各种活动不少同学的网盘大小可以到达1t,对比国外网盘如onedrive,google网盘,都可以通过特定的软件实现本地和远程的文件同步但是毕竟某些原因也不能愉快的使用,阿里就做到了容量大且速度快的特点非常适合做同步盘

怎么下载?

只需要到git clone下来即可 gitee.com/yxhpy/sync-…

怎么使用?

按照gitee文档使用即可,预先在网盘root目录创建一个文件夹,在本地准备一个文件夹,改写python项目中的配置文件

效果如何?

现在版本相当于只是能用的地步能实现慢同步,也就是放在那能慢慢的同步到一致,能实现多级文件夹的同步,但是无法实时监测文件的改动上传,这才是接下来我要挑战的东西,但是免费的何乐而不为呢?对吧

速度如何?

基于阿里网盘的秒传,部分知名软件或文件如果达到hash一致,能非常快速的同步到网盘上去,下载单线程下载后续优化,上传也是单线程上传后续优化,能用!!

后续?

我不清楚是写web客户端管理还是就写python这样运行,这个项目说小往后也不小,说大现在就是个幼儿园吗,我也是初出茅庐按照脑子里的想法慢慢写吧!!

本文转载自: 掘金

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

Springboot+mybatis-plus+注解 实现数

发表于 2021-07-05

ps:新人报道,第一次写笔记,技术比较渣,大佬们勿喷,代码参考了很多大佬的文章,也是找不到原文地址了,如有问题,联系删除

1.创建注解

当此注解打在类上,不需要传参,该类下所有查询接口开启数据隔离;打在方法上默认开启数据隔离,传参为false则该方法关闭验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
/**
* 数据权限验证注解
* @author xiaohua
* @date 2021/6/23
*/
@Documented
@Target({METHOD, ANNOTATION_TYPE, TYPE})
@Retention(RUNTIME)
public @interface DataPermission {
/**
* 是否要进行数据权限隔离
*/
boolean isPermi() default true;
}

2. 具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
java复制代码@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataPermissionInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(DataPermissionInterceptor.class);

@Autowired
private TokenService tokenService;

//扫描的包路径(根据自己的项目路径来),这里是取的配置里的包路径
@Value("${permission.package-path}")
private String packagePath;

private final static String DEPT_ID = "dept_id";

private final static String USER_ID = "create_user";

private static List<String> classNames;

@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
LoginInfo user = tokenService.getLoginInfo();
if (user == null){
return invocation.proceed();
}

List<Long> deptIds = (List<Long>) Convert.toList(user.getDataScope());
if (deptIds == null){
deptIds = new ArrayList<>();
}
//反射扫包会比较慢,这里做了个懒加载
if (classNames == null) {
synchronized (LazyInit.class){
if (classNames == null){
//扫描指定包路径下所有包含指定注解的类
Set<Class<?>> classSet = ClassUtil.scanPackageByAnnotation(packagePath, DataPermission.class);
if (classSet == null && classSet.size() == 0){
classNames = new ArrayList<>();
} else {
//取得类全名
classNames = classSet.stream().map(Class::getName).collect(Collectors.toList());
}
}
}
}

// 拿到mybatis的一些对象
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

// mappedStatement.getId()为执行的mapper方法的全路径名,newId为执行的mapper方法的类全名
String newId = mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."));
// 如果不是指定的方法,直接结束拦截
if (!classNames.contains(newId)) {
return invocation.proceed();
}
String newName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length());
//是否开启数据权限
boolean isPermi = true;
Class<?> clazz = Class.forName(newId);
//遍历方法
for (Method method : clazz.getDeclaredMethods()) {
//方法是否含有DataPermission注解,如果含有注解则将数据结果过滤
if (method.isAnnotationPresent(DataPermission.class) && newName.equals(method.getName())) {
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
if (dataPermission != null) {
//不验证
if (!dataPermission.isPermi()) {
isPermi = false;
} else { //开启验证
isPermi = true;
}
}
}
}

if (isPermi){
// 获取到原始sql语句
String sql = statementHandler.getBoundSql().getSql();

// 解析并返回新的SQL语句,只处理查询sql
if (mappedStatement.getSqlCommandType().toString().equals("SELECT")) {
// String newSql = getNewSql(sql,deptIds,user.getUserId());
sql = getSql(sql,deptIds,user.getUserId());
}
// 修改sql
metaObject.setValue("delegate.boundSql.sql", sql);
}
return invocation.proceed();
} catch (Exception e){
logger.error("数据权限隔离异常:", e);
return invocation.proceed();
}

}


/**
* 解析SQL语句,并返回新的SQL语句
* 注意,该方法使用了JSqlParser来操作SQL,该依赖包Mybatis-plus已经集成了。如果要单独使用,请先自行导入依赖
*
* @param sql 原SQL
* @return 新SQL
*/
private String getSql(String sql,List<Long> deptIds,Long userId) {

try {
String condition = "";
String permissionSql = "(";
if (deptIds.size() > 0){
for (Long deptId : deptIds) {
if ("(".equals(permissionSql)){
permissionSql = permissionSql + deptId;
} else {
permissionSql = permissionSql + "," + deptId;
}
}
permissionSql = permissionSql + ")";
// 修改原语句
condition = DEPT_ID +" in " + permissionSql;
} else {
condition = USER_ID +" = " + userId;
}

if (StringUtils.isBlank(condition)){
return sql;
}
Select select = (Select)CCJSqlParserUtil.parse(sql);
PlainSelect plainSelect = (PlainSelect)select.getSelectBody();
//取得原SQL的where条件
final Expression expression = plainSelect.getWhere();
//增加新的where条件
final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);
if (expression == null) {
plainSelect.setWhere(envCondition);
} else {
AndExpression andExpression = new AndExpression(expression, envCondition);
plainSelect.setWhere(andExpression);
}
return plainSelect.toString();
} catch (JSQLParserException e) {
logger.error("解析原SQL并构建新SQL错误:" + e);
return sql;
}
}

本文转载自: 掘金

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

一口气说出9种分布式ID生成方式,面试官有点懵 送大家以下j

发表于 2021-07-05

一、为什么要用分布式ID?

在说分布式ID的具体实现之前,我们来简单分析一下为什么用分布式ID?分布式ID应该满足哪些特征?

1、什么是分布式ID?

拿MySQL数据库举个栗子:

在我们业务数据量不大的时候,单库单表完全可以支撑现有业务,数据再大一点搞个MySQL主从同步读写分离也能对付。

但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。那么这个全局唯一ID就叫分布式ID。

2、那么分布式ID需要满足那些条件?
  • 全局唯一:必须保证ID是全局性唯一的,基本要求
  • 高性能:高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈
  • 高可用:100%的可用性是骗人的,但是也要无限接近于100%的可用性
  • 好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单
  • 趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求

二、 分布式ID都有哪些生成方式?

今天主要分析一下以下9种,分布式ID生成器方式以及优缺点:

  • UUID
  • 数据库自增ID
  • 数据库多主模式
  • 号段模式
  • Redis
  • 雪花算法(SnowFlake)
  • 滴滴出品(TinyID)
  • 百度 (Uidgenerator)
  • 美团(Leaf)

那么它们都是如何实现?以及各自有什么优缺点?我们往下看

图片

图片源自网络

以上图片源自网络,如有侵权联系删除

1、基于UUID

在Java的世界里,想要得到一个具有唯一性的ID,首先被想到可能就是UUID,毕竟它有着全球唯一的特性。那么UUID可以做分布式ID吗?答案是可以的,但是并不推荐!

1
2
3
4
5
6
typescript复制代码

public static void main(String\[\] args) { 
       String uuid = UUID.randomUUID().toString().replaceAll("-","");
       System.out.println(uuid);
 }

UUID的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718,但UUID却并不适用于实际的业务需求。像用作订单号UUID这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID。

优点:

  • 生成足够简单,本地生成无网络消耗,具有唯一性

缺点:

  • 无序的字符串,不具备趋势自增特性
  • 没有具体的业务含义
  • 长度过长16 字节128位,36位长度的字符串,存储以及查询对MySQL的性能消耗较大,MySQL官方明确建议主键要尽量越短越好,作为数据库主键 UUID 的无序性会导致数据位置频繁变动,严重影响性能。
2、基于数据库自增ID

基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:

1
2
3
4
5
6
7
8
sql复制代码

CREATE DATABASE \`SEQ\_ID\`;
CREATE TABLE SEQID.SEQUENCE\_ID (
    id bigint(20) unsigned NOT NULL auto\_increment, 
    value char(10) NOT NULL default '',
    PRIMARY KEY (id),
) ENGINE=MyISAM;
1
2
sql复制代码
insert into SEQUENCE\_ID(value)  VALUES ('values');

当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

优点:

  • 实现简单,ID单调自增,数值类型查询速度快

缺点:

  • DB单点存在宕机风险,无法扛住高并发场景
3、基于数据库集群模式

前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。

那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?

解决方案:设置起始值和自增步长

MySQL_1 配置:

1
2
ini复制代码set @@auto\_increment\_offset = 1;     -- 起始值  
set @@auto\_increment\_increment = 2;  -- 步长

MySQL_2 配置:

1
2
ini复制代码set @@auto\_increment\_offset = 2;     -- 起始值  
set @@auto\_increment\_increment = 2;  -- 步长

这样两个MySQL实例的自增ID分别就是:

1、3、5、7、9

2、4、6、8、10

那如果集群后的性能还是扛不住高并发咋办?就要进行MySQL扩容增加节点,这是一个比较麻烦的事。

图片

在这里插入图片描述

从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点压力的问题,同时为了ID生成特性,将自增步长按照机器数量来设置。

增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些,但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例的起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改。

优点:

  • 解决DB单点问题

缺点:

  • 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
4、基于数据库的号段模式

号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE id\_generator (  
  id int(10) NOT NULL,
  max\_id bigint(20) NOT NULL COMMENT '当前最大id',
  step int(20) NOT NULL COMMENT '号段的布长',
  biz\_type    int(20) NOT NULL COMMENT '业务类型',
  version int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (\`id\`)
)

biz_type :代表不同业务类型

max_id :当前最大的可用id

step :代表号段的长度

version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]。

1
python复制代码update id\_generator set max\_id = #{max\_id+step}, version = version + 1 where version = # {version} and biz\_type = XXX

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

5、基于Redis模式

Redis也同样可以实现,原理就是利用redis的 incr命令实现ID的原子性自增。

1
2
3
4
bash复制代码127.0.0.1:6379\> set seq\_id 1     // 初始化自增ID为1  
OK
127.0.0.1:6379\> incr seq\_id      // 增加1,并返回递增后的数值
(integer) 2

用redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
  • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。
6、基于雪花算法(Snowflake)模式

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。

图片

在这里插入图片描述

以上图片源自网络,如有侵权联系删除

Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。

Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。

  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。
  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID

根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。

Java版本的Snowflake算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
go复制代码`/**`
`* Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数,然后转化为62进制变成一个短地址URL`
`*`
`* https://github.com/beyondfengyu/SnowFlake`
`*/`
`public class SnowFlakeShortUrl {`
`/**`
`* 起始的时间戳`
`*/`
`private final static long START_TIMESTAMP = 1480166465631L;`
`/**`
`* 每一部分占用的位数`
`*/`
`private final static long SEQUENCE_BIT = 12;   //序列号占用的位数`
`private final static long MACHINE_BIT = 5;     //机器标识占用的位数`
`private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数`
`/**`
`* 每一部分的最大值`
`*/`
`private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);`
`private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);`
`private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);`
`/**`
`* 每一部分向左的位移`
`*/`
`private final static long MACHINE_LEFT = SEQUENCE_BIT;`
`private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;`
`private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;`
`private long dataCenterId;  //数据中心`
`private long machineId;     //机器标识`
`private long sequence = 0L; //序列号`
`private long lastTimeStamp = -1L;  //上一次时间戳`
`private long getNextMill() {`
`long mill = getNewTimeStamp();`
`while (mill <= lastTimeStamp) {`
`mill = getNewTimeStamp();`
`}`
`return mill;`
`}`
`private long getNewTimeStamp() {`
`return System.currentTimeMillis();`
`}`
`/**`
`* 根据指定的数据中心ID和机器标志ID生成指定的序列号`
`*`
`* @param dataCenterId 数据中心ID`
`* @param machineId    机器标志ID`
`*/`
`public SnowFlakeShortUrl(long dataCenterId, long machineId) {`
`if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {`
`throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");`
`}`
`if (machineId > MAX_MACHINE_NUM || machineId < 0) {`
`throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");`
`}`
`this.dataCenterId = dataCenterId;`
`this.machineId = machineId;`
`}`
`/**`
`* 产生下一个ID`
`*`
`* @return`
`*/`
`public synchronized long nextId() {`
`long currTimeStamp = getNewTimeStamp();`
`if (currTimeStamp < lastTimeStamp) {`
`throw new RuntimeException("Clock moved backwards.  Refusing to generate id");`
`}`
`if (currTimeStamp == lastTimeStamp) {`
`//相同毫秒内,序列号自增`
`sequence = (sequence + 1) & MAX_SEQUENCE;`
`//同一毫秒的序列数已经达到最大`
`if (sequence == 0L) {`
`currTimeStamp = getNextMill();`
`}`
`} else {`
`//不同毫秒内,序列号置为0`
`sequence = 0L;`
`}`
`lastTimeStamp = currTimeStamp;`
`return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分`
`| dataCenterId << DATA_CENTER_LEFT       //数据中心部分`
`| machineId << MACHINE_LEFT             //机器标识部分`
`| sequence;                             //序列号部分`
`}`
`public static void main(String[] args) {`
`SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);`
`for (int i = 0; i < (1 << 4); i++) {`
`//10进制`
`System.out.println(snowFlake.nextId());`
`}`
`}`
`}`
7、百度(uid-generator)

uid-generator是由百度技术部开发,项目GitHub地址 github.com/baidu/uid-g…

uid-generator是基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。

对于uid-generator ID组成结构:

workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,而且同一应用每次重启就会消费一个workId。

参考文献

github.com/baidu/uid-g…

8、美团(Leaf)

Leaf由美团开发,github地址:github.com/Meituan-Dia…

Leaf同时支持号段模式和snowflake算法模式,可以切换使用。

号段模式

先导入源码 github.com/Meituan-Dia… ,在建一张表leaf_alloc

1
2
3
4
5
6
7
8
9
10
sql复制代码DROP TABLE IF EXISTS \`leaf\_alloc\`;  

CREATE TABLE \`leaf\_alloc\` (
  \`biz\_tag\` varchar(128)  NOT NULL DEFAULT '' COMMENT '业务key',
  \`max\_id\` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',
  \`step\` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',
  \`description\` varchar(256)  DEFAULT NULL COMMENT '业务key的描述',
  \`update\_time\` timestamp NOT NULL DEFAULT CURRENT\_TIMESTAMP ON UPDATE CURRENT\_TIMESTAMP COMMENT '数据库维护的更新时间',
  PRIMARY KEY (\`biz\_tag\`)
) ENGINE=InnoDB;

然后在项目中开启号段模式,配置对应的数据库信息,并关闭snowflake模式

1
2
3
4
5
6
7
8
9
ini复制代码leaf.name=com.sankuai.leaf.opensource.test  
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf\_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=root

leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=

启动leaf-server 模块的 LeafServerApplication项目就跑起来了

号段模式获取分布式自增ID的测试url :http://localhost:8080/api/segment/get/leaf-segment-test

监控号段模式:http://localhost:8080/cache

snowflake模式

Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。

1
2
3
4
ini复制代码
leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1
leaf.snowflake.port=2181

snowflake模式获取分布式自增ID的测试url:http://localhost:8080/api/snowflake/get/test

9、滴滴(Tinyid)

Tinyid由滴滴开发,Github地址:github.com/didi/tinyid…

Tinyid是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

图片

在这里插入图片描述

Tinyid提供http和tinyid-client两种方式接入

Http方式接入

(1)导入Tinyid源码:

git clone github.com/didi/tinyid…

(2)创建数据表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
r复制代码CREATE TABLE \`tiny\_id\_info\` (  
  \`id\` bigint(20) unsigned NOT NULL AUTO\_INCREMENT COMMENT '自增主键',
  \`biz\_type\` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
  \`begin\_id\` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin\_id和max\_id应相同',
  \`max\_id\` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
  \`step\` int(11) DEFAULT '0' COMMENT '步长',
  \`delta\` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
  \`remainder\` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
  \`create\_time\` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  \`update\_time\` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  \`version\` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (\`id\`),
  UNIQUE KEY \`uniq\_biz\_type\` (\`biz\_type\`)
) ENGINE=InnoDB AUTO\_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';

CREATE TABLE \`tiny\_id\_token\` (
  \`id\` int(11) unsigned NOT NULL AUTO\_INCREMENT COMMENT '自增id',
  \`token\` varchar(255) NOT NULL DEFAULT '' COMMENT 'token',
  \`biz\_type\` varchar(63) NOT NULL DEFAULT '' COMMENT '此token可访问的业务类型标识',
  \`remark\` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  \`create\_time\` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
  \`update\_time\` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO\_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'token信息表';

INSERT INTO \`tiny\_id\_info\` (\`id\`, \`biz\_type\`, \`begin\_id\`, \`max\_id\`, \`step\`, \`delta\`, \`remainder\`, \`create\_time\`, \`update\_time\`, \`version\`)
VALUES
    (1, 'test', 1, 1, 100000, 1, 0, '2018-07-21 23:52:58', '2018-07-22 23:19:27', 1);

INSERT INTO \`tiny\_id\_info\` (\`id\`, \`biz\_type\`, \`begin\_id\`, \`max\_id\`, \`step\`, \`delta\`, \`remainder\`, \`create\_time\`, \`update\_time\`, \`version\`)
VALUES
    (2, 'test\_odd', 1, 1, 100000, 2, 1, '2018-07-21 23:52:58', '2018-07-23 00:39:24', 3);


INSERT INTO \`tiny\_id\_token\` (\`id\`, \`token\`, \`biz\_type\`, \`remark\`, \`create\_time\`, \`update\_time\`)
VALUES
    (1, '0f673adf80504e2eaa552f5d791b644c', 'test', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

INSERT INTO \`tiny\_id\_token\` (\`id\`, \`token\`, \`biz\_type\`, \`remark\`, \`create\_time\`, \`update\_time\`)
VALUES
    (2, '0f673adf80504e2eaa552f5d791b644c', 'test\_odd', '1', '2017-12-14 16:36:46', '2017-12-14 16:36:48');

(3)配置数据库:

1
2
3
4
5
ini复制代码datasource.tinyid.names=primary  
datasource.tinyid.primary.driver-class\-name\=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456

(4)启动tinyid-server后测试

1
2
3
4
5
6
makefile复制代码获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c'  
返回结果: 3

批量获取分布式自增ID:
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=0f673adf80504e2eaa552f5d791b644c&batchSize=10'
返回结果:  4,5,6,7,8,9,10,11,12,13
Java客户端方式接入

重复Http方式的(2)(3)操作

引入依赖

1
2
3
4
5
bash复制代码       <dependency>  
            <groupId\>com.xiaoju.uemc.tinyid</groupId\>
            <artifactId>tinyid-client</artifactId>
            <version>${tinyid.version}</version>
        </dependency\>

配置文件

1
2
ini复制代码tinyid.server =localhost:9999  
tinyid.token =0f673adf80504e2eaa552f5d791b644c

test 、tinyid.token是在数据库表中预先插入的数据,test 是具体业务类型,tinyid.token表示可访问的业务类型

1
2
3
4
5
ini复制代码// 获取单个分布式自增ID  
Long id =  TinyId . nextId( " test " );

// 按需批量分布式自增ID
List< Long > ids =  TinyId . nextId( " test " , 10 );

总结

本文只是简单介绍一下每种分布式ID生成器,旨在给大家一个详细学习的方向,每种生成方式都有它自己的优缺点,具体如何使用还要看具体的业务需求。

END

送大家以下java学习资料,关注公众号:IT老哥,回复面试题和实战项目即可领取

面试题1.png

面试题2.png

实战项目1.png

实战项目2.png

本文转载自: 掘金

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

上海某大公司:你是了解Redis对吧?

发表于 2021-07-05

《对线面试官》系列目前已经连载26篇啦!有深度风趣的系列!

  • 【对线面试官】Java注解
  • 【对线面试官】Java泛型
  • 【对线面试官】 Java NIO
  • 【对线面试官】Java反射 && 动态代理
  • 【对线面试官】多线程基础
  • 【对线面试官】 CAS
  • 【对线面试官】synchronized
  • 【对线面试官】AQS&&ReentrantLock
  • 【对线面试官】线程池
  • 【对线面试官】ThreadLocal
  • 【对线面试官】CountDownLatch和CyclicBarrier
  • 【对线面试官】为什么需要Java内存模型?
  • 【对线面试官】Java从编译到执行,发生了什么?
  • 【对线面试官】双亲委派机制
  • 【对线面试官】List
  • 【对线面试官】Map
  • 【对线面试官】SpringMVC
  • 【对线面试官】Spring基础
  • 【对线面试官】SpringBean生命周期
  • 【对线面试官】Redis基础
  • 【对线面试官】Redis持久化
  • 【对线面试官】Kafka基础
  • 【对线面试官】使用Kafka会考虑什么问题?
  • 【对线面试官】MySQL索引
  • 【对线面试官】MySQL 事务&&锁机制&&MVCC
  • 【对线面试官】MySQL调优

文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的。

鉴于很多同学反馈没看懂【对线面试官】系列,基础相关的知识我确实写过文章讲解过啦,但有的同学就是不爱去翻。

为了让大家有更好的体验,我把基础文章也找出来(重要的知识点我还整理过电子书,比如说像多线程、集合、Spring这种面试必考的早就已经转成PDF格式啦)

我把这些上传到网盘,你们有需要直接下载就好了。做到这份上了,不会还想白嫖吧?点赞和转发又不用钱。

链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom

欢迎关注我的微信公众号【Java3y】来聊聊Java面试

本文转载自: 掘金

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

30 个 Python 教程和技巧

发表于 2021-07-05
  • 💂 个人网站:【 海拥】【摸鱼小游戏】
  • 🤟 风趣幽默的人工智能学习网站:👉人工智能
  • 💅 想寻找共同学习交流的小伙伴,请点击【全栈技术交流群】
  • 💬 免费且实用的计算机相关知识题库:👉进来逛逛

给大家安利一个免费且实用的 Python 刷题(面经大全)网站,👉点击跳转到网站。

如果您让任何 Python 程序员讲述 Python 的优势,他会引用简洁和高可读性作为最有影响力的优势。在本 Python 教程中,我们将介绍许多基本的 Python 教程和技巧,这些技巧和技巧将验证上述两点。

自从我开始使用 Python 以来,我一直在收集这些有用的快捷方式。还有什么事比分享我们所知道的并且可以使他人受益的东西更加有意义?

所以今天,我带来了一些基本的 Python 教程和技巧。所有这些技巧都可以帮助您减少代码并优化执行。此外,您可以在处理常规任务时轻松地在实时项目中使用它们。

目录

1.就地交换两个数字2.比较运算符的链接3.使用三元运算符进行条件赋值。4.使用多行字符串。5.将列表元素存储到新变量中。6.打印导入模块的文件路径。7.使用交互式“_”运算符。8.字典/集合理解。9.调试脚本。10.设置文件共享。11.在 Python 中检查对象。12.简化 if 语句。13.在运行时检测 Python 版本。14.组合多个字符串。15.反转 string/list 的四种方法。16.玩枚举。17.在 Python 中使用枚举。18.从函数返回多个值。19.使用 splat 运算符解包函数参数。20.使用字典来存储 switch。21.计算一行中任意数字的阶乘。22.查找列表中出现频率最高的值。23.重置递归限制。24.检查对象的内存使用情况。25.使用 slots 减少内存开销。26.Lambda 模仿打印功能。27.从两个相关序列创建字典。28.在线搜索字符串中的多个前缀。29.形成一个统一的列表,不使用任何循环。30.在 Python 中实现真正的 switch-case 语句。总结——Python 提示和技巧

技巧1 就地交换两个数字

Python 提供了一种在一行中进行赋值和交换的直观方式。请参考下面的例子。

1
2
3
4
5
6
7
8
python复制代码x, y = 10, 20
print(x, y)

x, y = y, x
print(x, y)

#1 (10, 20)
#2 (20, 10)

右边的赋值为一个新的元组播种。而左边的立即将那个(未引用的)元组解包到名称 <a> 和 <b>。

分配完成后,新元组将被取消引用并标记为垃圾收集。变量的交换也发生在最终。回到目录


技巧2 比较运算符的链接。

比较运算符的聚合是另一个有时可以派上用场的技巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码n = 10 
result = 1 < n < 20
print(result)

# True

result = 1 > n <= 9
print(result)

# False

```[回到目录](#mulu)

---


技巧3 使用三元运算符进行条件赋值。
------------------


三元运算符是 if-else 语句的快捷方式,也称为条件运算符。

python复制代码[on_true] if [expression] else [on_false]

1
2
3
4
5

以下是一些示例,您可以使用它们使代码紧凑简洁。


下面的语句与它的意思相同,即“如果 y 为 9,则将 10 分配给 x,否则将 20 分配给 x ”。如果需要,我们可以扩展运算符的链接。

python复制代码x = 10 if (y == 9) else 20

1
2

同样,我们可以对类对象做同样的事情。

python复制代码x = (classA if y == 1 else classB)(param1, param2)

1
2
3
4
5

在上面的例子中,classA 和 classB 是两个类,其中一个类构造函数将被调用。


下面是一个没有的例子。加入评估最小数字的条件。

python复制代码def small(a, b, c):
return a if a <= b and a <= c else (b if b <= a and b <= c else c)

print(small(1, 0, 1))
print(small(1, 2, 2))
print(small(2, 2, 3))
print(small(5, 4, 3))

#Output
#0 #1 #2 #3

1
2

我们甚至可以在列表推导式中使用三元运算符。

python复制代码[m2 if m > 10 else m4 for m in range(50)]

#=> [0, 1, 16, 81, 256, 625, 1296, 2401, 4096, 6561, 10000, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401]

1
2
3
4
5
6
7
8
9

---


技巧4 使用多行字符串
-----------


基本方法是使用从 C 语言派生的反斜杠。

python复制代码multiStr = “select * from multi_row
where row_id < 5”
print(multiStr)

select * from multi_row where row_id < 5

1
2

另一个技巧是使用三引号。

python复制代码multiStr = “””select * from multi_row
where row_id < 5”””
print(multiStr)

#select * from multi_row
#where row_id < 5

1
2
3
4
5

上述方法的共同问题是缺乏适当的缩进。如果我们尝试缩进,它会在字符串中插入空格。


所以最终的解决方案是将字符串拆分成多行,并将整个字符串括在括号中。

python复制代码multiStr= (“select * from multi_row “
“where row_id < 5 “
“order by age”)
print(multiStr)

#select * from multi_row where row_id < 5 order by age

1
2
3
4
5
6
7
8
9

---


技巧5 将列表元素存储到新变量中
----------------


我们可以使用一个列表来初始化一个 no。的变量。在解压列表时,变量的数量不应超过编号。列表中的元素。

python复制代码testList = [1,2,3]
x, y, z = testList

print(x, y, z)

#-> 1 2 3

1
2
3
4
5
6
7
8
9

---


技巧6 打印导入模块的文件路径
---------------


如果您想知道代码中导入的模块的绝对位置,请使用以下技巧。

python复制代码import threading
import socket

print(threading)
print(socket)

#1- <module ‘threading’ from ‘/usr/lib/python2.7/threading.py’>
#2- <module ‘socket’ from ‘/usr/lib/python2.7/socket.py’>

1
2
3
4
5
6
7
8
9
10
11
12

---


技巧7 使用交互式“\_”运算符
----------------


这是一个有用的功能,我们很多人都不知道。


在 Python 控制台中,每当我们测试表达式或调用函数时,结果都会发送到临时名称 \_(下划线)。

python复制代码>>> 2 + 1
3

_
3
print _
3

1
2
3
4
5
6
7
8
9
10
11

“\_”引用上次执行的表达式的输出。[回到目录](#mulu)

---


技巧8 字典/集合理解
-----------


就像我们使用列表推导一样,我们也可以使用字典/集合推导。它们易于使用且同样有效。这是一个例子。

python复制代码testDict = {i: i * i for i in xrange(10)}
testSet = {i * 2 for i in xrange(10)}

print(testSet)
print(testDict)

#set([0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
#{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

1
2
3
4
5
6
7
8
9
10
11

注意 -两个语句中只有 <:> 的区别。此外,要在 Python3 中运行上述代码,请将 替换为 。[回到目录](#mulu)

---


技巧9 调试脚本
--------


我们可以在 模块的帮助下在 Python 脚本中设置断点。请按照以下示例进行操作。

python复制代码import pdb
pdb.set_trace()

1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们可以在脚本的任何地方指定 <pdb.set\_trace()> 并在那里设置断点。这是非常方便的。[回到目录](#mulu)

---


技巧10 设置文件共享
-----------


Python 允许运行 HTTP 服务器,您可以使用它从服务器根目录共享文件。下面是启动服务器的命令。


### Python 2

python复制代码python -m SimpleHTTPServer

1
2

### Python 3

python复制代码python3 -m http.server

1
2
3
4
5
6
7
8
9
10
11

以上命令将在默认端口 8000 上启动服务器。您还可以通过将自定义端口作为最后一个参数传递给上述命令来使用自定义端口。[回到目录](#mulu)

---


技巧11 在 Python 中检查对象
-------------------


我们可以通过调用 dir() 方法来检查 Python 中的对象。这是一个简单的例子。

python复制代码test = [1, 3, 5, 7]
print( dir(test) )

1
2


python复制代码[‘add‘, ‘class‘, ‘contains‘, ‘delattr‘, ‘delitem‘, ‘delslice‘, ‘doc‘, ‘eq‘, ‘format‘, ‘ge‘, ‘getattribute‘, ‘getitem‘, ‘getslice‘, ‘gt‘, ‘hash‘, ‘iadd‘, ‘imul‘, ‘init‘, ‘iter‘, ‘le‘, ‘len‘, ‘lt‘, ‘mul‘, ‘ne‘, ‘new‘, ‘reduce‘, ‘reduce_ex‘, ‘repr‘, ‘reversed‘, ‘rmul‘, ‘setattr‘, ‘setitem‘, ‘setslice‘, ‘sizeof‘, ‘str‘, ‘subclasshook‘, ‘append’, ‘count’, ‘extend’, ‘index’, ‘insert’, ‘pop’, ‘remove’, ‘reverse’, ‘sort’]

1
2
3
4
5
6
7
8
9

---


技巧12 简化 if 语句
-------------


要验证多个值,我们可以通过以下方式进行。

python复制代码if m in [1,3,5,7]:

1
2

代替:

python复制代码if m==1 or m==3 or m==5 or m==7:

1
2
3
4
5
6
7
8
9
10
11

或者,我们可以使用 '{1,3,5,7}' 而不是 '[1,3,5,7]' 作为 'in' 运算符,因为 'set' 可以通过 O(1) 访问每个元素。[回到目录](#mulu)

---


技巧13 在运行时检测 Python 版本
---------------------


有时,如果当前运行的 Python 引擎低于支持的版本,我们可能不想执行我们的程序。为此,您可以使用以下代码片段。它还以可读格式打印当前使用的 Python 版本。

python复制代码import sys

#Detect the Python version currently in use.
if not hasattr(sys, “hexversion”) or sys.hexversion != 50660080:
print(“Sorry, you aren’t running on Python 3.5\n”)
print(“Please upgrade to 3.5.\n”)
sys.exit(1)

#Print Python version in a readable format.
print(“Current Python version: “, sys.version)

1
2
3
4
5

或者,您可以在上面的代码中使用sys.version\_info >= (3, 5)替换sys.hexversion!= 50660080。这是一位知情读者的建议。


在 Python 2.7 上运行时的输出。

python复制代码Python 2.7.10 (default, Jul 14 2015, 19:46:27)
[GCC 4.8.2] on linux

Sorry, you aren’t running on Python 3.5

Please upgrade to 3.5.

1
2

在 Python 3.5 上运行时的输出。

python复制代码Python 3.5.1 (default, Dec 2015, 13:05:11)
[GCC 4.8.2] on linux

Current Python version: 3.5.2 (default, Aug 22 2016, 21:11:05)
[GCC 5.3.0]

1
2
3
4
5
6
7
8
9

---


技巧14 组合多个字符串
------------


如果您想连接列表中所有可用的标记,请参见以下示例。

python复制代码>>> test = [‘I’, ‘Like’, ‘Python’, ‘automation’]

1
2

现在,让我们从上面给出的列表中的元素创建一个字符串。

python复制代码>>> print ‘’.join(test)

1
2
3
4
5
6
7
8
9

---


技巧15 反转 string/list 的四种方法
-------------------------


### 反转列表本身

python复制代码testList = [1, 3, 5]
testList.reverse()
print(testList)

#-> [5, 3, 1]

1
2

### 在循环中迭代时反转

python复制代码for element in reversed([1,3,5]): print(element)

#1-> 5
#2-> 3
#3-> 1

1
2

### 反转一个字符串

python复制代码”Test Python”[::-1]

1
2
3
4
5

这使输出为“nohtyP tseT”


### 使用切片反转列表

python复制代码[1, 3, 5][::-1]

1
2
3
4
5
6
7
8
9
10
11

上面的命令将输出 [5, 3, 1]。[回到目录](#mulu)

---


技巧16 玩枚举
--------


使用枚举器,在循环中很容易找到索引。

python复制代码testlist = [10, 20, 30]
for i, value in enumerate(testlist):
print(i, ‘: ‘, value)

#1-> 0 : 10
#2-> 1 : 20
#3-> 2 : 30

1
2
3
4
5
6
7
8
9

---


技巧17 在 Python 中使用枚举。
--------------------


我们可以使用以下方法来创建枚举定义。

python复制代码class Shapes:
Circle, Square, Triangle, Quadrangle = range(4)

print(Shapes.Circle)
print(Shapes.Square)
print(Shapes.Triangle)
print(Shapes.Quadrangle)

#1-> 0
#2-> 1
#3-> 2
#4-> 3

1
2
3
4
5
6
7
8
9
10
11
12

---


技巧18 从函数返回多个值。
--------------


支持此功能的编程语言并不多。但是,Python 中的函数确实会返回多个值。


请参考以下示例以查看它的工作情况。

python复制代码# function returning multiple values.
def x():
return 1, 2, 3, 4

Calling the above function.

a, b, c, d = x()

print(a, b, c, d)

1
2
3
4
5
6
7
8
9
10
11

#-> 1 2 3 4[回到目录](#mulu)

---


技巧19 使用 splat 运算符解包函数参数。
------------------------


splat 运算符提供了一种解压参数列表的艺术方式。为清楚起见,请参阅以下示例。

python复制代码def test(x, y, z):
print(x, y, z)

testDict = {‘x’: 1, ‘y’: 2, ‘z’: 3}
testList = [10, 20, 30]

test(testDict)
test(*
testDict)
test(*testList)

#1-> x y z
#2-> 1 2 3
#3-> 10 20 30

1
2
3
4
5
6
7
8
9

---


技巧20 使用字典来存储 switch。
--------------------


我们可以制作一个字典存储表达式。

python复制代码stdcalc = {
‘sum’: lambda x, y: x + y,
‘subtract’: lambda x, y: x - y
}

print(stdcalc‘sum’)
print(stdcalc‘subtract’)

#1-> 12
#2-> 6

1
2
3
4
5
6
7
8
9

---


技巧21 计算一行中任意数字的阶乘。
------------------


### Python 2.x.

python复制代码result = (lambda k: reduce(int.mul, range(1,k+1),1))(3)
print(result)
#-> 6

1
2

### Python 3.x.

python复制代码import functools
result = (lambda k: functools.reduce(int.mul, range(1,k+1),1))(3)
print(result)

6[回到目录](#mulu)
1
2
3
4
5
6

---


技巧22 查找列表中出现频率最高的值。
-------------------

python复制代码test = [1,2,3,4,2,2,3,1,4,4,4]
print(max(set(test), key=test.count))

#-> 4

1
2
3
4
5
6
7
8
9

---


技巧23 重置递归限制。
------------


Python 将递归限制限制为 1000。我们可以重置它的值。

python复制代码import sys

x=1001
print(sys.getrecursionlimit())

sys.setrecursionlimit(x)
print(sys.getrecursionlimit())

#1-> 1000
#2-> 1001

1
2
3
4
5
6
7
8
9
10
11
12
13
14

请仅在需要时应用上述技巧。[回到目录](#mulu)

---


技巧24 检查对象的内存使用情况。
-----------------


在 Python 2.7 中,32 位整数消耗 24 字节,而在 Python 3.5 中使用 28 字节。为了验证内存使用情况,我们可以调用 方法。


### Python 2.7.

python复制代码import sys
x=1
print(sys.getsizeof(x))

#-> 24

1
2

### Python 3.5.

python复制代码import sys
x=1
print(sys.getsizeof(x))

#-> 28

1
2
3
4
5
6
7
8
9

---


技巧25 使用 \_\_slots\_\_ 减少内存开销。
-----------------------------


你有没有观察到你的 Python 应用程序消耗了大量资源,尤其是内存?这是使用`<__slots__>`类变量在一定程度上减少内存开销的一种技巧。

python复制代码import sys
class FileSystem(object):

def __init__(self, files, folders, devices):
    self.files = files
    self.folders = folders
    self.devices = devices

print(sys.getsizeof( FileSystem ))

class FileSystem1(object):

__slots__ = ['files', 'folders', 'devices']

def __init__(self, files, folders, devices):
    self.files = files
    self.folders = folders
    self.devices = devices

print(sys.getsizeof( FileSystem1 ))

#In Python 3.5
#1-> 1016
#2-> 888

1
2
3
4
5
6
7
8

显然,您可以从结果中看到内存使用量有所节省。但是当一个类的内存开销不必要地大时,你应该使用 `__slots__` 。仅在分析应用程序后执行此操作。否则,您将使代码难以更改并且没有真正的好处。[回到目录](#mulu)

---


技巧26 Lambda 模仿打印功能。
-------------------

python复制代码import sys
lprint=lambda *args:sys.stdout.write(“ “.join(map(str,args)))
lprint(“python”, “tips”,1000,1001)

#-> python tips 1000 1001

1
2
3
4
5
6

---


技巧27 从两个相关序列创建字典。
-----------------

python复制代码t1 = (1, 2, 3)
t2 = (10, 20, 30)

print(dict (zip(t1,t2)))

#-> {1: 10, 2: 20, 3: 30}

1
2
3
4
5
6

---


技巧28 在线搜索字符串中的多个前缀。
-------------------

python复制代码print(“http://www.baidu.com".startswith((“http://“, “https://“)))
print(“https://juejin.cn".endswith((“.com”, “.cn”)))

#1-> True
#2-> True

1
2
3
4
5
6

---


技巧29 形成一个统一的列表,不使用任何循环。
-----------------------

python复制代码import itertools
test = [[-1, -2], [30, 40], [25, 35]]
print(list(itertools.chain.from_iterable(test)))

#-> [-1, -2, 30, 40, 25, 35]

1
2

如果您有一个包含嵌套列表或元组作为元素的输入列表,请使用以下技巧。但是,这里的限制是它使用了 for 循环。

python复制代码def unifylist(l_input, l_target):
for it in l_input:
if isinstance(it, list):
unifylist(it, l_target)
elif isinstance(it, tuple):
unifylist(list(it), l_target)
else:
l_target.append(it)
return l_target

test = [[-1, -2], [1,2,3, [4,(5,[6,7])]], (30, 40), [25, 35]]

print(unifylist(test,[]))

#Output => [-1, -2, 1, 2, 3, 4, 5, 6, 7, 30, 40, 25, 35]

1
2

统一包含列表和元组的列表的另一种更简单的方法是使用 Python 的 < more\_itertools > 包。它不需要循环。只需执行 < pip install more\_itertools >,如果还没有的话。

python复制代码import more_itertools

test = [[-1, -2], [1, 2, 3, [4, (5, [6, 7])]], (30, 40), [25, 35]]

print(list(more_itertools.collapse(test)))

#Output=> [-1, -2, 1, 2, 3, 4, 5, 6, 7, 30, 40, 25, 35]

1
2
3
4
5
6
7
8
9

---


技巧30 在 Python 中实现真正的 switch-case 语句。
------------------------------------


这是使用字典来模仿 switch-case 构造的代码。

python复制代码def xswitch(x):
return xswitch._system_dict.get(x, None)

xswitch._system_dict = {‘files’: 10, ‘folders’: 5, ‘devices’: 2}

print(xswitch(‘default’))
print(xswitch(‘devices’))

#1-> None
#2-> 2


---


总结——Python 提示和技巧
----------------


我们希望上面给出的基本 Python 提示和技巧将帮助您快速有效地完成任务。您可以将它们用于您的作业和项目。


我已经写了很长一段时间的技术博客,这是我的一篇技巧教程。希望你们会喜欢!这里汇总了我的全部原创及作品源码:[Github](https://github.com/wanghao221/)、[Gitee](https://gitee.com/haiyongcsdn/haiyong)


如果你真的从这篇文章中学到了一些新东西,喜欢它,收藏它并与你的小伙伴分享。🤗最后,不要忘了❤或📑支持一下哦



**本文转载自:** [掘金](https://juejin.cn/post/6981262439069253640)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

【微软算法面试高频题】可怜的小猪 1 题目 2 解析

发表于 2021-07-05

微软和谷歌的几个大佬组织了一个面试刷题群,可以加管理员VX:sxxzs3998(备注掘金),进群参与讨论和直播

  1. 题目

有 buckets 桶液体,其中正好有一桶含有毒药,其余装的都是水。它们从外观看起来都一样。为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。不幸的是,你只有 minutesToTest 分钟时间来确定哪桶液体是有毒的。

喂猪的规则如下:

  • 选择若干活猪进行喂养
  • 可以允许小猪同时饮用任意数量的桶中的水,并且该过程不需要时间。
  • 小猪喝完水后,必须有 minutesToDie 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。
  • 过了 minutesToDie 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。
  • 重复这一过程,直到时间用完。

给你桶的数目 buckets ,minutesToDie 和 minutesToTest ,返回在规定时间内判断哪个桶有毒所需的最小猪数。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码示例 1:
输入:buckets = 1000, minutesToDie = 15, minutesToTest = 60
输出:5

示例 2:
输入:buckets = 4, minutesToDie = 15, minutesToTest = 15
输出:2

示例 3:
输入:buckets = 4, minutesToDie = 15, minutesToTest = 30
输出:2
  1. 解析

举例说明:

假设:总时间 minutesToTest = 60,死亡时间 minutesToDie = 15,pow(x, y) 表示 x 的 y 次方,ceil(x)表示 x 向上取整 当前有只小猪,最多可以喝 times = minutesToTest / minutesToDie = 4 次水 最多可以喝 4 次水,能够携带 base = times + 1 = 5 个的信息量,也就是(便于理解从 0 开始): (1) 喝 0 号死去,0 号桶水有毒 (2) 喝 1 号死去,1 号桶水有毒 (3) 喝 2 号死去,2 号桶水有毒 (4) 喝 3 号死去,3 号桶水有毒 (5) 喝了上述所有水依然活蹦乱跳,4 号桶水有毒

结论是 1 只小猪最多能够验证 5 桶水中哪只水桶含有毒,当 buckets ≤ 5 时,answer = 1。那么 2 只小猪可以验证的范围最多到多少呢?我们把每只小猪携带的信息量看成是 base进制数,2 只小猪的信息量就是 pow(base, 2) = pow(5, 2) = 25,所以当 5 ≤ buckets ≤ 25时,anwser = 2。那么可以得到公式关系:pow(base, ans) ≥ buckets,取对数后即为:ans ≥ log(buckets) / log(base),因为 ans 为整数,所以 ans = ceil(log(buckets) / log(base))

image.png

那小猪到底是如何喂水的呢?下面这个动态图可以介绍整个喂水的过程:

image.png

1
2
3
4
5
6
7
c++复制代码class Solution {
public:
int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
int states = minutesToTest / minutesToDie + 1;
return ceil(log(buckets) / log(states));
}
};

微软和谷歌的几个大佬组织了一个面试刷题群,可以加管理员VX:sxxzs3998(备注掘金),进群参与讨论和直播

本文转载自: 掘金

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

1…620621622…956

开发者博客

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