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

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


  • 首页

  • 归档

  • 搜索

从头学Java Java入门及进阶(二)——运算符、循环

发表于 2021-11-04

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

感激相遇 你好 我是阿ken

特此声明 未经允许 不得转载

📚 运算符

📌算术运算符(+、-、*、/、%)

重点:++

++ 无论出现在变量前还是后,运算结束后,一定会自加1。

📌自增、自减运算符

自增自减运算符(+、-),++、–是单目运算符,可以放在操作元之前,也可以放在操作元之后。操作元必须是一个整型或浮点型变量,作用是使变量的值增1或减1。

++x和x++的不同之处在于,++x是先执行x=x+1再使用x的值,而x++是先使用x的值再执行x=x+1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码// 
int i = 10;
i++;
System.out.println(i); // 11

int k = 10;
++k;
System.out.println(k); // 11

++出现在变量前:
int i = 10;
int k = ++i;
System.out.println(); // 11
System.out.println(); // 11

++出现在变量后:
int i = 10;
int k = i++;
System.out.println(k); // 10
System.out.println(i); // 11

int i = 10;
System.out.println(i++); // 10
// 拆代码:
int temp = i++;
System.out.println(temp); // 10
System.out.println(i); // 11

int i = 10;
System.out.println(++i); // 11
// 拆代码:
int temp = ++i;
System.out.println(temp); // 11
System.out.println(i); // 11

// 原文出自: CSDN-请叫我阿ken

📌关系运算符(>、>=、<、<=、==、!=)

关系运算符结果都是布尔类型(true/false)

📌逻辑运算符(&、|、!、&&、||)

逻辑运算符要求两边都是布尔类型,并且最终结果还是布尔类型。

& 两边都是true,结果才是true

| 一边是true,结果就是true

!取反

&& 实际上和&运算结果完全相同,区别在于:&&存在短路现象。

左边为false的时候:&&短路。

左边为true的时候: || 短路。

📌赋值运算符(=、+=、-=、*=、/=、%=)

重要规则:扩展赋值运算符在使用的时候要注意,不管怎么运算,最终的运算结果类型不会变。

1
2
3
4
5
6
7
8
java复制代码byte x = 100; // byte 最大值127
x += 1000; // 编译可以通过,x变量还是byte类型,只不过损失精度了。

x += 1000; 等同于: x = (byte)(x + 1000);
int i = 10;
i += 10; // 等同于: i = i + 10; 累加。

// 原文出自: CSDN-请叫我阿ken

📌条件运算符

三目运算符语法: 布尔表达式 ? 表达式1:表达式2

布尔表达式为 true,选择表达式1作为结果。反之选择表达式2作为结果。

📌字符串连接运算符

+…

+两边都是数字,进行求和

+有一边是字符串,进行字符串的拼接

+有多个的话,遵循自左向右依次执行:1 + 2 + 3

如果想让其中某个加号先执行,可以添加小括号:1+(2+3)

注意:字符串拼接完之后的结果还是一个字符串。

技巧:怎么把一个变量塞到一个字符串当中。

1
2
3
4
java复制代码String name = "jackson";
System.out.println("登陆成功,欢迎"+name+"回来");

// 原文出自: CSDN-请叫我阿ken

📌位运算符

整形数据在内存中以二进制的形式表示,例如一个 int 型变量在内存中占4个字节共32位,int 型数据 7 的二进制表示是:

1
bash复制代码00000000 00000000 00000000 00000111

左面最高位是符号位,最高位是 0 表示正数,是 1 表示负数。负数采用补码表示,例如-8的补码表示是:

1
2
3
4
5
6
7
8
9
bash复制代码正数的补码,原码,反码都是相同的。
+8的补码,原码,反码,都是 0000 1000。

负数的补码,原码,反码都是用1放在符号位,后面7位有变化:
-8的原码:1000 1000;
-8的反码:1111 0111;
-8的补码:1111 1000。

// 原文出自: CSDN-请叫我阿ken

!!!!

📌instanceof 运算符

该运算符是二目运算符。左面的操作元是一个对象,右面是一个类。当左面的对象是右面的类或子类创建的对象时,该运算符运算的结果是 true,否则是false(有关细节后续会讲解)

📌switch 开关语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码switch(表达式){
case 常量值1;
若干个语句
break;

case 常量值2;
若干个语句
break;
......

case 常量值n;
若干个语句
break;

default;
若干语句
}

// switvh语句中表达式的值可以为byte、short、int、char型;常量值1到常量值n,也是byte、short、int、char型,而且要互不相同。

// 原文出自: CSDN-请叫我阿ken

📚 循环语句

📌while 循环

  • 其语法机制及执行原理:

while (布尔表达式) {

循环体;

}

执行原理:如果布尔表达式为true,就执行循环体,循环体结束之后,再次判断布尔表达式的结果,如果还是true,则再执行循环体,如果为false,则循环结束。

  • 案例 死循环:
1
2
3
4
5
java复制代码while(true){
System.out.println("死循环");
}

// 原文出自: CSDN-请叫我阿ken

控制语句

  1. 关于循环语句

for 循环、while 循环、do…while 循环

什么是循环语句,为什么要使用这种语句?

因为在现实世界当中,有很多事情都是需要反复/重复的去做。为了减少代码量,要使用循环语句。

要求:

第一点:必须要将语法结构背会。

第二点:必须要理解他们的执行原理。

去实现案例,去实现功能。

先从简单的案例开始,慢慢的经过一个过程你才能解决复杂的问题。

📌do…while 循环

1
2
3
4
5
6
java复制代码do {
循环体;
} while(布尔表达式);
/*最后这个分号很关键*/

// 原文出自: CSDN-请叫我阿ken

运行原理:先执行循环语句,再判断布尔表达式,如果为true就继续执行,如果为false就停止循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码int i = 0;
do{
System.out.println(i); // 0 1 2 3...8 9
i++;
// 也可直接把上两行代码等价替换为
// System.out.println(i++);
}while(i < 10);

int i = 0;
do{
System.out.println(++i); // 1 2 3 ... 8 9 10
}while(i < 10);

int k = 100;
System.out.println(++k); // 101
System.out.println(k); // 101

int m = 10;
System.out.println(m++); // 10
System.out.println(m) // 11

// 原文出自: CSDN-请叫我阿ken

转向语句:

break、continue、return(后期整理到方法时再详细学习)

📌for 循环语句

1
2
3
4
5
6
java复制代码for(表达式1; 表达式2; 表达式3) {
循环语句;
}


// 原文出自: CSDN-请叫我阿ken
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// anli
public class Example_n {
public static void main(String args[]){
long sum = 0, a = 8, item = a, n = 12, i = 1;
for(i = 1; i <= n; i++) {
sum = sum + item;
item = item*10 + a;
}
System.out.println(sum);
}
}


// 原文出自: CSDN-请叫我阿ken

if、switch 属于分支语句,属于选择语句(选择结构)。

for、while、do…while…这些都是循环语句(循环结构)。

break、continue、return 属于转向结构。

for、while完全可以互换,只不过就是语法格式不一样。

🌊 回馈粉丝

关注公众号「请叫我阿ken」 随机分享两本Java优质电子书

感谢阅读 我是阿ken

本文转载自: 掘金

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

SpringBoot实现Excel导入导出,好用到爆,POI

发表于 2021-11-04

image.png

在我们平时工作中经常会遇到要操作Excel的功能,比如导出个用户信息或者订单信息的Excel报表。你肯定听说过POI这个东西,可以实现。但是POI实现的API确实很麻烦,它需要写那种逐行解析的代码(类似Xml解析)。今天给大家推荐一款非常好用的Excel导入导出工具EasyPoi,希望对大家有所帮助!

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

EasyPoi简介

用惯了SpringBoot的朋友估计会想到,有没有什么办法可以直接定义好需要导出的数据对象,然后添加几个注解,直接自动实现Excel导入导出功能?

EasyPoi正是这么一款工具,如果你不太熟悉POI,想简单地实现Excel操作,用它就对了!

EasyPoi的目标不是替代POI,而是让一个不懂导入导出的人也能快速使用POI完成Excel的各种操作,而不是看很多API才可以完成这样的工作。

集成

在SpringBoot中集成EasyPoi非常简单,只需添加如下一个依赖即可,真正的开箱即用!

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
复制代码

使用

接下来介绍下EasyPoi的使用,以会员信息和订单信息的导入导出为例,分别实现下简单的单表导出和具有关联信息的复杂导出。

简单导出

我们以会员信息列表导出为例,使用EasyPoi来实现下导出功能,看看是不是够简单!

  • 首先创建一个会员对象Member,封装会员信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码/**
* 购物会员
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
@Excel(name = "ID", width = 10)
private Long id;
@Excel(name = "用户名", width = 20, needMerge = true)
private String username;
private String password;
@Excel(name = "昵称", width = 20, needMerge = true)
private String nickname;
@Excel(name = "出生日期", width = 20, format = "yyyy-MM-dd")
private Date birthday;
@Excel(name = "手机号", width = 20, needMerge = true, desensitizationRule = "3_4")
private String phone;
private String icon;
@Excel(name = "性别", width = 10, replace = {"男_0", "女_1"})
private Integer gender;
}
复制代码
  • 在此我们就可以看到EasyPoi的核心注解@Excel,通过在对象上添加@Excel注解,可以将对象信息直接导出到Excel中去,下面对注解中的属性做个介绍;
+ name:Excel中的列名;
+ width:指定列的宽度;
+ needMerge:是否需要纵向合并单元格;
+ format:当属性为时间类型时,设置时间的导出导出格式;
+ desensitizationRule:数据脱敏处理,`3_4`表示只显示字符串的前`3`位和后`4`位,其他为`*`号;
+ replace:对属性进行替换;
+ suffix:对数据添加后缀。
  • 接下来我们在Controller中添加一个接口,用于导出会员列表到Excel,具体代码如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
less复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation(value = "导出会员列表Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
ExportParams params = new ExportParams("会员列表", "会员列表", ExcelType.XSSF);
map.put(NormalExcelConstants.DATA_LIST, memberList);
map.put(NormalExcelConstants.CLASS, Member.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "memberList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
}
复制代码
  • LocalJsonUtil工具类,可以直接从resources目录下获取JSON数据并转化为对象,例如此处使用的members.json;

  • 运行项目,直接通过Swagger访问接口,注意在Swagger中访问接口无法直接下载,需要点击返回结果中的下载按钮才行,访问地址:http://localhost:8088/swagger-ui/

  • 下载完成后,查看下文件,一个标准的Excel文件已经被导出了。

简单导入

导入功能实现起来也非常简单,下面以会员信息列表的导入为例。

  • 在Controller中添加会员信息导入的接口,这里需要注意的是使用@RequestPart注解修饰文件上传参数,否则在Swagger中就没法显示上传按钮了;
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
less复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation("从Excel导入会员列表")
@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
@ResponseBody
public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
ImportParams params = new ImportParams();
params.setTitleRows(1);
params.setHeadRows(1);
try {
List<Member> list = ExcelImportUtil.importExcel(
file.getInputStream(),
Member.class, params);
return CommonResult.success(list);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("导入失败!");
}
}
}
复制代码
  • 然后在Swagger中测试接口,选择之前导出的Excel文件即可,导入成功后会返回解析到的数据。

复杂导出

当然EasyPoi也可以实现更加复杂的Excel操作,比如导出一个嵌套了会员信息和商品信息的订单列表,下面我们来实现下!

  • 首先添加商品对象Product,用于封装商品信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码/**
* 商品
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
@Excel(name = "ID", width = 10)
private Long id;
@Excel(name = "商品SN", width = 20)
private String productSn;
@Excel(name = "商品名称", width = 20)
private String name;
@Excel(name = "商品副标题", width = 30)
private String subTitle;
@Excel(name = "品牌名称", width = 20)
private String brandName;
@Excel(name = "商品价格", width = 10)
private BigDecimal price;
@Excel(name = "购买数量", width = 10, suffix = "件")
private Integer count;
}
复制代码
  • 然后添加订单对象Order,订单和会员是一对一关系,使用@ExcelEntity注解表示,订单和商品是一对多关系,使用@ExcelCollection注解表示,Order就是我们需要导出的嵌套订单数据;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码/**
* 订单
* Created by macro on 2021/10/12.
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
@Excel(name = "ID", width = 10,needMerge = true)
private Long id;
@Excel(name = "订单号", width = 20,needMerge = true)
private String orderSn;
@Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss",needMerge = true)
private Date createTime;
@Excel(name = "收货地址", width = 20,needMerge = true )
private String receiverAddress;
@ExcelEntity(name = "会员信息")
private Member member;
@ExcelCollection(name = "商品列表")
private List<Product> productList;
}
复制代码
  • 接下来在Controller中添加导出订单列表的接口,由于有些会员信息我们不需要导出,可以调用ExportParams中的setExclusions方法排除掉;
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
less复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation(value = "导出订单列表Excel")
@RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)
public void exportOrderList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Order> orderList = getOrderList();
ExportParams params = new ExportParams("订单列表", "订单列表", ExcelType.XSSF);
//导出时排除一些字段
params.setExclusions(new String[]{"ID", "出生日期", "性别"});
map.put(NormalExcelConstants.DATA_LIST, orderList);
map.put(NormalExcelConstants.CLASS, Order.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "orderList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
}
复制代码
  • 在Swagger中访问接口测试,导出订单列表对应Excel;

  • 下载完成后,查看下文件,EasyPoi导出复杂的Excel也是很简单的!

自定义处理

如果你想对导出字段进行一些自定义处理,EasyPoi也是支持的,比如在会员信息中,如果用户没有设置昵称,我们添加下暂未设置信息。

  • 我们需要添加一个处理器继承默认的ExcelDataHandlerDefaultImpl类,然后在exportHandler方法中实现自定义处理逻辑;
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
typescript复制代码/**
* 自定义字段处理
* Created by macro on 2021/10/13.
*/
public class MemberExcelDataHandler extends ExcelDataHandlerDefaultImpl<Member> {

@Override
public Object exportHandler(Member obj, String name, Object value) {
if("昵称".equals(name)){
String emptyValue = "暂未设置";
if(value==null){
return super.exportHandler(obj,name,emptyValue);
}
if(value instanceof String&&StrUtil.isBlank((String) value)){
return super.exportHandler(obj,name,emptyValue);
}
}
return super.exportHandler(obj, name, value);
}

@Override
public Object importHandler(Member obj, String name, Object value) {
return super.importHandler(obj, name, value);
}
}
复制代码
  • 然后修改Controller中的接口,调用MemberExcelDataHandler处理器的setNeedHandlerFields设置需要自定义处理的字段,并调用ExportParams的setDataHandler设置自定义处理器;
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
less复制代码/**
* EasyPoi导入导出测试Controller
* Created by macro on 2021/10/12.
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {

@ApiOperation(value = "导出会员列表Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
ExportParams params = new ExportParams("会员列表", "会员列表", ExcelType.XSSF);
//对导出结果进行自定义处理
MemberExcelDataHandler handler = new MemberExcelDataHandler();
handler.setNeedHandlerFields(new String[]{"昵称"});
params.setDataHandler(handler);
map.put(NormalExcelConstants.DATA_LIST, memberList);
map.put(NormalExcelConstants.CLASS, Member.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "memberList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
}
复制代码
  • 再次调用导出接口,我们可以发现昵称已经添加默认设置了。

总结

体验了一波EasyPoi,它使用注解来操作Excel的方式确实非常好用。如果你想生成更为复杂的Excel的话,可以考虑下它的模板功能。

参考资料

项目官网:gitee.com/lemur/easyp…

本文转载自: 掘金

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

go-zero基础组件-分布式锁RedisLock

发表于 2021-11-04

什么场景下需要分布式锁

  1. 用户下单
    锁住uid,防止重复下单。
  2. 库存扣减
    锁住库存,防止超卖。
  3. 余额扣减
    锁住账户,防止并发操作。
    分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性。

分布式锁需要具备特性

  1. 排他性
    锁的基本特性,并且只能被第一个持有者持有。
  2. 防死锁
    高并发场景下临界资源一旦发生死锁非常难以排查,通常可以通过设置超时时间到期自动释放锁来规避。
  3. 可重入
    锁持有者支持可重入,防止锁持有者再次重入时锁被超时释放。
  4. 高性能高可用
    锁是代码运行的关键前置节点,一旦不可用则业务直接就报故障了。高并发场景下,高性能高可用是基本要求。

实现Redis锁应先掌握哪些知识点

  1. set命令

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。
  1. Redis.lua脚本

使用redis lua脚本能将一系列命令操作封装成pipline实现整体操作的原子性。

go-zero分布式锁RedisLock源码分析

core/stores/redis/redislock.go

  1. 加锁流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lua复制代码--KEYS[1]: 锁key
--ARGV[1]: 锁value,随机字符串
--ARGV[2]: 过期时间
--判断锁key持有的value是否等于传入的value
--如果相等说明是再次获取锁并更新获取时间,防止重入时过期
--这里说明是“可重入锁”
if redis.call("GET", KEYS[1]) == ARGV[1] then
--设置
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"

else
--锁key.value不等于传入的value则说明是第一次获取锁
--SET key value NX PX timeout : 当key不存在时才设置key的值
--设置成功会自动返回“OK”,设置失败返回“NULL Bulk Reply”
--为什么这里要加“NX”呢,因为需要防止把别人的锁给覆盖了
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end

image.png

  1. 解锁流程
1
2
3
4
5
6
7
8
lua复制代码--释放锁
--不可以释放别人的锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
--执行成功返回“1”
return redis.call("DEL", KEYS[1])
else
return 0
end

image.png

  1. 源码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
go复制代码package redis

import (
"math/rand"
"strconv"
"sync/atomic"
"time"

red "github.com/go-redis/redis"
"github.com/tal-tech/go-zero/core/logx"
)

const (
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
randomLen = 16
//默认超时时间,防止死锁
tolerance = 500 // milliseconds
millisPerSecond = 1000
)

// A RedisLock is a redis lock.
type RedisLock struct {
//redis客户端
store *Redis
//超时时间
seconds uint32
//锁key
key string
//锁value,防止锁被别人获取到
id string
}

func init() {
rand.Seed(time.Now().UnixNano())
}

// NewRedisLock returns a RedisLock.
func NewRedisLock(store *Redis, key string) *RedisLock {
return &RedisLock{
store: store,
key: key,
//获取锁时,锁的值通过随机字符串生成
//实际上go-zero提供更加高效的随机字符串生成方式
//见core/stringx/random.go:Randn
id: randomStr(randomLen),
}
}

// Acquire acquires the lock.
//加锁
func (rl *RedisLock) Acquire() (bool, error) {
//获取过期时间
seconds := atomic.LoadUint32(&rl.seconds)
//默认锁过期时间为500ms,防止死锁
resp, err := rl.store.Eval(lockCommand, []string{rl.key}, []string{
rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance),
})
if err == red.Nil {
return false, nil
} else if err != nil {
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
return false, err
} else if resp == nil {
return false, nil
}

reply, ok := resp.(string)
if ok && reply == "OK" {
return true, nil
}

logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
return false, nil
}

// Release releases the lock.
//释放锁
func (rl *RedisLock) Release() (bool, error) {
resp, err := rl.store.Eval(delCommand, []string{rl.key}, []string{rl.id})
if err != nil {
return false, err
}

reply, ok := resp.(int64)
if !ok {
return false, nil
}

return reply == 1, nil
}

// SetExpire sets the expire.
//需要注意的是需要在Acquire()之前调用
//不然默认为500ms自动释放
func (rl *RedisLock) SetExpire(seconds int) {
atomic.StoreUint32(&rl.seconds, uint32(seconds))
}

func randomStr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

关于分布式锁还有哪些实现方案

  1. etcd
  2. redis redlock

本文转载自: 掘金

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

Java8之Optional类的使用

发表于 2021-11-04

简介

optional类是java8中引入的针对NPE问题的一种优美处理方式,源码作者也希望以此替代null。

历史

1965年,英国一位名为Tony Hoare的计算机科学家在设计ALGOL W语言时提出了null引用的想法。Hoare选择null引用这种方式,“只是因为这种方法实现起来非常容易”。很多年后,他开始为自己曾经做过这样的决定而后悔不迭,把它称为“我价值百万的重大失误”。我们已经看到它带来的后果——程序员对对象的字段进行检查,判断它的值是否为期望的格式,最终却发现我们查看的并不是一个对象,而是一个空指针,它会立即抛出一个让人厌烦的NullPointerException异常[1]。

null带来的种种问题

  • 错误之源。

NullPointerException是目前Java程序开发中最典型的异常。

  • 代码膨胀。

它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。

  • 自身是毫无意义的。

null自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。

  • 破坏了Java的哲学。

Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。

  • 在Java的类型系统上开了个口子。

null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的赋值到底是什么类型。

方案

汲取Haskell和Scala的灵感,Java 8中引入了一个新的类java.util.Optional。这是一个封装Optional值的类。举例来说,使用新的类意味着,如果你知道一个人可能有学校也可能没有,那么Student类内部的school变量就不应该声明为Schoold,遭遇某学生没有学校时把null引用赋值给它,而是应该像本篇那样直接将其声明为Optional类型。

场景引入

首先我们引入一个常见的两个场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码/**
* >1 引入,常规判断一个学生的学校是不是公立学校,判断是否成年
*/
public static boolean checkIsPublicV1(Student student) {
if (student != null) {
School school = student.getSchool();
if (school != null) {
return school.isPublicFlag();
}
}
throw new RuntimeException("参数异常");
}

public static String getAdultV1(Student student) {
if (student != null) {
int age = student.getAge();
if (age > 18) {
return student.getName();
}
}
return "无";
}
复制代码

上述方式是我们常见的判读流程,optional就是针对每次空判断导致的代码欣赏性问题进行了一套解决方案

方法说明

image.png

构造函数

  • Optional(T var1)

源码

1
2
3
4
5
6
7
8
9
10
csharp复制代码private final T value;

private Optional() {
this.value = null;
}

private Optional(T var1) {
this.value = Objects.requireNonNull(var1);
}
复制代码

从源码可知,optional的构造器私有,不能直接创建,只能通过类中的其他静态方法创建,optional构造器支持一个泛型入参,且改参数不能为空

创建Optional对象

  • 声明一个空的Optional: Optional empty()
  • 依据一个非空值创建Optional: Optional of(T var0)
  • 可接受null的Optional: Optional ofNullable(T var0)

源码

empty()源码
1
2
3
4
5
6
7
8
9
10
11
csharp复制代码private static final Optional<?> EMPTY = new Optional();

private Optional() {
this.value = null;
}

public static <T> Optional<T> empty() {
Optional var0 = EMPTY;
return var0;
}
复制代码

从源码可知,optional类中维护了一个null值的对象,使用empty静态方法即可返回该空值对象

of(T var0)源码
1
2
3
4
typescript复制代码public static <T> Optional<T> of(T var0) {
return new Optional(var0);
}
复制代码

返回常见的有参构造对象,注意由于构造器要求入参不能为空,因此of方法的入参为空的话,依然会报NPE异常

ofNullable(T var0)源码
1
2
3
4
scss复制代码public static <T> Optional<T> ofNullable(T var0) {
return var0 == null ? empty() : of(var0);
}
复制代码

这个方法是对of方法的补强,当ofNullable方法的入参不为空是正常返回构造对象,当入参为空时,返回一个空值的optional对象,而不会抛出异常。 ofNullable方法大部分场景优于of的使用。

null引用和Optional.empty()有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果你尝试解引用一个null,一定会触发NullPointerException,不过使用Optional.empty()就完全没事儿,它是Op-tional类的一个有效对象,多种场景都能调用,非常有用。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码public static void testOptionalBuild() {
// 1. 无法直接new 'Optional()' has private access in 'java.util.Optional'
// Optional<School> school = new Optional<>();

// 2. of构建非空和空对象(NullPointerException)
/*Optional<String> o1 = Optional.of("test");
System.out.println(o1);
Optional<Object> o2 = Optional.of(null);
System.out.println(o2);*/

// 3. ofNullable构建非空和空对象(Optional.empty)
/*Optional<String> o3 = Optional.ofNullable("test");
System.out.println(o3);
Optional<Object> o4 = Optional.ofNullable(null);
System.out.println(o4);*/
}
复制代码

使用map从Optional对象中提取和转换值

  • Optional map(Function<? super T, ? extends U> var1)

源码

1
2
3
4
5
typescript复制代码public <U> Optional<U> map(Function<? super T, ? extends U> var1) {
Objects.requireNonNull(var1);
return !this.isPresent() ? empty() : ofNullable(var1.apply(this.value));
}
复制代码

当optional包裹的值为空时直接放回空对象,否则执行入参中的Function.apply方法

使用flatMap链接Optional对象

  • Optional flatMap(Function<? super T, Optional> var1)

源码

1
2
3
4
5
kotlin复制代码public <U> Optional<U> flatMap(Function<? super T, Optional<U>> var1) {
Objects.requireNonNull(var1);
return !this.isPresent() ? empty() : (Optional)Objects.requireNonNull(var1.apply(this.value));
}
复制代码

与map几乎一致。注意的是,入参的Function.apply方法中,返回类型为optional类型

举例

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码public static void testOptionalMap() {
Student student1 = getDefaultStudent();
Student student2 = getBackStudent();
// map
String school1 = Optional.ofNullable(student1).map(i -> i.getName()).orElse("无名");
String school2 = Optional.ofNullable(student2).map(i -> i.getName()).orElse("无名");
System.out.println("school1: " + school1 + "| school2: " + school2);
// flapMap 链式
String school3 = Optional.ofNullable(getOptionalStudent()).flatMap(i -> getOptionalStudent()).flatMap(i->i.getSchool()).map(i->i.getSchoolName()).orElse("没上大学");
System.out.println("school3: " + school3);
}
复制代码

默认行为及解引用Optional对象1

  • T orElse(T var1)
  • T orElseGet(Supplier<? extends T> var1)
  • T orElseThrow(Supplier<? extends X> var1)

注:这三个方法方法不是静态方法,因此需要通过实例对象调用,一般跟在方法ofNullable后用于处理空值或返回值

源码

orElse源码
1
2
3
4
kotlin复制代码public T orElse(T var1) {
return this.value != null ? this.value : var1;
}
复制代码

当optional中的包裹值不为空时返回包裹的值,若为空则返回orElse中的入参值

orElseGet源码
1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码public T orElseGet(Supplier<? extends T> var1) {
return this.value != null ? this.value : var1.get();
}
public T get() {
if (this.value == null) {
throw new NoSuchElementException("No value present");
} else {
return this.value;
}
}
复制代码

与上个方法类似,当optional中的包裹值不为空时返回包裹的值,若为空执行orElseGet中的Supplier方法

orElseThrow源码
1
2
3
4
5
6
7
8
kotlin复制代码public <X extends Throwable> T orElseThrow(Supplier<? extends X> var1) throws X {
if (this.value != null) {
return this.value;
} else {
throw (Throwable)var1.get();
}
}
复制代码

类似的,当optional中的包裹值不为空时返回包裹的值,若为空抛出orElseThrow中的Supplier.get的异常方法

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码public static void testOptionalOrElse() {
// orElse
Student stu = getDefaultStudent();
Student backStudent = getBackStudent();
Student realStu1 = Optional.ofNullable(stu).orElse(backStudent);
System.out.println(realStu1);

// orElseGet
Student realStu2 = Optional.ofNullable(stu).orElseGet(()-> getBackStudent());
System.out.println(realStu2);

// orElseGet
Student realStu3 = Optional.ofNullable(stu).orElseThrow(()-> new RuntimeException("学生不存在"));
System.out.println(realStu3);
}
复制代码

默认行为及解引用Optional对象2

  • boolean isPresent()
  • void ifPresent(Consumer<? super T> var1)

源码

isPresent()源码
1
2
3
4
typescript复制代码public boolean isPresent() {
return this.value != null;
}
复制代码

用户判断optional包裹的值是否为空,返回布尔值

ifPresent(Consumer var1)源码
1
2
3
4
5
6
typescript复制代码public void ifPresent(Consumer<? super T> var1) {
if (this.value != null) {
var1.accept(this.value);
}
}
复制代码

用户处理optional包裹的值不为空时,继续处理入参中Consumer.accept的方法。类似于 if(var!=null) {do sth}

举例

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码public static void testOptionalIfPresent() {
// isPresent()
Student student1 = getDefaultStudent();
Student student2 = getBackStudent();
boolean b1 = Optional.ofNullable(student1).isPresent();
boolean b2 = Optional.ofNullable(student2).isPresent();
System.out.println("b1: " + b1 + "| b2: " + b2);

// isPresent(Consumer)
Optional.ofNullable(student2).ifPresent(i-> acceptStudent(i, LocalDate.now()));
}
复制代码

使用filter剔除特定的值

  • Optional filter(Predicate<? super T> var1)

源码

1
2
3
4
5
6
7
8
9
kotlin复制代码public Optional<T> filter(Predicate<? super T> var1) {
Objects.requireNonNull(var1);
if (!this.isPresent()) {
return this;
} else {
return var1.test(this.value) ? this : empty();
}
}
复制代码

用于对optional对象的过滤,当optional包裹的值不为空时返回该值,否则执行filter入参的Predicate.test方法

举例

1
2
3
4
5
6
7
8
9
10
11
scss复制代码public static void testOptionalFilter() {
Student student1 = getDefaultStudent();
Student student2 = getBackStudent();
System.out.println(student1);
System.out.println(student2);
Student student3 = Optional.ofNullable(student1).filter(i -> i.getAge() > 18).orElse(getBackStudent());
Student student4 = Optional.ofNullable(student2).filter(i -> i.getAge() > 18).orElse(getBackStudent());
System.out.println(student3);
System.out.println(student4);
}
复制代码

实战

关于optional类的说明大致已经讲完,再回到开始的时候,提到的场景引入,结合optional进行改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码/**
* 实战1
* 针对原来的checkIsPublicV1进行改造
*/
public static boolean checkIsPublicV2(Student student) {
return Optional.ofNullable(student).map(i -> i.getSchool()).map(i -> i.isPublicFlag()).orElseThrow(() -> new RuntimeException("参数异常"));
}

/**
* 实战1
* 针对原来的getAdultV1进行改造
*/
public static String getAdultV2(Student student) {
return Optional.ofNullable(student).filter(i->i.getAge()>18).map(i->i.getName()).orElseGet(()->getDefaultStudent().getName());
}
复制代码

附:

补充代码

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
typescript复制代码public static void main(String[] args) {
//逐个放开
// 引入
// System.out.println(checkIsPublicV1(stu2));
// System.out.println(getAdultV1(stu2));
// optional方法
// testOptionalBuild();
// testOptionalOrElse();
// testOptionalIfPresent();
// testOptionalMap();
// testOptionalFilter();
// 实战
// System.out.println(getAdultV2(stu3));
// System.out.println(checkIsPublicV2(stu3));
}

/**========模型数据=======**/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
static class Student {
private String name;
private int age;
private School school;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
static class School {
private String schoolName;
private boolean publicFlag;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
static class StudentOpt {
private String name;
private int age;
private Optional<School> school;
}

public static Student getDefaultStudent() {
return null;
}

public static Student getBackStudent() {
return Student.builder().name("小红").age(19).build();
}

public static Optional<StudentOpt> getOptionalStudent() {
return Optional.ofNullable(StudentOpt.builder().name("小莫").age(18)
.school(Optional.ofNullable(School.builder().schoolName("蓝鲸大学").publicFlag(true).build())).build());
}

public static void acceptStudent(Student stu, LocalDate date) {
System.out.println("日期: " + date + " 新增一位学生: " + stu.getName());
}
复制代码

[1] 参考自java8实战

详细源码,请参考:github.com/chetwhy/clo…

本文转载自: 掘金

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

合理使用Java与Spring Boot建立短链接生成器

发表于 2021-11-04

URL短链接生成器是一种根据冗长的URL,创建短链接的服务。通常,短链接的长度只有原始URL的三分之一、甚至四分之一。因此它们更容易被输入、呈现、以及推送。用户只需单击短链接,便可被自动重定向到原始的URL处。

目前,tiny.cc、bitly.com和cutt.ly都能够提供在线式的URL缩短服务。当然,您也可以为应用系统自行设计和开发出缩短URL的服务。下面,我和您讨论具体的实现过程。首先,让我们来探讨一下与之相关的功能性和非功能性的需求。

功能要求:

  • 保存用户输入的长URL,并据此生成相应的短链接。
  • 允许用户选择到期日期,以便生成的短链接在该日期后自动无效。
  • 方便用户在单击短链接后,重定向到原始的长链接处。
  • 作为可选的方式,允许用户创建服务帐户,并让生成的短链接仅对该账户有效。
  • 以可选的方式,允许用户自行创建短链接。
  • 以可选的方式,允许用户标记出那些最常访问的链接。

非功能性要求:

  • 生成服务具有持续的有效性和可访问性。
  • 重定向的用时应不超过2秒。

URL转换的方式

URL短链接生成器中最重要的是转换算法。不同的转换方式通常会产生不同的输出,而且它们各有优、缺点。假设我们需要一个最长为7个字符的短链接。那么我们可以采用 MD5 或 SHA-2之类的哈希函数,对原始的URL进行散列处理。由于散列的结果会超过7个字符,因此我们只取前7个字符。不过,由于前7个字符可能已经被用于其他短链接,并由此会引发冲突,因此我们需要依次截取后面的7个字符,直至找到一个被使用过的短链接为止。

生成短链接的第二种方法是使用 UUID 。UUID被复制的概率近似为零,因此可以完全忽略冲突的可能。由于UUID是由36个字符组成,仍然可能遇到上述问题,因此我们应当截取前7个字符,然后检查该组合是否已被占用。

第三种方法是将数字从 Base 10 转换为Base 62。Base是可用于表示特定数字的字符数。Base 10是我们日常生活中使用的数字,即:[0-9],而Base 62则是:[0-9][az][AZ]。这意味着,以10为Base的四位数字,将与以62为Base、但具有两个字符的数字相同。因此在URL转换中,使用最大长度为7个字符的Base 62,将允许我们为短链接提供62^7个唯一值。

Base 62的转换机制

我使用如下算法,将一个Base为10的数字转换为Base为62:

1
2
3
4
typescript复制代码while(number > 0)     
remainder = number % 62     
number = number / 62     
attach remainder to start of result collection

据此,我们只需要将结果集中的数字映射到Base为62的字符 [0,1,2,…,a,b,c…,A,B,C,…]即可。

下面,我通过将1000从Base 10转换为Base 62的例子,来讨论其工作机制。

1
2
3
4
5
6
7
8
9
10
11
ini复制代码1st iteration: 
         number = 1000 
         remainder = 1000 % 62 = 8 
         number = 1000 / 62 = 16 
         result list = [8] 
2nd iteration: 
         number = 16 
         remainder = 16 % 62 = 16 
         number = 16 / 62 = 0 
         result list = [16,8] 
         There is no more iterations since number = 0 after 2nd iteration

[16,8] 被映射到Base 62后为g8,即1000base10 = g8base62。

而从Base 62转换为Base 10的过程也很简单,即:

1
2
3
4
5
6
ini复制代码i = 0     
while(i < inputString lenght) 
         counter = i + 1 
         mapped = base62alphabet.indexOf(inputString[i]) // map character to number based on its index in alphabet          
         result = result + mapped * 62^(inputString lenght - counter) 
         i++

所以其对应的代码示例为:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码inputString = g8     
inputString length = 2     
i = 0     
result = 0 
1st iteration 
        counter = 1 
        mapped = 16 // index of g in base62alphabet is 16 
        result = 0 + 16 * 62^1 = 992 
2nd iteration 
        counter = 2 
        mapped = 8 // index of 8 in base62alphabet is 8 
        result = 992 + 8 * 62^1 = 1000

实现

我使用Spring Boot和MySQL来实现该服务 ,我用到了数据库的自动递增功能来实现Base 62的转换。当然,您也可以使用任何其他具有自动递增功能的数据库。

首先,请访问 Spring initializr ,并选择Spring Web与MySQL Driver。接着,请单击“生成(Generate)”按钮,并下载对应的zip文件。完成解压缩之后,我们就可以在自己的IDE中打开该项目了。

我通过创建文件夹:控制器、实体、服务、存储库、dto和配置,实现在逻辑上划分程序代码。

在“实体”文件夹中,我创建了一个具有id、longUrl、createdDate和expiresDate四个属性的 Url.java类

请注意,此处既没有短链接的属性,也不会去保存短链接。每次只要有GET请求的出现,我们都会将id属性从Base 10转换为Base 62,以便节省数据库中的空间。

用户在访问该短链接时,应根据longURL属性重定向到目标网站。createdDate则只是为了查看longURL何时被保存(并不重要)。而如果用户希望在一段时间后让短链接失效的话,可以对expiresDate进行设置。

接着,我在“服务”文件夹中,创建了一个 BaseService.java 文件。其中包含了从Base 10到Base 62相互转换的方法。

1
2
3
ini复制代码private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 
private char[] allowedCharacters = allowedString.toCharArray(); 
private int base = allowedCharacters.length;

正如前面所提到的,若要使用Base 62转换,则需要有一个被称为allowedCharacters的Base 62的字母表。此外,为了方便按需更改被允许的字符,我们可根据字符的长度,计算出基本变量的值。其中,编码(encode)方法会将一个数字作为输入,返回一个短链接;而解码(decode)方法则会接受一个字符串(如:短链接)作为输入,并返回一个数字。

在存储库文件夹中,我创建了 UrlRepository.java 文件。它只是JpaRepository的一个扩展,并给出了诸如“findById”,“save”等方法。在此,我们无需进行任何添加。

然后,我在“控制器”文件夹中创建了一个URLController.java文件(请参见如下代码)。它提供一种用于创建短链接的POST方法,以及一种被用于重定向到原始URL的GET方法。

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@PostMapping("create-short") 
    public String convertToShortUrl(@RequestBody UrlLongRequest request) { 
        return urlService.convertToShortUrl(request); 
    } 
 
    @GetMapping(value = "{shortUrl}") 
    public ResponseEntity<Void> getAndRedirect(@PathVariable String shortUrl) { 
        var url = urlService.getOriginalUrl(shortUrl); 
        return ResponseEntity.status(HttpStatus.FOUND) 
        .location(URI.create(url)) 
        .build(); 
}

其中,POST方法会将 UrlLongRequest 作为请求体。它是一个具有longURL和expiresDate属性的类。而GET方法会将一个短的URL作为路径变量,以获取并重定向到原始的URL处。

在控制器的上层,urlService会作为依赖项被注入,以便后续进行解释。

UrlService.java 既包含了大量逻辑,又为控制器提供了服务。ConvertToShortUrl仅供控制器的POST方法所使用。它只是在数据库中创建了一条新的记录,并获取一个id,以便将其转换为Base 62的短链接,并返回给控制器。

控制器使用GetOriginalUrl方法,首先将字符串转换为Base 10类型的id。然后,它通过该id从数据库中获取一条记录。当然,如果该记录不存在的话,则会抛出异常。最后,它会将原始的URL返回给控制器。

下面,我将和您讨论Swagger文档、应用的dockerization(容器化)、缓存以及MySQL的计划事件。

Swagger的用户界面

在开发过程中文档记录无疑能够使得API更易于理解和使用。在该项目中,我使用Swagger UI来记录API。 Swagger UI 允许任何人在没有任何实现逻辑的情况下,可视化API资源,并与之交互。它不但能够自动生成,而且带有可视化的文档,以便于后端的实现和客户端的使用。

我通过执行如下步骤,在项目中引入了Swagger UI。首先,我在pom.xml文件 中添加了Maven依赖项:

1
2
3
4
5
6
7
8
9
10
11
xml复制代码XML 
<dependency> 
  <groupId>io.springfox</groupId> 
  <artifactId>springfox-swagger2</artifactId> 
  <version>2.9.2</version> 
</dependency> 
<dependency> 
  <groupId>io.springfox</groupId> 
  <artifactId>springfox-swagger-ui</artifactId> 
  <version>2.9.2</version> 
</dependency>

添加了Maven依赖项后,我们便可以添加Swagger的相关配置了。我在“配置”文件夹中,创建了一个新的类– SwaggerConfig.java ,请参考如下代码段。

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码 @Configuration 
    @EnableSwagger2 
    public class SwaggerConfig { 
 
    @Bean     
    public Docket apiDocket() {    
        return new Docket(DocumentationType.SWAGGER_2)   
            .apiInfo(metadata())     
            .select()     
            .apis(RequestHandlerSelectors.basePackage("com.amarin"))     
            .build();     
    } 
               
    private ApiInfo metadata(){ 
        return new ApiInfoBuilder() 
        .title("Url shortener API")     
        .description("API reference for developers")     
        .version("1.0")     
        .build();     
        }   
}

在该类的顶部,我添加了如下注释:

  • @Configuration表示一个类声明了一到多个@Beans方法,并且可以由Spring容器通过处理,在运行时为这些bean生成相应的定义和服务请求。
  • @EnableSwagger2表示应该启用Swagger支持。

接下来,我添加了Docket bean。它提供的主要API配置,带有各种合理的默认值、以及便捷的配置方法。

此处的apiInfo()方法除了可以使用默认值,还能够接受ApiInfo对象,以便我们配置所有必要的API信息。为了使代码更加简洁,我们可以创建一个私有的方法—metadata(),来配置和返回ApiInfo对象,并将该方法作为apiInfo()方法的参数进行传递。同时,apis()方法也允许我们过滤那些被文档化的包。

在完成了Swagger UI的配置后,我们便可以开始文档化API了。在 UrlController 内部的每个端点上,我们可以使用 @ApiOperation 来添加描述性的注释。当然,您也可以按需使用 其他类型的注释 。

我们还可以文档化 DTO ,并使用 @ApiModelProperty 来添加各种允许的值和描述。

缓存

根据维基百科的定义, 缓存 是存储数据的软、硬件组件,可用来更快地处理后续对于相同数据的请求。而存储在缓存中的数据,往往是早期计算的结果、或是已存储在其他地方的数据副本。

目前,最常用的缓存类型是内存缓存(in-memory cache)。它能够将缓存的数据存储到RAM中。当被请求数据与缓存一致时,它是从RAM、而非从数据库被提取。据此,我们避免频繁调用后端的开销。

由于URL短链接生成器可以被认为是一种读取多于写入的请求应用,因此它是使用缓存的理想应用场景。若想在Spring Boot应用中启用缓存,我们只需要在 UrlShortenerApiApplication类中添加 @EnableCaching 注释即可。

接着,在 控制器 中,我们需要在GET方法上设置 @Cachable 注解,以实现自动将方法调用的结果存入缓存中。在@Cachable的注解中,我设置了缓存名称的value参数和缓存键的key参数。鉴于缓存键的唯一性,我使用了“shortUrl”,并将Sync参数设置为true,以确保只有一个线程正在构建缓存值。

至此,当我们首次加载带有短链接的URL时,其结果将会被保存到缓存中。后续,任何端点若想调用相同短链接,都会从缓存、而非从数据库中检索结果。

Dockerization

Dockerization是将应用程序及其依赖项打包到 Docker 容器中的过程。一旦配置了Docker容器,我们便可以轻松地在任何支持Docker的服务器、或主机上运行应用程序。

因此,我们首先需要创建一个包含所有命令的 Dockerfile 文本文件,以便用户通过调用命令行的方式,挂载某个镜像。

Dockerfile

1
2
3
4
bash复制代码FROM openjdk:13-jdk-alpine    
    COPY ./target/url-shortener-api-0.0.1-SNAPSHOT.jar /usr/src/app/url-shortener-api-0.0.1-SNAPSHOT.jar     
    EXPOSE 8080     
ENTRYPOINT ["java","-jar","/usr/src/app/url-shortener-api-0.0.1-SNAPSHOT.jar"]
  • FROM:表示需要构建的基础镜像。我使用的是Java免费开源版–OpenJDK v13。您也可以在共享的Docker镜像平台–Docker hub(hub.docker.com/)上,找到其他类型ba…%E4%B8%8A%EF%BC%8C%E6%89%BE%E5%88%B0%E5%85%B6%E4%BB%96%E7%B1%BB%E5%9E%8Bbase%E9%95%9C%E5%83%8F%E3%80%82)
  • COPY:此命令会将文件从本地文件系统,复制到指定路径的容器文件系统中。在此,我将目标文件夹中的JAR文件,复制到容器中的/usr/src/app文件夹中(稍后我将解释如何创建JAR文件)。
  • EXPOSE:负责通知Docker容器在运行时,侦听指定网络端口的指令。其默认协议为TCP,您也可以使用UDP。
  • ENTRYPOINT:负责配置可执行的容器。在此,我通过命令为“java -jar.jar”,指定Docker将如何运行一个.jar文件类型的应用程序。

为了在项目中创建.jar文件,以便Dockerfile中的COPY命令能够正常工作,我使用Maven来创建可执行的.jar。如果您的pom.xml缺少Maven,请用如下方式进行添加:

XML

1
2
3
4
5
6
7
8
xml复制代码<build>      
    <plugins>      
        <plugin>      
            <groupId>org.springframework.boot</groupId>      
            <artifactId>spring-boot-maven-plugin</artifactId>      
        </plugin>      
    </plugins>      
</build>

随后,我运行命令:mvn clean package,以构建出一个Docker镜像。接着,在Dockerfile文件夹中,我运行了命令:docker build -t url-shortener:latest。其中,-t可用于标记一个镜像,并实现版本控制。在此,即为最新的存储库URL-shortener。我们可以使用命令“docker images”来创建镜像。屏幕上的显示结果为:

最后,我还需要在docker容器中构建MySQL服务器镜像,以方便数据库容器与应用容器相隔离。为此,我在Docker容器中运行了如下命令:

1
css复制代码$ docker run --name shortener -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 3306:3306 mysql:8

您可以在 Docker hub 上查看到相关文档。

为了在容器内运行数据库,我通过配置,将现有的应用程序连接上该MySQL服务器。即:在 application.properties 中设置spring.datasource.url,以连接到shortener容器。

然后,我使用以下命令来运行已构建好的Docker 镜像容器:

1
arduino复制代码docker run -d –-name url-shortener-api -p 8080:8080 --link shortener url-shortener
  • -d表示Docker容器在终端的后台运行。
  • –name可设置容器的名称。
  • -p host-port:docker-port:是将本地端口映射到容器内的端口上。在本例中,我在容器内公开了端口8080,并映射到了本地的8080上。
  • –link:用于链接应用容器与数据库容器,以实现容器间的相互发现和安全传输。
  • url-shortener:则指明了待运行的Docker镜像名称。

至此,我们便可以在浏览器中访问http://localhost:8080/swagger-ui.html了。通过将镜像发布到Docker Hub上,任何计算机和服务器都可以轻松地运行该应用。

当然,为了改善该Docker的使用体验,我们需要注意多阶段构建,以及docker-compose两个方面。

多阶段构建

使用 多阶段构建 ,您将可以在Dockerfile中使用多个FROM语句。每个FROM指令都可以使用不同的base,并且每个指令都能够开启构建的新阶段。您可以有选择性地将各个工件(artifacts)从一个阶段复制到另一个阶段,并在最终镜像中去掉不想要的内容。

多阶段构建有利于我们避免每次对代码进行更改后,都必须手动重建.jar文件。据此,我们可以定义一个构建阶段,来执行Maven包命令。而另一个阶段会将来自第一次构建的结果,直接复制到Docker容器的文件系统中。您可以通过链接–github.com/AnteMarin/U…

Docker-compose

Compose 是一个用于定义和运行多容器Docker应用的工具。借助Compose,您可以使用YAML文件,来配置应用程序的服务,然后使用单个命令,从配置中创建并启动所有的服务。

使用docker-compose,我们能够将应用程序和数据库打包到一个配置文件中,以便立即运行所有的内容。据此,我们避免了每次去运行MySQL容器,将其链接到应用容器的繁琐。

由 Docker-compose.yml 文件的具体配置内容可知:首先,我们通过设置镜像mysql v8.0和MySQL服务器的凭据,来配置MySQL容器。接着,我们通过设置构建参数,来配置应用容器,毕竟我们需要的是镜像,而非使用MySQL进行拉取。此外,我们还需要通过设置,让应用容器依赖于MySQL容器。最终,我们可以使用命令“docker-compose up”,来运行整个项目。

MySQL计划事件(Scheduled Event)

说到短链接的到期设置,我们既可以让用户自定义,又可以保持默认值。为此,我们可以在数据库中设置一个计划事件。通过每x分钟运行一次该事件,到期时间只要小于当前时间,数据库就会自动删除某一行,就这么简单。这非常适用于保持数据库中的少量数据。不过,该方法有两个问题值得注意:

  • 首先,该事件只会从数据库中删除记录,而不会从缓存中删除数据。如前所述,如果缓存可以找到匹配的数据的话,就不会去查看数据库。因此,某条短链接即便已经在数据库中被删除了,我们仍然可以从缓存中获取它。
  • 其次,在示例脚本中,我设置该事件为每隔2分钟运行一次。如果数据库的记录变动较大,则可能出现前一个事件尚未在其预定的间隔周期内执行完毕,后一个事件已被触发,进而出现多个事件实例同时在执行的混乱局面。

小结

通过上述示例和讨论,我向您展示了如何使用Java和Spring Boot,来创建URL短链接生成器的API。这是一个十分常见的面试问题,您既可以据此创建自己的改进版本,又可以从上述GitHub处克隆项目的存储库,并创建自己的前端。

本文转载自: 掘金

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

go-zero基础组件-分布式布隆过滤器(Bloom Fil

发表于 2021-11-04

为什么需要布隆过滤器

想象一下遇到下面的场景你会如何处理:

  1. 手机号是否重复注册
  2. 用户是否参与过某秒杀活动
  3. 伪造请求大量id查询不存在的记录,此时缓存未命中,如何避免缓存穿透。

针对以上问常规做法是:查询数据库,数据库硬扛,如果压力并不大可以使用此方法,保持简单即可。

改进做法:用list/set/tree维护一个元素集合,判断元素是否在集合内,时间复杂度或空间复杂度会比较高。如果是微服务的话可以用redis中的list/set数据结构, 数据规模非常大此方案的内存容量要求可能会非常高。

这些场景有个共同点,可以将问题抽象为:如何高效判断一个元素不在集合中?
那么有没有一种更好方案能达到时间复杂度和空间复杂双优呢?

有!布隆过滤器。

什么是布隆过滤器

布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法。

工作原理

布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点(offset),把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
​

简单来说就是准备一个长度为m的位数组并初始化所有元素为0,用k个散列函数对元素进行k次散列运算跟len(m)取余得到k个位置并将m中对应位置设置为1。

布隆过滤器优缺点

优点:

  1. 空间占用极小,因为本身不存储数据而是用比特位表示数据是否存在,某种程度有保密的效果。
  2. 插入与查询时间复杂度均为O(k),常数级别,k表示散列函数执行次数。
  3. 散列函数之间可以相互独立,可以在硬件指令层加速计算。

缺点:

  1. 误差(假阳性率)。
  2. 无法删除。

误差(假阳性率)

布隆过滤器可以100%判断元素不在集合中,但是当元素在集合中时可能存在误判,因为当元素非常多时散列函数产生的k位点可能会重复。
维基百科有关于假阳性率的数学推导(见文末链接)这里我们直接给结论(实际上是我没看懂…),假设:

  • 位数组长度m
  • 散列函数个数k
  • 预期元素数量n
  • 期望误差_ε_

在创建布隆过滤器时我们为了找到合适的m和k,可以根据预期元素数量n与_ε_来推导出最合适的m与k。

image.png

java中Guava,Redisson实现布隆过滤器估算最优m和k采用的就是此算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  //计算哈希次数
@VisibleForTesting
static int optimalNumOfHashFunctions(long n, long m) {
// (m / n) * log(2), but avoid truncation due to division!
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}

//计算位数组长度
@VisibleForTesting
static long optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}

无法删除

位数组中的某些k点是多个元素重复使用的,假如我们将其中一个元素的k点全部置为0则直接就会影响其他元素。
这导致我们在使用布隆过滤器时无法处理元素被删除的场景。
​

可以通过定时重建的方式清除脏数据。假如是通过redis来实现的话重建时不要直接删除原有的key,而是先生成好新的再通过rename命令即可,再删除旧数据即可。

go-zero中的bloom filter源码分析

core/bloom/bloom.go
​
一个布隆过滤器具备两个核心属性:

  1. 位数组:
  2. 散列函数

go-zero实现的bloom filter中位数组采用的是Redis.bitmap,既然采用的是redis自然就支持分布式场景,散列函数采用的是MurmurHash3

Redis.bitmap为什么可以作为位数组呢?

Redis中的并没有单独的bitmap数据结构,底层使用的是动态字符串(SDS)实现,而Redis中的字符串实际都是以二进制存储的。
a 的ASCII码是 97,转换为二进制是:01100001,如果我们要将其转换为b只需要进一位即可:01100010。下面通过Redis.setbit实现这个操作:

set foo a

OK

get foo

“a”

setbit foo 6 1

0

setbit foo 7 0

1

get foo

“b”

bitmap底层使用的动态字符串可以实现动态扩容,当offset到高位时其他位置bitmap将会自动补0,最大支持232-1长度的位数组(占用内存512M),需要注意的是分配大内存会阻塞Redis进程。
根据上面的算法原理可以直到实现布隆过滤器主要做三件事情:

  1. k次散列函数计算出k个位点。
  2. 插入时将位数组中k个位点的值设置为1。
  3. 查询时根据1的计算结果判断k位点是否全部为1,否则表示该元素一定不存在。

下面来看看go-zero是如何实现的:

对象定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码//表示经过多少散列函数计算
//固定14次
maps = 14

type (
// 定义布隆过滤器结构体
Filter struct {
bits uint
bitSet bitSetProvider
}
//位数组操作接口定义
bitSetProvider interface {
check([]uint) (bool, error)
set([]uint) error
}
)

位数组操作接口实现

首先需要理解两段lua脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码//ARGV:偏移量offset数组
//KYES[1]: setbit操作的key
//全部设置为1
setScript = `
for _, offset in ipairs(ARGV) do
redis.call("setbit", KEYS[1], offset, 1)
end
`
//ARGV:偏移量offset数组
//KYES[1]: setbit操作的key
//检查是否全部为1
testScript = `
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
`

为什么一定要用lua脚本呢?
因为需要保证整个操作是原子性执行的。

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
go复制代码//redis位数组
type redisBitSet struct {
store *redis.Client
key string
bits uint
}
//检查偏移量offset数组是否全部为1
//是:元素可能存在
//否:元素一定不存在
func (r *redisBitSet) check(offsets []uint) (bool, error) {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return false, err
}
//执行脚本
resp, err := r.store.Eval(testScript, []string{r.key}, args)
//这里需要注意一下,底层使用的go-redis
//redis.Nil表示key不存在的情况需特殊判断
if err == redis.Nil {
return false, nil
} else if err != nil {
return false, err
}

exists, ok := resp.(int64)
if !ok {
return false, nil
}

return exists == 1, nil
}

//将k位点全部设置为1
func (r *redisBitSet) set(offsets []uint) error {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return err
}
_, err = r.store.Eval(setScript, []string{r.key}, args)
//底层使用的是go-redis,redis.Nil表示操作的key不存在
//需要针对key不存在的情况特殊判断
if err == redis.Nil {
return nil
} else if err != nil {
return err
}
return nil
}

//构建偏移量offset字符串数组,因为go-redis执行lua脚本时参数定义为[]stringy
//因此需要转换一下
func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) {
var args []string
for _, offset := range offsets {
if offset >= r.bits {
return nil, ErrTooLargeOffset
}
args = append(args, strconv.FormatUint(uint64(offset), 10))
}
return args, nil
}

//删除
func (r *redisBitSet) del() error {
_, err := r.store.Del(r.key)
return err
}

//自动过期
func (r *redisBitSet) expire(seconds int) error {
return r.store.Expire(r.key, seconds)
}

func newRedisBitSet(store *redis.Client, key string, bits uint) *redisBitSet {
return &redisBitSet{
store: store,
key: key,
bits: bits,
}
}

到这里位数组操作就全部实现了,接下来看下如何通过k个散列函数计算出k个位点

k次散列计算出k个位点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lua复制代码//k次散列计算出k个offset
func (f *Filter) getLocations(data []byte) []uint {
//创建指定容量的切片
locations := make([]uint, maps)
//maps表示k值,作者定义为了常量:14
for i := uint(0); i < maps; i++ {
//哈希计算,使用的是"MurmurHash3"算法,并每次追加一个固定的i字节进行计算
hashValue := hash.Hash(append(data, byte(i)))
//取下标offset
locations[i] = uint(hashValue % uint64(f.bits))
}

return locations
}

插入与查询

添加与查询实现就非常简单了,组合一下上面的函数就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码//添加元素
func (f *Filter) Add(data []byte) error {
locations := f.getLocations(data)
return f.bitSet.set(locations)
}

//检查是否存在
func (f *Filter) Exists(data []byte) (bool, error) {
locations := f.getLocations(data)
isSet, err := f.bitSet.check(locations)
if err != nil {
return false, err
}
if !isSet {
return false, nil
}

return true, nil
}

改进建议

整体实现非常简洁高效,那么有没有改进的空间呢?

个人认为还是有的,上面提到过自动计算最优m与k的数学公式,如果创建参数改为:

预期总数量expectedInsertions

期望误差falseProbability

就更好了,虽然作者注释里特别提到了误差说明,但是实际上作为很多开发者对位数组长度并不敏感,无法直观知道bits传多少预期误差会是多少。

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
java复制代码// New create a Filter, store is the backed redis, key is the key for the bloom filter,
// bits is how many bits will be used, maps is how many hashes for each addition.
// best practices:
// elements - means how many actual elements
// when maps = 14, formula: 0.7*(bits/maps), bits = 20*elements, the error rate is 0.000067 < 1e-4
// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
func New(store *redis.Redis, key string, bits uint) *Filter {
return &Filter{
bits: bits,
bitSet: newRedisBitSet(store, key, bits),
}
}

//expectedInsertions - 预期总数量
//falseProbability - 预期误差
//这里也可以改为option模式不会破坏原有的兼容性
func NewFilter(store *redis.Redis, key string, expectedInsertions uint, falseProbability float64) *Filter {
bits := optimalNumOfBits(expectedInsertions, falseProbability)
k := optimalNumOfHashFunctions(bits, expectedInsertions)
return &Filter{
bits: bits,
bitSet: newRedisBitSet(store, key, bits),
k: k,
}
}

//计算最优哈希次数
func optimalNumOfHashFunctions(m, n uint) uint {
return uint(math.Round(float64(m) / float64(n) * math.Log(2)))
}

//计算最优数组长度
func optimalNumOfBits(n uint, p float64) uint {
return uint(float64(-n) * math.Log(p) / (math.Log(2) * math.Log(2)))
}

​

资料

布隆过滤器(Bloom Filter)原理及Guava中的具体实现

布隆过滤器-维基百科

Redis.setbit

本文转载自: 掘金

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

译:Go 与常量 常量

发表于 2021-11-04

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

常量

Pob Pike 2014年8月24日

原文

介绍

Go是一门静态语言,它不允许不同数字类型间的操作。你不能将一个浮点数(float64)和一个整数(int)相加,也不能将一个32位整数(int32)和一个通用整数(int)相加。这些写法也是非法的:1e6*time.Second、math.Expr(1)、1<<(‘\t’+2.0)。在Go中,常量和变量不同,它很像是常数(regular number)。这篇文章将解释其中缘由。

背景:C

在之前关于Go的思考中,我们讨论过C语言和C延伸出的语言允许你混合不同数字类型会引发很多问题。许多匪夷所思的bug,异常中断和兼容性问题都是由于表达式里由不同长度的整型和符号类型(signedness)组成所导致的。即便是对经验丰富的C语言工程师,对于下面代码的计算结果也会迟疑。

1
2
3
ini复制代码unsigned int u = 1e9;
long signed int i = -1;
... i + u ...

最后的答案是多少?它是什么类型的?有符号还是无符号的?

代码里还潜伏这bug。

C语言有一系列“通用算数转换”规则,it is an indicator of their subtlety that they have changed over the years (introducing yet more bugs, retroactively).

当设计Go时,我们决定避免这个雷区,我们不允许不同数字类型之间的混合操作。如果你想将i和u相加,你必须显示声明你最终想要得到的类型。

1
2
csharp复制代码var u uint
var i int

你可以写 uint(i)+u 或者 i+int(u),这样清楚地表达了相加操作的结果类型,你不能像C语言那样写 i+u。即使int是32bit位的,你也不能将int类型数字和int32类型数字混合操作。

这个强制要求消除类一些通常的bug和异常,他是Go中重要的属性。但它也需要代价:有时需要程序员去用笨拙的数字类型装换去装饰代码以清楚地表达出含义。

那常量怎么办?在上面的声明里,怎么合法地写 i=0 或者 u=0?0的类型是什么?

1
csharp复制代码var i int = int(0)

这种声明方式显然是不合理的。

我们很快意识到答案就是让数字常量的工作方式和其他C类语言不同。在更多的思考和实验之后,我们想出了我们相信最正确的设计,将程序员从总是要转换常量中解放出来,他们可以这样写:math.Sqrt(2),编译器也不会报错。

总之,在Go中,常量总是行得通的。让我们看看发生了什么。

术语

首先在Go中,const是一个关键字,用一个标量(比如:2、3.14159、”scrumptious”)来声明一个名字。这样的值(命名或其他形式)在Go中称为常量。常量也可通过由常量构建的表达式创建,比如:2+3、2+3i、Pi/2、(“go” + “pher”)。

某些语言没有常量,而另一些语言则具有常量的更一般定义或const单词的应用。例如,在C和C++中,const是一个类型限定符,可以将更多复杂值的更复杂属性进行编码。

但是在Go中,常量仅仅是单一不可变的值,现在开始我们只讨论Go中的常量。

字符串

这里有很多种类型的数字常量:整型、浮点型、rune、有符号型、无符号型、虚数型、复数型。让我们以简单的字符串常量作为开始。字符串常量很容易理解,可以在其中探索Go中常量的类型问题。

一个字符串常量是双引号闭合的文本(Go也支持原生字符串写法,即用反引号闭合

1
2


arduino复制代码”Hello, 世界”

1
2
3
4
5

字符串常量的类型是什么?string是错误的答案。


它是无类型字符串常量,即它没有固定的类型。没错,它是一个字符串,但它的类型不是Go里面的string。即便提供一个变量名,它仍然是无类型字符串常量:

ini复制代码const hello = “Hello, 世界”

1
2
3
4
5
6
7
8

这样声明之后,hello还是一个无类型字符串常量,一个无类型常量只有一个没有定义类型的值,所以它不受强制相同类型才可操作的限制。


正是无类型常量的概念让我们可以在Go中自由使用常量。


那么,有类型字符串常量是什么?下面是一个有类型的例子:

c复制代码const typedHello string = “Hello, 世界”

1
2

注意typedHello的声明,有一个显式string类型在等号前面。这意味着typedHell是string类型,它不可以被分配给其他类型。正确示例:

csharp复制代码var s string
s = typeHello
fmt.Println(s)

1
2

错误示例:

go复制代码type MyString string
var m Mystring
m = typedHello // Type error
fmt.Println(m)

1
2

变量m是Mystring类型的,它不能被其他类型值赋值。它只能被MyString类型值赋值,像这样:

ini复制代码const myStringHello MyString = “Hello, 世界”
m = myStringHello // OK
fmt.Println(m)

1
2

或者通过强制类型装换来解决:

scss复制代码m = MyString(typedHello)
fmt.Println(m)

1
2

回到我们的无类型字符串常量,因为它没有类型,所有把它赋值给一个有类型的变量不会引起类型错误。我们可以这样写:

ini复制代码m = “Hello, 世界”

1
2

或者

ini复制代码m = hello

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

不像有类型常量typedHello和myStringHello,无类型常量"Hello, 世界"和hello没有类型,所有把他们赋值给任何类型兼容string的变量都不会出错。


这些无类型字符串常量当然是字符串,所以他们只能用在string允许的地方,但他们没有类型。


默认类型
----


做为一名Go程序员,你肯定见过这里的声明:

go复制代码str := “Hello, 世界”

1
2

现在,你可能会问:“如果常量是无类型的,那比变量str是什么类型的?”答案是string,无类型常量会有一个默认类型,会在需要时自动将自己转换为该类型。对于无类型字符串常量,默认类型是string,所以

go复制代码str := “Hello, 世界”

1
2

或者

ini复制代码var str = “Hello, 世界”

1
2

意思都是

csharp复制代码var str string = “Hello, 世界”

1
2
3
4
5

你可以把无类型常量当成一种在特殊空间的值,它们不会受到Go类型系统的大部分限制。但需要将它赋值给有类型的变量时,它们把自己的默认类型告诉变量。在这个例子中,str变成了一个string类型的值,因为无类型字符串常量提供它们的默认类型string用来声明。


在上面的声明中,一个变量会被声明并带着类型和初始值。有时当我们使用常量时,并没有明确的目标值,例如:

perl复制代码fmt.Printf(“%s”, “Hello, 世界”)

1
2

输出:

复制代码Hello, 世界

1
2

fmt.Printf的签名是

go复制代码func Printf(format string, a …interface{}) (n int, err error)

1
2
3
4
5

format之后的参数是接口类型(interface)。但调用fmt.Printf时,使用一个无类型常量作为参数传递时,参数的具体类型是常量的默认类型。这个过程类似于我们之前使用无类型的字符串常量声明初始化值时看到的过程。


你可以看到洗下面例子的结果,使用%v打印值,%T打印值的类型:

perl复制代码fmt.Printf(“%T: %v\n”, “Hello, 世界”, “Hello, 世界”)
fmt.Printf(“%T: %v\n”, hello, hello)

1
2

输出:

c复制代码string: Hello, 世界
string: Hello, 世界

1
2

如果常量有类型,那么会传递到接口,像下面这样:

perl复制代码fmt.Printf(“%T: %v\n”, myStringHello, myStringHello)

1
2

输出:

css复制代码main.MyString: Hello, 世界

1
2
3
4
5
6
7
8
9

总结一下,一个有类型常量会遵循Go里所有的类型规则;一个无类型常量可以不用遵守,并且可以更自由地混合和匹配。无类型常量的默认类型只会在必要时且没有其他类型信息时被使用。


语法决定默认类型
--------


一个无类型常量的默认类型被它的语法所决定。对于字符串常量,默认类型只会是string。对于数字常量,默认类型有多种。整型常量默认是int,浮点型常量默认是float64,rune类型常量默认是rune(int32),虚数类型常量默认是comlex128。例子:

perl复制代码fmt.Printf(“%T %v\n”, 0, 0)
fmt.Printf(“%T %v\n”, 0.0, 0.0)
fmt.Printf(“%T %v\n”, ‘x’, ‘x’)
fmt.Printf(“%T %v\n”, 0i, 0i)

1
2

输出:

go复制代码int 0
float64 0
int32 120
complex128 (0+0i)

1
2
3
4
5
6

Booleans
--------


无类型boolean常量和无类型字符串常量是一样的。true和false是无类型boolean常量,他们可以被赋值给任何boolean变量,一旦确定了类型,就不能和boolean变量混合操作了:

ini复制代码type MyBool bool
const True = true
const TypedTrue bool = true
var m Mybool
mb = true // OK
mb = True // OK
mb = TypedTrue // Bad
fmt.Println(mb)

1
2
3
4
5
6

浮点类型
----


浮点常量和boolean常量有很多相同的地方:

go复制代码type MyFloat64 float64
const Zero = 0.0 // 无类型浮点常量
const TypedZero float64 = 0.0
var mf MyFloat64
mf = 0.0 // OK
mf = Zero // OK
mf = TypedZero // Bad
fmt.Println(mf)

1
2

唯一不同的地方是Go里面有两种浮点类型:float32和float64。浮点常量默认的类型是float64,但是可以把无类型浮点常量赋值给float32类型的变量:

rust复制代码var f32 float32
f32 = 0.0
f32 = Zero // OK: Zero is untyped
f32 = TypedZero // Bad: TypedZero is float64 not float32.
fmt.Println(f32)

1
2
3
4
5

我们来用浮点数介绍一下溢出的问题。


数字常量存储在一个任意精度的数字空间了,它们是常规的数字。但当他们被分配给一个变量时,大小必须在变量的类型所支持的范围内。我们可以给一个常量声明一个非常大的值:

ini复制代码const Huge = 1e1000

1
2

虽然我们声明了Huge这个常量,但是我们不能把Huge分配给其他变量,甚至无法打印出来。下面语句甚至不会编译:

scss复制代码fmt.Println(Huge)

1
2

会报错:"constant 1.00000e+1000 overflows float64"。但是Huge是可用的:我们可以在表达式里用它和其他常量进行运算,如果结果在存在float64的范围里,如下:

scss复制代码fmt.Println(Huge / 1e999)

1
2

输出:

复制代码10

1
2

另外,浮点常量会有更高的精读,所以用他们计算更加准确。在math包中定义的常量比float64类型的变量拥有更多的精度位。下面是math.Pi的定义:

ini复制代码Pi = 3.14159265358979323846264338327950288419716939937510582097494459

1
2

当Pi被分配给一个变量时,会有精读丢失;该变量会是float64或float32类型来尽可能接近原来的值。比如:

lua复制代码pi := math.Pi
fmt.Println(pi)

1
2

输出:

复制代码3.141592653589793

1
2
3
4
5
6
7
8
9

数字常量拥有更多的精度位,意味着在进入比如:Pi/2这样的计算中结果的精度很高,直到结果被分配给变量时精度才会丢失。并且在计算过程不会出现无穷大、溢出和NaN的问题。(如果表达式中有除以常量0的操作,在编译时会报错。


复数
--


复数常量也和浮点数常量类似。

go复制代码type MyComplex128 complex128
const I = (0.0 + 1.0i)
const TypedI complex128 = (0.0 + 1.0i)
var mc MyComplex128
mc = (0.0 + 1.0) // OK
mc = I // Ok
mc = TypedI // Bad
fmt.Println(mc)

1
2
3
4
5
6
7
8

复数常量的默认类型是complex128, 是由两个float64的值组成,有很大的精度。


声明一下,在例子中,我们写了完整的表达式(0.0+1.0i),其实我们可以简写成0.0+1.0i、1.0i或者1i。


我们知道在Go中,数字常量只是一个数字,如果一个复数没有虚数部分,它会是什么?实数吗?

go复制代码const Two = 2.0 + 0i

1
2

Two数一个无类型复数常量,即使它没有虚数部分,表达式的语法定义了它的默认类型是complex128。因此,如果我们用它去声明一个变量,默认类型将会是complex128。

perl复制代码s := Two
fmt.Printf(“%T: %v\n”, s, s)

1
2

输出:

go复制代码complex128: (2+0i)

1
2

在数值上,Two既可以被存储在float32空间里,也可以存储在float64空间里,不会发生精度丢失。我们在初始化或分配时把Two分配给float64是没问题的:

go复制代码var f float64
var g float64 = Two
f = Two
fmt.Printf(f, “and”, g)

1
2

输出:

复制代码2 and 2

1
2
3
4
5
6
7
8
9

即使Two是复数常量,也可以将其分配给浮点类型变量。这种跨界操作是很有用的。


整型
--


最后我们来到了整型。他们的种类更多,不同的大小、有符号或无符号等等,他们遵循一样的规则:

ini复制代码type MyInt int
const Three = 3
const TypedThree int = 3
var mi Myint
mi = 3 // OK
mi = Three // OK
mi = TypedThree // Bad
fmt.Println(mi)

1
2

上面的类型对所有的整数类型都适用,整数类型有:

go复制代码int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr

1
2
3
4
5
6
7
8

(还有uint8也叫byte, int32也叫rune)。整数类型有很多,但是在整数常量的工作方式都是相似的,现在我们来具体看一看。


如上所述,整型有两种形式,每种形式都有自己的默认类型:对于像123、0xFF、-14这些简单的常量,默认类型是int;像'a'、'世'、'\r'这些单引号单字符,默认类型是rune。


没有哪种常量形式的默认类型无符号整型。但是我们可以利用无类型常量的灵活性,初始化出无符号整型变量。这就和我们可以用虚部为0的复数初始化出float64变量一样。下面有几中初始化uint的方式,每种方法都是明确指定了类型为uint:

go复制代码var u uint = 17
var u = uint(17)
u := uint(17)

1
2

整型也有类型浮点数那样数值范围的问题,不同整型之间的转换、分配可能会出现问题。可能会有两个问题发生:大范围的值转换为小范围的值;负数分配到无符号整型上。例如:int8的范围是-128到127,所以如果常量超出这个范围将不能被分配到一个int8类型的变量上:

go复制代码var i8 int8 = 128 // Error: too large.

1
2

同样,uint8(或者叫byte)的范围是0到255,不在这个范围中的常量也不能被分配到一个uint8变量上:

go复制代码var u8 uint8 = -1 // Error: negative value.

1
2

下面的代码也会被类型检查捕捉到错误:

go复制代码type Char byte
var c Char = ‘世’ // Error: ‘世’ has value 0x4e16, too large

1
2
3
4
5
6
7
8
9

如果编译器检查出常量的用法错误,真实太悲伤了。


练习:最大的无符号整型
-----------


这里有一个有意思的小练习。我们如何用常量表示uint中的最大值呢?如果我们是说uint32,我们可以这个样写:

ini复制代码const MaxUint32 = 1<<32 - 1

1
2
3
4
5
6
7
8

但是我们要的是uint,而不是uint32。int和uint类型没有确定的bit位数,32位或者64位。因为具体的bit位数取决架构,我们不能写一个简单的值。


...省略...


最简单的uint值是有类型常量uint(0),如果uint是32位,那么uint(32)就用32位0bit,反之uint是64位,那么uint(0)就有64位0bit。如果我们将这些bit位倒置成1,我们将会得到uint的最大值:

csharp复制代码const MaxUint = ^uint(0)
fmt.Printf(“%x\n”, MaxUint)

1
2
3
4
5
6
7
8
9

无论当前环境uint32的bit位是多少,这个常量都正确表示uint变量能承载的最大值。


数字
--


在Go中无类型常量的概念表示所有的数字常量,无论整型、浮点型、复数型还是字符值,都存储在一种统一的空间里。我们将它们作用在变量、复制和运算中时,类型才会有意义。只要我们在数字常量的世界里,我们就可以随意混合不同的类型进行运算,下面所有的常量的数值都是1:

go复制代码1
1.000
1e3-99.0*10-9
‘\x01’
‘\u0001’
‘b’ - ‘a’
1.0+3i-3.0i

1
2

因此,即使他们有不同的隐含默认类型,写做无类型的常量可以被分配任何整型变量:

go复制代码var f float32 = 1
var i int = 1.000
var u uint32 = 1e3 - 99.0*10.0 - 9
var c float64 = ‘\x01’
var p uintptr = ‘\u0001’
var r complex64 = ‘b’ - ‘a’
var b byte = 1.0 + 3i - 3.0i

fmt.Println(f, i, u, c, p, r, b)

1
2

输出:

go复制代码1 1 1 1 1 (1+0i) 1

1
2

你甚至可以这样写:

ini复制代码var f = ‘a’ * 1.5
fmt.Println(f)

1
2

输出:

复制代码145.5

1
2

尽管在Go中同一个表达式中混合使用浮点数和整数变量或者int和int32变量是不合法的,但是这种灵活性让下面的写法可行,即不同类型的数字常量是可以混合的:

css复制代码sqrt2 := math.Sqrt(2)

1
2

或者

ini复制代码const millisecond = time.Second/1e3

1
2

或者

go复制代码bigBufferWithHeader := make([]byte, 512+1e6


结果也是你期望的那样。


因为在Go里,数字常量的工作方式就像普通的数字一样。



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

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

性能优化反思:不要在for循环中操作DB 进阶版 参与互动

发表于 2021-11-04

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

如何提高程序运行速度,减轻服务器压力是服务端开发必须面对的一个问题。

简单且朴素的原则:不要在for循环中操作DB,包括关系型数据库和NoSql。

我们应该根据自己的业务场景,在for循环之前批量拿到数据,用尽量少的sql查询批量查到结果。
在for循环中进行数据的匹配组装。

上一篇文章 性能优化反思:不要在for循环中操作DB ,被推荐到首页也收到了大家的互动评论,再接再厉,进阶一版。

说明:继续上一篇文档的demo整理,不赘述重复代码了,建议大家先读上一篇内容。

场景说明

  1. 我们允许用户选择职业,系统预制了一批职业标签;又开放了自定义职业标签的功能,不限制自定义标签的次数。允许用户编辑资料时选择2个职业标签。
  2. 发现用户自定义的职业真的五花八门,随着业务增长,数量级越来越大;比如目前职业标签是2千个,以后可能有2万个,甚至20万个。
  3. 这种情况下,我们上一篇提到的在for循环之前批量查询全量数据,在for循环中用自定义函数匹配,避免在for循环中操作DB的方式命中率太低了,造成了极大的浪费。
  4. 比如:每个列表返回30个用户信息,每个用户选择了2个职业标签,最大标签数量是60;而我全量查到的职业标签数量是2千,命中率只有3%;如果职业标签达到2万个,命中率就只有0.3%了。

解题思路

  1. 首先,在for循环中不操作DB,这个大原则不变
  2. 上述问题的核心是命中率太低,就是全量查了很多用不到的数据
  3. 解决思路就是只批量查询命中的标签数据:
    1. 取到30个用户在user表中保存的职业id
    2. 30个用户的id去重后重组
    3. 在职业表通过whereIn查询匹配的职业标签
    4. 其他逻辑不变,替换的只是数据源:之前的数据源是全量数据,优化后的数据源是精准命中的数据。

思路清晰之后,开始coding

代码示例:

为了行文紧凑,代码段中省略了和文章无关的代码,用竖着的三个.省略。

  1. 核心代码:抽取 renderUserInfo ,统一输出用户信息,这个函数在for循环中调用,获得数据源在for循环之前。
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
php复制代码<?php

namespace App\Render;

.
.
.

class CommonRender extends BaseRender
{
public static function renderUserinfo($data, $hobbyInfo = [],$professionInfo = [])
{
$hobbyInfo = !empty($hobbyInfo) ? $hobbyInfo : HobbyInfo::getAllInfo();
//特殊处理,因为职业用户可以自定义 数字一直增长 不全量查数据;$professionInfo为空时不是批量查询,只查单条记录
$professionInfo = !empty($professionInfo) ? $professionInfo : (isset($data['profession']) ? ProfessionInfo::getByIds($data['profession']) : []);

if (!is_array($data)) {
return [];
}

$ret = [
.
.
.

//优化之前
// 'hobby' => !isset($data['hobby']) ? [] : HobbyInfo::getByIds($data['hobby']),
// 'profession' => !isset($data['profession']) ? [] : ProfessionInfo::getByIds($data['profession']),


//优化之后
'hobby' => !isset($data['hobby']) ? [] : self::_renderHobby($data['hobby'], $hobbyInfo),
'profession' => !isset($data['profession']) ? [] : self::_renderProfession($data['profession'], $professionInfo),

.
.
.

return $ret;
}
}
  1. isset() 判断,避免传入的数据不存在,提示数组越界。

我还整理了一篇 如何避免数组下标越界 ,有兴趣可以阅读一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码protected static function _renderProfession($userProfession, $professionInfo)
{
$ret = [];
if ($userProfession) {
$userProfessionIds = explode(',', $userProfession);
foreach ($userProfessionIds as $key => $userProfessionId) {
if (isset($professionInfo[$userProfessionId])) {
$ret[$key] = $professionInfo[$userProfessionId];
}
}
}
return $ret;
}
  1. 调用 commonRender() 的代码,即展示数据源是怎么来的。
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
php复制代码public static function getBatchUserIntro($userid, $userList)
{
$retData = [];
if (empty($userList)) {
return $retData;
}

.
.
.

$hobbyInfo = HobbyInfo::getAllInfo();

//按需批量查职业,不全量查询职业
$professionIds = array_column($batchUserInfo, 'profession');
$professionIds = implode(',', $professionIds);
$professionIds = array_unique(explode(',', $professionIds));
$professionInfo = ProfessionInfo::batchGetByIds($professionIds);

foreach ($batchUserInfo as $item) {
$retData[$item['userid']] = CommonRender::renderUserinfo($item, $hobbyInfo, $professionInfo, $expectInfo);
}

return $retData;
}
  1. 封装的工具方法,通过id数组批量获得数据,做了特殊判断,兼容值为空的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
php复制代码public static function batchGetByIds($ids = [])
{
//兼容职业为空的情况
foreach ($ids as $key => $id) {
if (empty($id)) {
unset($ids[$key]);
}
}

if (empty($ids)) {
return [];
}

return self::query()->selectRaw('id,name,pid')
->whereIn('id', $ids)
->get()
->keyBy('id')
->toArray();
}

核心代码就是上述4部分

性能对比

以此举例:每次列表返回30个用户信息,每个用户选择了2个职业标签,最大标签数量是60;

优化之前:全量查到的职业标签数量为2千,命中率只有3%;如果职业标签达到2万个,命中率就只有0.3%了。

优化之后:全量查到的职业标签数量为2千,命中率为100%;如果职业标签达到2万个,命中率仍然为100%。

反思总结

程序设计一定要结合业务场景,没有绝对正确的程序设计;

随着业务增长原本稳健的程序设计也可能遇到问题,技术人必须能和业务一起成长。

参与互动

大佬们有啥好的方案欢迎在评论区指教

本文转载自: 掘金

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

不会用Redis做分布式流水号? Redis生成分布式流水号

发表于 2021-11-04

Redis生成分布式流水号

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

引言

最近做项目,需要做单据编号,格式固定为:单据类型固定前缀+年月日时间戳+4位流水号,要求是每个单据类型的流水号唯一,方便后续业务使用。之前项目中使用的是UUID作为其他业务的单据编号,和组长沟通了一下,项目中有使用Redis,正好使用Redis做更便捷并能解决分布式部署单号唯一问题。

分布式Id也可以用相同的方式处理

思路

例子:以特别单据为例,今天是2021年11月4日,今天的第一个单据的单据编号应该为:S202111040001

通过Redis实现的话,则需要以my-test:no:S20211104作为Redis缓存的key,1作为value做存储,同时每次获取一个value就让其自增计数,同时每次取key的时候更新失效时间,保证如果key自动失效,Redis中不会有大量的key堆积。

Redis实现

Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。

  • 优点:不依赖于数据库,灵活方便,且性能优于数据库。
    根据业务特点,需要将Redis的key封装,单据类型封装,为了后续做扩展,将单据编号总长度,流水号长度等也做了封装。下面是代码实现:

常量类

常量中保存的是Redis的缓存key前缀,以及年月日时间戳的前缀格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* @className: FormNoConstant
* @description: 单号生成常量
* @author: hanHang
* @create: 2021/9/28 9:41
**/
public class FormNoConstant {
/**
* 单号流水号缓存Key前缀
*/
public static final String SERIAL_CACHE_PREFIX = "my-test:no:";
/**
* 单号流水号yyyyMMdd前缀
*/
public static final String SERIAL_YYYY_MM_DD_PREFIX = "yyyyMMdd";
}

枚举

枚举中保存的是单据类型前缀、日期时间戳格式化表达式、流水号长度、单据编号总长度等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码/**
* @className: FormNoTypeEnum
* @description: 编号生成类型枚举
* @author: hanHang
* @create: 2021/9/28 9:40
**/
@Getter
public enum FormNoTypeEnum {
/**
* 特别单据单号:
*/
SPECIAL_ORDER("S", FormNoConstants.SERIAL_YYYY_MM_DD_PREFIX, 3 , 12),

/**
* 正常单据单号:
*/
NORMAL_ORDER("N", FormNoConstants.SERIAL_YYYY_MM_DD_PREFIX, 3 , 12);

/**
* 单号前缀 为空时填""
*/
private final String prefix;

/**
* 时间格式表达式
* 例如:yyyyMMdd
*/
private final String datePattern;

/**
* 流水号长度
*/
private final Integer serialLength;

/**
* 总长度
*/
private final Integer totalLength;


FormNoTypeEnum(String prefix, String datePattern, Integer serialLength, Integer totalLength) {
this.prefix = prefix;
this.datePattern = datePattern;
this.serialLength = serialLength;
this.totalLength = totalLength;
}
}

工具类

工具类中包含了获取单号前缀、根据单号前缀获取缓存key、补全流水号生成完整流水号方法。

getCacheKey(String serialPrefix):此方法是从Redis中获取对应单据类型-日期的key

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
java复制代码/**
* @className: FormNoSerialUtil
* @description: 单据编号工具类
* @author: hanHang
* @create: 2021/9/28 9:45
**/
public class FormNoSerialUtil {
/**
* 根据单据类型获取单号前缀
* @param formNoTypeEnum
* @return 单号前缀
*/
public static String getFormNoPrefix(FormNoTypeEnum formNoTypeEnum) {
//格式化时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formNoTypeEnum.getDatePattern());
return formNoTypeEnum.getPrefix() +
formatter.format(LocalDateTime.now());
}

/**
* 获取流水号缓存Key
* @param serialPrefix 流水号前缀
* @return 流水号缓存Key
*/
public static String getCacheKey(String serialPrefix) {
return FormNoConstants.SERIAL_CACHE_PREFIX.concat(serialPrefix);
}

/**
* 补全流水号
*
* @param serialPrefix 单号前缀
* @param incrementalSerial 当天自增流水号
* @param formNoTypeEnum 单据类型枚举
* @return 完整流水号
*/
public static String completionSerial(String serialPrefix, Long incrementalSerial,
FormNoTypeEnum formNoTypeEnum) {
StringBuffer sb = new StringBuffer(serialPrefix);

//需要补0的长度=流水号长度 -当日自增计数长度
int length = formNoTypeEnum.getSerialLength() - String.valueOf(incrementalSerial).length();
//补零
for (int i = 0; i < length; i++) {
sb.append("0");
}
//redis当日自增数
sb.append(incrementalSerial);
return sb.toString();
}
}

Component

利用Component注解将此类交给Spring代理,暴露generateFormNo方法,根据单据类型枚举获得缓存key,在Redis中获取key对应的value,每次获取并自增1。

需要注意:如果是当天第一次调用,value取出来的是0,需要再进行一次自增,因为需要从1开始计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
java复制代码/**
* @className: FormNoGenerateComponent
* @description: 单号生成
* @author: hanHang
* @create: 2021/9/28 9:52
**/
@Component
public class FormNoGenerateComponent {
@Resource
RedisTemplate<String, Object> redisTemplate;

/**
* 根据单据编号类型 生成单据编号
* @param formNoTypeEnum 单据编号类型
* @return 单据编号
*/
public String generateFormNo(FormNoTypeEnum formNoTypeEnum) {
//获得单号前缀 格式 固定前缀 +时间前缀 示例 :S20211104
String formNoPrefix = FormNoSerialUtil.getFormNoPrefix(formNoTypeEnum);
//获得缓存key
String cacheKey = FormNoSerialUtil.getCacheKey(formNoPrefix);

Long incrementalSerial = getIncr(cacheKey,getCurrent2TodayEndMillisTime());
if (incrementalSerial==0){
incrementalSerial = getIncr(cacheKey,getCurrent2TodayEndMillisTime());
}

//组合单号并补全流水号
return FormNoSerialUtil
.completionSerial(formNoPrefix, incrementalSerial, formNoTypeEnum);
}

/**
* 获得当日自增数
* @param key key
* @param liveTime 存活时间
* @return 当日自增数
*/
private Long getIncr(String key, long liveTime) {
RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, Objects.requireNonNull(redisTemplate.getConnectionFactory()));
long increment = entityIdCounter.getAndIncrement();

//初始设置过期时间
if (increment == 0 && liveTime > 0) {
entityIdCounter.expire(liveTime, TimeUnit.MILLISECONDS);
}
return increment;
}

/**
* @return 现在到今天结束的毫秒数
*/
private Long getCurrent2TodayEndMillisTime() {
Calendar todayEnd = Calendar.getInstance();
todayEnd.set(Calendar.HOUR_OF_DAY, 23);
todayEnd.set(Calendar.MINUTE, 59);
todayEnd.set(Calendar.SECOND, 59);
todayEnd.set(Calendar.MILLISECOND, 999);
return todayEnd.getTimeInMillis() - System.currentTimeMillis();
}
}

总结

使用Redis做分布式编号是目前工作中用到场景比较多的,也可以使用这种方法做分布式唯一主键,但是如果业务处理失败,此编号就被占用,会有一天之内的数据编号有断的场景,我没有想到很好的解决办法,需要大佬指教。

还有面试中有被问到用雪花算法处理分布式唯一主键的,还没有研究过,等研究好了,再写一篇文章。

本文转载自: 掘金

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

拒做职场小白?计算机关键概念你不得不掌握!

发表于 2021-11-04

开始卷起来了!!!💪 为让学习更有趣,这篇文章我会列出计算机科学理论和一些概念,并且用类比的方式和尽量少的技术术语来为你进行解释。这样做的目的就是为了让你快速了解计算机,查漏补缺。

👋如果对这些概念有任何解释不妥之处,请及时指正我。

  1. 核心概念:数据结构和算法

👋👋一个很酷的算法视频

1.1 递归

比如你坐在电影院正准备好看电影的时候,这时候刚来电影院的人问你坐的是第几排,你懒的数就问前面的人,“哥们,你那一排是第几排?”你只需要从对方口中得知他的行号 + 1 就是你坐的行号,但是你前面那哥们也做了同样的事情,他也问了他前面的人。。。。。。依此类推,一直问到第一排,他回答:“我这是第一排!” 然后从现在开始,正确的行号将会一直 + 1 直到传达给刚进电影院的那哥们。

千万别在电影院这么做,否则第二排或者第三排的人可能会被第一排的当成傻子。。。。。。因为第二排的人问第一排的人:“哥们,你坐第几排?” 想象一下这个场景。。。。。。

我给你几幅图,让你理解一下什么是递归。

image-20211020221644910

1.2 大数据

假设你有一个花园,但是你花园中的水管漏水,你需要拿一些桶和密封材料来解决这个问题,但是过了一会儿,你发现实际泄漏的要大的多,你需要水管工拿更多的工具来处理,同时,你仍在使用水桶排水。 过了一会儿,你发现地下有一条巨大的地下溪流已经打开。你需要每秒处理数加仑的水。

这时候桶就没用了,你需要一种全新的方法来解决这个问题,因为水的体积和速度都在增加。为了防止城镇发生洪水,你需要 zf 建造一座大型水坝,然而这需要大量的土木工程专业知识和复杂的控制系统。

大数据描述了使用传统数据处理工具无法管理的庞大而复杂的数据集。

1.3 数据结构

关于数据结构,每位程序员都应该知道

  • 数组 en.wikipedia.org/wiki/Array_…
  • 树 en.wikipedia.org/wiki/Tree_%…
  • 栈 en.wikipedia.org/wiki/Stack_…
  • 队列 en.wikipedia.org/wiki/Queue_…
  • 图 en.wikipedia.org/wiki/Graph_…
  • 哈希表 en.wikipedia.org/wiki/Hash_t…
  • 链表 en.wikipedia.org/wiki/Linked…
  • 堆 en.wikipedia.org/wiki/Heap_%…
  1. 核心概念 AI

2.1 贪心算法

想象一下,你要去徒步旅行,而你的目标是尽可能到达最高峰,在开始之前你已经有了地图,但是地图上显示了成千上万条路,但是你无法评估每一条,所以扔掉了地图,你从一个看起来很简单的路开始走,这种方式就是根据感性来选出来的,是一种贪婪和短视的表现,你选择只走最倾斜向上的路线。

但是旅行结束后,全身酸痛,你查看地图却发现旁边有一条泥泞的小河,跨过去就好了,而不用一直向上走。

image-20211022213807138

贪心算法会选择当下的最佳路线,而不是重新开始考虑评估选择。

2.2 爬山算法

这次你要爬另一座山,你决定要找到能够带你到达最高峰的那条路。但是很不幸,你的地图丢了,而且山上雾气很大,那怎么办呢?所以你为了让旅途更轻松,你下载了一个 app ,这个 app 能够跟踪你走过的路测量你当前的高度。于是你每次走过的路线都是能够把你带到最高峰的路线,但是在中途,你选择了一条另外的路线,而这个路线也能把你带到最高峰。

2.3 模拟退火

在你前面的是珠穆朗玛峰。

image-20211022224048884

这是你所面临的最大的挑战。你的目标是登顶,但是一遍又一遍爬珠峰是不切实际的,所以你只有一次机会。你现在非常谨慎,你不会总是向上爬,而是偶尔移动到一个比较低的点来探索其他可能的路径,以减少出错的机会,你爬的越高,你移动到较低点进行探索的可能性就越低。

2.4 动态规划

父亲:在一张纸上写下 “1+1+1+1+1+1+1+1 =”。

父亲:那等于什么?

儿子:三秒后数了数等于 8 。

父亲:在左侧写下另一个“ + 1”。

父亲:现在等于多少?

儿子:立刻确定了是 9 !

父亲:现在为何你数的这么快?

儿子:因为你刚刚才加了一个!

父亲:所以你不需要重新数,而是记住了刚刚那个数字等于 8 !

2.5 P 和 NP 的问题

P vs NP 是计算机科学领域中最流行和最重要的未解决问题之一。

假设我给你一个乘法问题,例如:

Q1:P = 7 * 17

答案是119 。这个很容易算吧?如果我颠倒这个问题怎么办:

Q2:P * Q = 119 (P 和 Q 都不能是 1 或者 119)

如果你没有看到 Q1,你要解决 Q2 ,你可能会从 2 开始尝试,一直遍历到 118 为止。所以我们还没有一种找到一个数因数的高效算法。

如果我问你,P 可能是 7 吗?这时候你可以轻松验证答案吗?你当然可以,只需要将 119 / 7 就可以了。

乘法很容易,但是找到一个数的原始因数却很难。

所以 Q1 是一个 P(多项式) 问题,它很容易得到解决,因为计算机可以轻松的将任意两个数相乘。

但是 Q2 是一个 NP(非多项式) 问题,它很难解决,找到 119 的因数对计算机来说还是很容易解决的,但是 500 位数字呢?现在对任何计算机都是不可能的。

这是这个问题的最重要的部分:NP 问题(分解)是否也是 P(乘法),只是我们还没有发现解决 NP 问题的有效方法?还是人类太笨了?想象一下,存在比人类智能得多的机器或生命。他们看我们就像我们看蚂蚁一样。我们的智力水平对他们来说太微不足道了。解决 P vs NP 问题就像为他们解决 1 + 1!

那么为什么 P vs NP 问题很重要呢?如果我们能够证明 P=NP,则意味着所有 NP 问题都可以在合理的计算机时间内轻松解决。我们将能够治愈癌症(蛋白质折叠)、破解密码(RSA)等。这将会改变世界。

  1. 核心概念:并发

前景提要:假设你正在某公司担任秘书,你所做的工作包括接打电话、安排会议、写文件等,你总是需要根据任务的优先级来停下手头的工作转而做其他的(工作),每次电话响起时,你都需要停止正在处理的工作。

3.1 并行

随着任务越积越多,你无法应对你的工作了,因为有太多的书写任务,你向老板抱怨,他愿意再雇个人来帮你分担书写任务。

并行允许有两个或者更多的任务同时运行,但是前提是你的 CPU 能够支持多处理能力,人只有一个 CPU ,所以一心不能二用。这也并不是一件坏事,有的时候单线程的工作效率反而更高,而中断(学习的时候被打断)是工作的天敌,这种开销实在是太大了。

所以你和新招的那个雇员一起分担书写任务就是一种并行。

并发概念的引入也引发了很多很多问题,比如下面的竞态条件。

3.2 竞态条件

这里就不得不提我们大家都耳熟能详的银行转账这个例子了:

  • 你的银行账户里有 1000 元。
  • 有人转给你 500 元,而你从 ATM 中提取了 300 元。
  • 假设这两笔交易同时进行,两笔交易都会看到你当前的余额是 1000 元(注意这是两笔交易,这两笔交易之前彼此不可见,这点很重要)。
  • 现在,第一笔交易给你的账户增加了 500 元,你现在就有了 1500 元。但是,第二笔交易看到的你的账户余额还是 1000 元,他从 1000 元中扣除了 300 ,变成了 700 元。
  • 所以你现在的账户余额会变为 700 元,而不是 1200 元,因为第二笔交易覆盖了第一笔交易。
  • 发生这种情况是因为银行系统不知道其他正在进行的交易。

那么,你应该如何处理这种情况呢?下面是几种解决方式。

3.3 互斥

现在我们采用这种方式:只要有正在进行的交易,系统就会锁定交易中涉及的账户。而且系统会认定转账开始 -> 转账完成是一个完整的事务周期。

所以这一次,当第一笔交易发生的那一刻,你的账户会被锁定,你不能再从你的账户中取钱,直到第一笔交易完成为止。

所以互斥是不是就解决问题了?

虽然互斥能解决系统数据安全性的问题,但是却不符合人的感性认知。但没有人希望每次有正在进行的交易时都被 ATM 拒绝。

所以我们需要修改一下方案。

3.4 信号量

二进制信号量

现在我们需要为不同类型的交易设置不同的优先级。假如提现请求的优先级要高于银行转账的优先级。所以当你从 ATM 中取款时,这笔交易的优先级要大于向你账户转账的优先级,所以此时转账的的交易会暂停,而提现的事务优先进行,因为它具有更高的优先级,等到提现成功后,转账事务再恢复。

二进制信号量很简单,1 = 正在进行的交易, 0 = 等待。

计数信号量

计数信号量允许多个进程同时运行。

假设你是一家游泳馆的工作人员,有 10 个储物柜,每个储物柜都有一把钥匙,每次收到或者分发钥匙时,你都需要把控所有的钥匙数量,如果所有储物柜都满了,其他人必须排队。 每当有人完成时,他会将钥匙交给队列中的第一个人。

3.5 死锁

死锁是另外一种并发中的共性问题。

还是让我们拿银行转账来举例子。请记住,只要有正在进行的事务,就会锁定对银行帐户的访问。

  • 假如你向张阿姨转账 500 元(来让张阿姨高兴一下),同时张阿姨向你转账 1000 元(让你补补身子)。
  • 于是事务 A 锁定了张阿姨的账户,并且从她的账户里扣除了 1000 元。
  • 事务B 锁定你的账户,并且从你的账户里扣除了 500 元。
  • 然后,事务 A 尝试访问你的账户来添加她给你转账的 1000 元。
  • 同时,事务 B 尝试访问张阿姨的账户来添加你给她转账的 500 元。

但是,由于两项交易都没有完成,因此两个事务都无法访问锁定的帐户,造成两个事务都在等待对方事务的结束,这种情况就是死锁。

还有为什么谈恋爱的过程中男生要主动一些,如果你不主动,可能就死局(死锁)了。

  1. 核心概念:计算机安全

4.1 计算机黑客

计算机黑客类似于强制闯入你的房子里,下面有几种流行的黑客技术。

暴力攻击

这种做法就是尝试使用成百上千种不同的密码。有经验的黑客会先尝试最常用的密码。

暴力攻击会尝试所有可能的密码,通常首先猜测常用的密码,如“123456”、“abcdef”等。

我之前尝试过用 kali linux 配上专用的设备跑过包破解过邻居家的 WI-FI ,结果密码只是 000000 。

社会工程

这是一项强大的技术!!!我愿称之为网络 Pua!!!

一对夫妇刚搬进隔壁。他们真的很好,乐于助人。他们经常请你吃晚饭。有一天,你说你很快就会去度假两周。 他们高兴地提出要照顾你的狗。你给他们留了一把备用钥匙。从那以后,你再也没有听到关于他们的任何消息。

社会工程学会诱骗用户透露他们的私人信息。

此处应该要有个配图。

image-20211023120448543

安全漏洞

安全漏洞就是黑客入侵你家能找到的缺口,比如你可能没关窗户。

特洛伊木马

黑客冒充水管工,让你帮他开门,他修理了你漏水的水管,但是他走后,你发现你的首饰不见了。

特洛伊木马是假装有用并在后台运行恶意代码的恶意软件程序。

RootKit

你的门锁被卡住了,你打电话给锁匠。他修理了你的门锁并偷偷复制了另一把钥匙。

Rootkit 通过社会工程等多种方式获得计算机管理员或 root 访问权限,然后伪装成杀毒软件难以检测到的必要文件。

DDos

这是一个书店的比喻。

假如你有一个书店,书店里面能容纳 200 多人,某一天突然有 200 多人进入你的书店,你的书店满了,而且你不能赶走他们,因为他们看起来互相都不认识,而且他们似乎真的都有兴趣买书,甚至有人问你 xxx 书在哪里,然而柜台上只有一个人付了几块钱。

人们不断进出几个小时,但是你卖的书还不到 5 本,你觉得你这个书店还能撑多久?

DDoS 试图通过大量访问者来关闭站点或服务。

4.2 密码学

对称加密

假设老王和老刘想要互相发送东西。为了确保没有人可以看到他们的东西,他们用一个盒子锁起来。他们为锁制作了 2 个相同(对称)的钥匙,并事先见面共享钥匙。

非对称加密

在两个人之间共享密钥能够正常工作,但是如果老王想和老赵交换东西,而且还不想让老刘知道该怎么办?重新共享一个全新的钥匙吗?那假如老王想和 10 个不同的人交换东西该怎么办?难道要消灭掉解决问题的老王吗?

所以老王想出了一个绝对fashion的主意:现在老王只维护一把钥匙(私钥),她将另一把钥匙(公钥)分给他的朋友们,任何人都能够加密,但是只有她有打开(解密)的钥匙,现在,任何人都能够使用她发的公钥进行加密,而且老王不需要再为任何人管理不同的钥匙。

  1. 核心概念:软件开发方法

5.1 瀑布开发方式

你需要弄清楚所有的事情并记录下来,就向是瀑布一样,除非重新开始,否则无法返回。只有当前阶段完成后,你才能进入下一阶段。

image-20211023195137609

5.2 敏捷开发

这是国内大多数软件公司采用的软件开发方式 — 敏捷开发。

一开始你就弄清楚一些你需要坐的事情,然后随着开发的进行,不断改进、发展协作和适应。

就像大多数产品经理或者你的领导说的:先弄一版出来再说。

5.3 现实世界中的软件开发

现在你毕业了。你编写了你认为比较漂亮的代码,一切都很完美。现在我给你介绍一种 牛仔编码,这是大学教授没有教你的开发方法。

你可能想知道为什么你不会评估开发事件,请看下图

image-20211023200236613

总结

这篇文章我给你整理了一些计算机科学中的一些常见概念,并且用生动形象的例子来为你阐述这些概念,希望能降低你学这些内容的门槛。

还是那句话,如果你想要完整的理解这些概念,去看相关书籍或者论文。

最后给大家推荐一下我自己的 Github ,里面有非常多的硬核文章,绝对会对你有帮助。

本文转载自: 掘金

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

1…422423424…956

开发者博客

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