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

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


  • 首页

  • 归档

  • 搜索

SpringBoot基础之集成MybatisPlus

发表于 2021-11-17

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

前言

对于Java系中,最著名的ORM框架也就是Mybatis,JPA,Hibernate了,在面对领导开发快速迭代的场景想,大部分的项目都是基于SSM的,这个M也就是Mybatis.

Mybatis简单灵活从另一个角度就是需要自己写所有的代码(简单的可以使用代码生成器),MybatisPlus就是Mybatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。

Mybatis-Plus官网在这里

集成

这里的连接池是用的默认的HikariCP连接池,想用Druid或者只用Mybatis可以看另一篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<!--mysql连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!--Mbatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>

<!--Mbatis Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>

application.yml配置

数据库和连接池配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #mysql 8.0
url: jdbc:mysql:///zdc_test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=GMT%2B8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123123
type: com.zaxxer.hikari.HikariDataSource #当前使用的数据源 Hikari
hikari:
minimum-idle: 1 # 池中维护的最小空闲连接数 默认10 根据实际情况来
maximum-pool-size: 10 # 池中最大连接数 根据实际情况来
auto-commit: true # 自动提交从池中返回的连接
idle-timeout: 600000 # 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟
max-lifetime: 1800000 # 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL
connection-timeout: 30000 # 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒
connection-test-query: select 1
read-only: false # 是否是只读

MyBatis配置

mybatisPlus 配置 (官方文档:mybatis中的配置 在mybatisplus上均可配置)

1
2
3
4
5
6
7
8
yaml复制代码mybatis-plus:
mapper-locations: classpath:/mappers/*Mapper.xml
type-aliases-package: zdc.enterprise.entity
configuration:
use-generated-keys: true
global-config:
db-config:
id-type: auto # 主键自增

必要的注解配置

在Application启动类上添加相关的注解

1
2
3
4
5
6
7
8
9
10
less复制代码@ComponentScan({"zdc.enterprise.*"})
@EnableTransactionManagement
@MapperScan("zdc.enterprise.mapper")
@SpringBootApplication
public class SpringBootEnterpriseApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootEnterpriseApplication.class, args);
}
}

@EnableTransactionManagement是开启事务管理

@MapperScan是配置dao层接口文件的扫描路径,也就是和xml对应的XXMapper或者XXDao的路径

使用

项目目录结构

1637069297(1).png

(1) 在数据库中创建一个student表

1
2
3
4
5
6
7
8
9
10
less复制代码CREATE TABLE `student` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`info` varchar(255) COMMENT 'zouzdc',
`del_flag` tinyint(1) DEFAULT '0',
`create_time` datetime,
`create_by` varchar(255),
`update_time` datetime ,
`update_by` varchar(255),
PRIMARY KEY (`id`)
) ;

(2) 在entity包下创建Student.java类

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
ruby复制代码@Data
@NoArgsConstructor
public class Student {

/**
* id
*/
@TableId
private Long id;

/**
* 其他信息
*/
private String info;

/**
* 是否伪删除 0否 1是
*/
@TableLogic
private String delFlag;

/**
* 创建日期
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;

/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;

/**
* 更新日期
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;

/**
* 更新人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
}

(3) controller层

studentService自带的方法演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
less复制代码@RestController
@RequestMapping("/student")
public class StudentController {

@Autowired(required = false)
public StudentService studentService;

@GetMapping("getById")
public R getStudentById( Student student){
//增
studentService.save(student);
//改
studentService.updateById(student);
//查
Student student = studentService.getById(student.getId());
//删
studentService.removeById(student.getId());
//列表
List<Student> zouzdc = studentService.lambdaQuery().eq(Student::getInfo, "zouzdc").list();
return R.success();
}
}

(4) service层,需要继承IService或实现ServiceImpl通用接口

service接口StudentService.java

1
2
csharp复制代码public interface StudentService extends IService<Student> {
}

service实现类StudentService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala复制代码@Service
@Slf4j
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {

//可以不声明studentMapper,当前类中的baseMapper就是实际上的studentMapper
@Autowired(required = false)
private StudentMapper studentMapper;

/**
*使用MybatisPlus的默认方法
*/
public void savePlus(Student vo) {
Student one = this.getById(vo.getId());
Student student = baseMapper.selectById(vo.getId());
}
}

(4) 在Mapper层需要继承BaseMapper接口

1
2
csharp复制代码public interface StudentMapper extends BaseMapper<Student> {
}

(5)在resources的mappers文件下创建StudentMapper.xml文件,和原生一样

1
2
3
4
5
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="zdc.enterprise.mapper.StudentMapper">

</mapper>

备注

根据项目需要权衡是否移动要使用原生的MyBatis

在使用MyBatis或者使用MyBatis-Plus的时候一定要配合代码生成器使用,网上很多开源的代码生成器,也可以自己写代码模版,提高效率

如果使用IDEA的话 推荐使用free mybatis plugin或者MybatisX插件, 能将接口类和mapper文件自动关联,并可以直接跳转

更详细的使用方式看下一篇文章

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

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

【代码审计】SQL 注入 0x01 JDBC 拼接不当造成

发表于 2021-11-17

0x01 JDBC 拼接不当造成 SQL 注入

JDBC 有两种方法执行 SQL 语句,分别为 PrepareStatement 和 Statement,两个方法的区别在于 PrepareStatement 会对 SQL 语句进行预编译,而 Statement 在每次执行时都需要编译,会增大系统开销。

理论上 PrepareStatement 的效率和安全性会比 Statement 好,但不意味着就不会存在问题。

以下是一个使用 Statement 执行 SQL 语句的示例

1
2
3
4
5
6
7
java复制代码String sql = "select * from user where id ="+req.getParameter("id");
PrintWriter out = resp.getWriter();
out.println("Statement Demo");
out.println("SQL: "+sql);
try {
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);

这里如果输入的 id 为 1 or 1 = 2,那么 SQL 语句就会被拼接为 select * from user where id = 1 or 1 = 2,改变了想要查询 id = 1 的语义。

PreqareStatement 方法支持使用 ? 对变量位进行占位,在预编译阶段填入相应的值会构造出完整的 SQL 语句,从而避免 SQL 注入的产生。

但开发有时为了便利,会直接采取拼接的方式构造 SQL 语句,这样一来依然会存在 SQL 注入,如下代码所示。

1
2
3
4
5
6
7
8
java复制代码String sql = "select * from user where id ="+req.getParameter("id");

PrintWriter out = resp.getWriter();
out.println("prepareStatement Demo");
out.println("SQL: "+sql);
try {
PreparedStatement pst = conn.prepareStatement(sql);
ResultSet rs = pst.executeQuery();

此时如果使用 or 1 = 1 仍然可以判断存在 SQL 注入,但是如果使用 ? 作为占位符,填入的字段的值就会进行严格的类型检查,就可以有效的避免 SQL 注入的产生,如下代码所示。

1
2
3
4
5
6
7
8
9
10
java复制代码PrintWriter out = resp.getWriter();
out.println("prepareStatement Demo");
String sql = "select * from user where id = ?";
out.println(sql);
try {
PreparedStatement pstt = conn.prepareStatement(sql);
// 参数已经强制要求是整型
pstt.setInt(1, Integer.parseInt(req.getParameter("id")));
ResultSet rs = pstt.executeQuery();
while (rs.next()){

0x02 框架使用不当造成 SQL 注入

通常框架底层已经实现了对 SQL 注入的防御,但是如果在开发未能恰当的使用框架的情况下,依然会存在 SQL 注入的风险。

1、MyBatis 框架

MyBatis 的思想是将 SQL 语句编入配置文件中,避免 SQL 语句在代码中大量出现,方便对 SQL 语句的修改和配置。

MyBatis 使用 parameterType 向 SQL 语句传参,在 SQL 引用传参的时候可以使用 #{} 和 ${} 两种方式,两种方式区别如下:

${}:SQL 拼接符号,直接将输入的语句拼接到 SQL 语句里,想避免 SQL 注入问题需要手动添加过滤

#{}:占位符号,在对数据解析时会自动将输入的语句前后加上单引号从而避免 SQL 注入

也就是说在 MyBatis 框架中,如果使用了 ${} 方法,同时又没有进行过滤就会产生 SQL 注入,而使用 #{} 方法时可以避免 SQL 注入。

2、Hibernate 框架

Hibernate 是现今主流的 Java 数据库持久化框架,采用 Hibernate 查询语句(HQL)注入。

HQL 查询语句来自 Hibernate 引擎进行解析,因此产生的错误可能来自数据库,也有可能来自 Hibernate 引擎。

HQL 和 SQL 的区别:

HQL 注入和 SQL 注入的成因都一样,使用拼接 HQL 语句的写法可能会导致 SQL 注入

1
java复制代码Query query = session.createQuery("from User where name='"+queryString+"'");

但是受语法影响,HQL注入在漏洞利用上有一定的限制,比如不能利用联合查询、不能跨库查表、执行命令等。

对于 Hibernate 的注入,这里只作为简单了解一下,平时代审的时候注意一下即可。

参考文章:

www.redhatzone.com/ask/article…

blog.csdn.net/qq_36594628…

原文链接:

www.teamssix.com/211117-0916…

更多信息欢迎关注我的个人微信公众号:TeamsSix

本文转载自: 掘金

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

Go 数据库操作异常处理

发表于 2021-11-17

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

Go 数据库操作异常处理

插入操作

第一种写法

1
2
3
4
5
vbnet复制代码err := db.Model(&XXX{}).Create(order).Error
if err != nil {
logs.CtxError(ctx, "Create XXX failed, err:%v", err.Error())
return err
}

第二种写法

1
2
3
4
5
vbnet复制代码db := db.Model(&XXX{}).Create(order)
if db.Error != nil {
logs.CtxError(ctx, "Create XXX failed, err:%v", db.Error)
return db.Error
}

上述两种写法说明:

两种写法都没啥问题,第一种写法, 如果只插入一条数据,可以使用第一种写法简单;第二种写法可以拿到执行的 *DB ,方便后续的 DB 操作

更新操作

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码db := db.Model(&Voucher{}).Where(whereMap).Updates(updateMap)
if db == nil {
return 0, errors.New(fmt.Sprintf("UpdateVoucherWithState failed, db is null, jrUid:%s, voucherNo:%s", jrUid, voucherNo))
}
err := db.Error

if err != nil {
logs.CtxError(ctx, "UpdateXXX failed err:%v", err)
}
if db.RowsAffected != 1{
logs.CtxWarn(ctx, "UpdateXXX failed RowsAffected:%v", db.RowsAffected)
// TODO 根据业务自身特性单独处理
}

说明:

update 方法将返回执行完之后的 *DB, 需要通过指针对象才能获取正确的 RowAffected。

事务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码tx, txErr = model.BeginTransactionNotShard(ctx)
if txErr != nil {
logs.CtxError(ctx, "db BeginTransaction err: %v", txErr)
return constant.GenRetCode(constant.DBError, txErr.Error())
}
defer func() {
if err := recover(); err != nil {
logs.CtxError(ctx, "panic:%+v", err)
tx.Rollback(ctx, tx)
} else if err != nil {
// logs.CtxError
tx.Rollback()
}
}()

if err := tx.Error; err != nil {
return err
}
if err:= tx.Create(&XXX).Error; err != nil {
return err
}
return tx.Commit().Error

事务的提交也可能会有 error, 要判断是否正确 commit

需要判断 tx.Error,因为事务的提交可能会有 error

查询的异常处理

1
2
3
4
5
6
7
8
go复制代码if err := db.Where("name = ?", "jianzhu").First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordFound) {
// TODO 业务处理查询不到数据的情况
}
if err != nil {
// do something ...
}
}

其实要注意的是,没查询到结果,也会返回一个Error

gorm 的ErrRecordNotFound也好理解,假设根据身份证号查询公民信息,如果是一个无效的身份证ID,那必然无法查询到结果, 其实就是查询不到结果,会返回一个错误。

当然 GORM 提供了一个处理 RecordNotFound 错误的快捷方式,如果发生了多个错误,它将检查每个错误,如果它们中的任何一个是RecordNotFound 错误。

1
2
3
4
5
6
7
8
9
10
go复制代码//检查是否返回 RecordNotFound 错误
db.Where("name = ?", "hello world").First(&user).RecordNotFound()

if db.Model(&user).Related(&credit_card).RecordNotFound() {
// 数据没有找到
}

if err := db.Where("name = ?", "jinzhu").First(&user).Error; gorm.IsRecordNotFoundError(err) {
// 数据没有找到
}
欢迎关注工作号:程序员财富自由之路

参考资料

  • gorm.io/docs/transa…

本文转载自: 掘金

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

一个基于PoS共识算法的区块链实例解析(升级版)

发表于 2021-11-17

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

一个基于PoS共识算法的区块链实例解析(升级版)

本文收录于我的专栏:细讲区块链

本专栏会讲述区块链共识算法以及以太坊智能合约、超级账本智能合约、EOS智能合约相关知识,还会详细的介绍几个实战项目。如果有可能的话,我们还能一起来阅读以太坊的源码。有兴趣的话我们一起来学习区块链技术吧~

一、前言

前面我们简单的介绍了一个基于PoS共识算法的例子,今天我们来解析一个升级版的例子。如果喜欢博主的话,记得点赞,关注,收藏哦~

二、本例中的一些数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码type Block struct {
Index     int
TimeStamp string
BPM       int
HashCode  string
PrevHash  string
Validator string
}
​
var Blockchain []Block
var tempBlocks []Block
​
var candidateBlocks = make(chan Block)
​
var announcements = make(chan string)
​
var validators = make(map[string]int)

首先是定义了一个区块结构体Block,然后定义一条区块链Blockchain,其实就是区块数组。这个tempBlocks是区块缓冲区。candidateBlocks是候选区块,任何一个节点提议一个新块时,都会将它发送到这个管道。announcements是来广播的通道。validators是验证者列表,存节点地址和他拥有的tokens。

三、生成区块和计算哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码func generateBlock(oldBlock Block, BPM int, address string) Block {
var newBlock Block
newBlock.Index = oldBlock.Index + 1
newBlock.TimeStamp = time.Now().String()
newBlock.BPM = BPM
newBlock.PrevHash = oldBlock.HashCode
newBlock.Validator = address
newBlock.HashCode = GenerateHashValue(newBlock)
return newBlock
}
​
func GenerateHashValue(block Block) string {
var hashcode = block.PrevHash +
block.TimeStamp + block.Validator +
strconv.Itoa(block.BPM) + strconv.Itoa(block.Index)
return calculateHash(hashcode)
}
​
func calculateHash(s string) string {
var sha = sha256.New()
sha.Write([]byte(s))
hashed := sha.Sum(nil)
return hex.EncodeToString(hashed)
}

这个真的前面每个例子都在讲,这里真的不想再讲了,不理解的小伙伴可以看一看本专栏前面的例子。

四、主逻辑

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
go复制代码func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
​
genesisBlock := Block{}
genesisBlock = Block{0, time.Now().String(), 0,
GenerateHashValue(genesisBlock), "", ""}
spew.Dump(genesisBlock)
​
Blockchain = append(Blockchain, genesisBlock)
​
port := os.Getenv("PORT")
​
server, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatal(err)
}
​
log.Println("HTTP Server Listening on port :", port)
​
defer server.Close()
​
go func() {
for cadidate := range candidateBlocks {
​
mutex.Lock()
​
tempBlocks = append(tempBlocks, cadidate)
mutex.Unlock()
}
}()
​
go func() {
for {
​
pickWinner()
}
}()
​
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}

我们先来看一看主逻辑,先是加载本地的.env文件,这个文件可以存储很多参数,这里我们存储一个端口号9000.

image-20211117174759924

然后是创建创世区块,创世区块注意它的高度为0.

1
go复制代码spew.Dump(genesisBlock)

就是把创世区块通过命令行格式化输出。

1
go复制代码Blockchain = append(Blockchain, genesisBlock)

这行代码是将创世区块添加到区块链。

1
go复制代码port := os.Getenv("PORT")

前面说.env文件中存储了端口号,这里就获取这个文件中的端口号到port变量中。

然后启动服务进程监听上面获取的端口。

1
go复制代码defer server.Close()

要养成启动服务就书写延迟关闭的习惯,不然后面任意忘记释放资源。

然后是并发操作,循环读取candidateBlocks,一旦这个管道有一个区块进入,马上把它读取到缓冲区。接着并发判断哪个节点应该去挖矿。

然后不断接收验证者节点的连接,连上就处理终端发送过来的信息。

五、获取记账权的节点

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
go复制代码func pickWinner() {
time.Sleep(30 * time.Second)
mutex.Lock()
temp := tempBlocks
mutex.Unlock()
​
lotteryPool := []string{}
if len(temp) > 0 {
OUTER:
for _, block := range temp {
for _, node := range lotteryPool {
if block.Validator == node {
continue OUTER
}
}
​
​
mutex.Lock()
​
setValidators := validators
mutex.Unlock()
​
k, ok := setValidators[block.Validator]
if ok {
​
for i := 0; i < k; i++ {
lotteryPool = append(lotteryPool, block.Validator)
}
}
}
​
s := rand.NewSource(time.Now().Unix())
r := rand.New(s)
​
lotteryWinner := lotteryPool[r.Intn(len(lotteryPool))]
​
for _, block := range temp {
if block.Validator == lotteryWinner {
mutex.Lock()
Blockchain = append(Blockchain, block)
mutex.Unlock()
for _ = range validators {
announcements <- "\nvalidator:" + lotteryWinner + "\n"
}
break
}
}
​
}
mutex.Lock()
tempBlocks = []Block{}
mutex.Unlock()
}

这里就是PoS的精髓,根据代币tokens数量来确定拥有记账权的节点。

先是每次选出拥有记账权的节点就得休息30秒,不能一直不停的选吧。

每次选拥有记账权的节点之前,将缓冲区的区块拷贝一份部分,然后操作副本。

我们先声明一个彩票池来放置验证者地址。

然后判断缓冲区是否为空,如果缓冲区副本不为空,就遍历缓冲区副本,然后如果区块的验证者在彩票池就继续遍历,如果不在就执行后面的内容。

然后是获取一个验证者列表副本,获取上面不在彩票池中的验证者节点的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
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
go复制代码
func handleConn(conn net.Conn) {

defer conn.Close()

go func() {

msg := <-announcements
io.WriteString(conn, msg)
}
}()

var address string

io.WriteString(conn, "Enter token balance:")

scanBalance := bufio.NewScanner(conn)
for scanBalance.Scan() {

balance, err := strconv.Atoi(scanBalance.Text())
if err != nil {
log.Printf("%v not a number: %v", scanBalance.Text(), err)
return
}

address = calculateHash(time.Now().String())

validators[address] = balance
fmt.Println(validators)
break
}

io.WriteString(conn, "\nEnter a new BPM:")

scanBPM := bufio.NewScanner(conn)
go func() {

for {
for scanBPM.Scan() {
bmp, err := strconv.Atoi(scanBPM.Text())
if err != nil {
log.Printf("%v not a number: %v", scanBPM.Text(), err)

delete(validators, address)
conn.Close()
}
mutex.Lock()
oldLastIndex := Blockchain[len(Blockchain)-1]
mutex.Unlock()
​
newBlock := generateBlock(oldLastIndex, bmp, address)
if err != nil {
log.Println(err)
continue
}

if isBlockValid(newBlock, oldLastIndex) {

candidateBlocks <- newBlock
}
}
}
}()
​
for {
time.Sleep(time.Second * 20)
mutex.Lock()
output, err := json.Marshal(Blockchain)
mutex.Unlock()
if err != nil {
log.Fatal(err)
}

io.WriteString(conn, string(output)+"\n")
}
​
}
​
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.HashCode != newBlock.PrevHash {
return false
}
if GenerateHashValue(newBlock) != newBlock.HashCode {
return false
}
return true
}

先是延时释放连接资源。

1
go复制代码defer conn.Close()

然后从管道中读取选出幸运儿的消息,并将其输出到连接conn。

然后在命令行窗口接收该节点的tokens数量。

然后根据当前时间生成验证者的地址。

1
less复制代码address = calculateHash(time.Now().String())

再将验证者地址和他拥有的tokens存到validators中。

然后再根据提示输入交易信息。如果输入的交易信息非法,就将该节点删除。

1
2
scss复制代码delete(validators, address)
conn.Close()

之后的逻辑是取上一个区块,然后生成新的区块信息,然后简单的验证区块是否合法,合法的话就将区块放入candidateBlocks管道,等待抽取幸运儿。

此处验证区块是否合法的方法很简单,就是验证当前区块的高度是不是上一个模块加一,然后判断新区块的PrevHash是不是等于上一个区块的哈希值。然后再一次检验哈希值是否正确。

七、运行结果

image-20211117202628046

image-20211117202650550

本文转载自: 掘金

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

Linux系统查看磁盘可用空间的5个命令

发表于 2021-11-17

大家好,我是良许。

工作中,经常会遇到磁盘爆满的情况,尤其是一台服务器运行了 N 年之后,里面会充满各种各样垃圾文件,比如:编译产生的中间文件、打包的镜像文件、日志文件,等等。

别问我怎么知道,我上家公司服务器就是这样的,我需要每天去删除一些没用的文件,才能腾出一些空间来工作。

那如何查看 Linux 系统磁盘的可用空间呢?下面给大家介绍 5 个我工作中常用到的命令。

df 命令

df 命令是英文单词 disk-free 的缩写,用于查看 Linux 系统中的可用的和已经使用的磁盘空间。这个命令一般有以下几个常用选项:

  • df -h :以人类可读的格式显示磁盘空间(否则默认显示单位是字节,不直观)
  • df -a :包含全部的文件系统

  • df -T :显示磁盘使用情况以及每个块的文件系统类型(例如,xfs、ext2、ext3、btrfs 等)
  • df -i :显示已使用和空闲的 inode

如果你不喜欢敲代码,更喜欢使用图形界面,那么在 GNOME 桌面中你可以使用一个叫 Disks 的软件(gnome-disk-utility)来获取这些信息。

Disks 启动之后可以查看计算机检测到的所有磁盘,然后单击分区以查看有关它的详细信息,包括已用空间和剩余空间。

du 命令

du 命令是英语单词 disk useage 的缩写,它是以默认千字节大小显示文件、文件夹等磁盘使用情况。常用的选项有以下几个:

  • du -h :以人类可读的格式显示所有目录和子目录的磁盘使用情况
  • du -a :显示所有文件的磁盘使用情况
  • du -s :仅显示总计,只列出最后加总的值

同样地,在 GNOME 桌面中,也有一个叫 Disk Usage 的软件,可以很直观查看磁盘的使用情况。而在 KDE 桌面中,对应的软件是 Filelight 软件。

在这两个软件中,磁盘使用情况被映射到一系列的同心圆里,中间是基本文件夹(通常是你的 /home 目录,但是可以自行设定),每个外环代表一个更深的目录级别。将鼠标悬停在任意位置上,就可以获取这部分磁盘占用空间的详细信息。

ls -al 命令

ls 命令大家再熟悉不过了,使用 ls -al 命令可以列出特定目录的全部内容及其大小。

stat 命令

stat 命令后面可以直接跟上文件或目录,用于显示文件/目录或文件系统的大小和其他统计信息。

Linux fdisk -l 命令

fdisk -l 可以显示磁盘大小以及磁盘分区信息。

以上这些命令是我在查看磁盘可用空间时非常常用的几个命令,而且都是 Linux 系统内置命令,无需额外安装。也有一些功能类似的第三方工具,比如 Disks 、Ncdu 等工具,可以直观显示磁盘空间利用率。

你最喜欢使用哪个命令呢?评论区一起讨论一下呗~


最后,最近很多小伙伴找我要Linux学习路线图,于是我根据自己的经验,利用业余时间熬夜肝了一个月,整理了一份电子书。无论你是面试还是自我提升,相信都会对你有帮助!

免费送给大家,只求大家金指给我点个赞!

电子书 | Linux开发学习路线图

也希望有小伙伴能加入我,把这份电子书做得更完美!

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

推荐阅读:

  • 干货 | 程序员进阶架构师必备资源免费送
  • 书单 | 程序员必读经典书单(高清PDF版)

欢迎关注我的博客:良许Linux教程网,满满都是干货!

本文转载自: 掘金

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

springboot webflux响应式编程再探

发表于 2021-11-17

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

有同学问我什么是响应式编程,

专业的解释是:
响应式编程(reactive programming)是一种基于数据流(data stream)和变化传递(propagation of change)的声明式(declarative)的编程范式。
那数据流(data stream)和声明式(declarative)怎么理解呢?那就要提一提我们的Stream流了。

Stream流的使用分为三个步骤(创建Stream流、执行中间操作、执行最终操作)。

stream的中间操作Intermediate:打开流做一定的数据过滤和映射,得到一个新的流。
filter过滤

map 处理

mapToInt 转int

flatMap 降维

peek 遍历操作是一个中间操作

skip 跳过多少元素skip(6)

limit裁剪操作limit(6)取前六个元素

sorted排序,默认从小到大排序,sorted() sorted(Comparator<? super T> comparator)

distinct去重。

stream的终止操作:Terminal:终结操作,一个流只能有一个terminal操作,当这个操作执行后,流就被使用”光”了,无法再被操作。所以这必定是流的最后一个操作。
Terminal操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。 f

orEach forEachOrdered

toArray返回一个Object[] toArray(Student[]::new);

reduce 归纳操作 需要提供一个起始值,根据一定的规则进行运算

collect
.collect(Collectors.toList());
.collect(Collectors.toCollection(TreeSet::new));
List转map:
.collect(Collectors.toMap(Student::getAge,s->s));

max/min 找出最大/最小的元素。max/min必须传入一个Comparator

count 返回元素数量

短路终止:
anyMatch 任意匹配;
allMatch 所有匹配;
noneMatch 没有匹配;
findFirst 返回第一个元素;
findAny 返回任意一个元素。

说了这么多,怎么理解数据流和声明式呢?
本来数据是我们自行处理的,后来我们把要处理的数据抽象出来(变成了数据流),然后通过API去处理数据流中的数据(是声明式的)

比如下面的代码;将数组中的数据变成数据流,通过显式声明调用.sum()来处理数据流中的数据,得到最终的结果:

1
2
3
4
5
ini复制代码public static void main(String[] args) {
int[] nums = { 1, 2, 3 };
int sum2 = IntStream.of(nums).parallel().sum();
System.out.println("结果为:" + sum2);
}

说了这么多,今天来看看webflux实现websocket的功能。

WebFlux 本身提供了对 WebSocket 协议的支持,处理 WebSocket 请求需要对应的 handler 实现 WebSocketHandler 接口,每一个 WebSocket 都有一个关联的 WebSocketSession,包含了建立请求时的握手信息 HandshakeInfo,以及其它相关的信息。可以通过 session 的 receive() 方法来接收客户端的数据,通过 session 的 send() 方法向客户端发送数据。

直接上代码:

第一步 配置websocket

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
typescript复制代码@Configuration
public class WebSocketConfig {

@Bean
public HandlerMapping handlerMapping(@Autowired EchoHandler echoHandler){
//定义映射集合
Map<String, WebSocketHandler> map = new HashMap<>();
//配置映射模型
map.put("/websocket/{token}", echoHandler);
//映射处理
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
//优先配置
handlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
//映射路劲
handlerMapping.setUrlMap(map);
return handlerMapping;//
}

//配置适配器
@Bean
public WebSocketHandlerAdapter webSocketHandlerAdapter(){
return new WebSocketHandlerAdapter();

}
}

第二步 开发webSocketHandler

1
2
3
4
5
6
7
8
9
10
11
less复制代码@Component
@Slf4j
public class EchoHandler implements WebSocketHandler {

//websocket处理终端
@Override
public Mono<Void> handle(WebSocketSession session) {
log.info("websocket的握手信息={}", session.getHandshakeInfo().getUri());
return session.send(session.receive().map(msg->session.textMessage("[Echo]"+msg.getPayloadAsText())));
}
}

第三步 websocket测试

www.jsons.cn/websocket/ 网站上输入:ws://localhost:8089/websocket/test
点击WebSocket连接,输入消息,控制台打印消息如下:

连接成功,现在你可以发送信息进行测试了!

你发送的信息 2021-11-17 17:08:45

hello

服务端回应 2021-11-17 17:08:45

[Echo]hello

你发送的信息 2021-11-17 17:08:54

你好呀

服务端回应 2021-11-17 17:08:54

[Echo]你好呀

这样就能够发/websocket/{msg} 的 WebSocket 请求交给 EchoHandler 处理。
还要为 WebSocket 类型的 handler 创建对应的 WebSocketHandlerAdapter,以便让 DispatcherHandler 能够调用我们的 WebSocketHandler。
完成这三个步骤后,当一个 WebSocket 请求到达 WebFlux 时,首先由 DispatcherHandler 进行处理,它会根据已有的 HandlerMapping 找到这个 WebSocket 请求对应的 handler,接着发现该 handler 实现了 WebSocketHandler 接口,于是会通过 WebSocketHandlerAdapter 来完成该 handler 的调用。

很明显这样的操作是有问题的。WebSocket 是全双工通信,如何实现双向通信,而不是一次请求一次返回,这样没有任何的的实际意义,另外SimpleUrlHandlerMapping的URL配置映射模型也是国定的,添加新的Handler则需要修改配置,很麻烦。那么该如何优化呢?

且在下回分解。

本文转载自: 掘金

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

快来给你的 jupyter notebook 设计一款独特的

发表于 2021-11-17

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

前言

jupyter notebook 作为 Python 中一款热门的交互式编程工具,深受广大数据计算工作者喜爱,今天我们来看看如何更换 jupyter notebook 主题,打造一款属于自己风格的 jupyter notebook。

安装主题包

打开命令提示符,执行下面代码。

1
cmd复制代码pip install jupyterthemes

使用 jt -h 可以查看帮助。

使用 jt -l 可以查看已安装的主题。

1
2
3
4
5
6
7
8
9
10
cmd复制代码Available Themes:
chesterish
grade3
gruvboxd
gruvboxl
monokai
oceans16
onedork
solarizedd
solarizedl

选择主题

使用 jt -t 主题 可以更换安装的主题。

1
cmd复制代码jt -t chesterish

我们看看9中主题的样式分别是什么样,给大家提供一个参考。

chesterish

grade3

gruvboxd

gruvboxd

monokai

oceans16

onedork

solarizedd

solarizedl

其他命令

除了 jt -t 指定主题外,还有以下参数可以调整主题更多细节。

命令行选项的描述

  • 代码的字体: -f
  • 代码字体大小: -fs(默认值:11 )
  • Notebook 字体: -nf
  • Notebook 字体大小: -nfs( 默认值: 13 )
  • Text/MD 单元格的字体: -tf
  • Text/MD 单元格字体大小:-tfs (默认值: 13)
  • Pandas DF Fontsize:-dfs(默认值: 9)
  • 输出面积字形大小: -ofs(默认值: 8.5 )
  • Mathjax 字形大小 (%): -mathfs(默认值: 100)
  • 介绍页边距 : -m(默认值: auto)
  • 单元格的宽度:-cellw ( 默认值: 980)
  • 行高: -lineh(默认值: 170 )
  • 光标宽度: -cursw(默认值: 2)
  • 光标的颜色:-cursc
  • Alt键提示布局:-altp
  • Alt键Markdown背景颜色:-altmd
  • Alt键输出背景色:-altout
  • Vim风格 :-vim
  • 工具栏可见: -T
  • 名称和标识可见: -N
  • 标志可见: -kl
  • 重置默认主题: -r
  • 强制默认字体:-dfonts

对于上面的一些命令有部分具有特定参数,下面列出可能的取值:

代码的字体(等宽字体):-f 参数

参数列表

anka
anonymous
aurulent
bitstream
bpmono
code
consolamono
cousine
dejavu
droidmono
fira
firacode
generic
hack
hasklig
inconsolata
inputmono
iosevka
liberation
meslo
office
oxygen
roboto
saxmono
source
sourcemed
ptmono
ubuntu

Notebook 字体和Text/MD 单元格的字体(无衬线字体):-nf/-tf 参数

参数列表

opensans
droidsans
exosans
latosans
ptsans
robotosans
sourcesans

Notebook 字体和Text/MD 单元格的字体(衬线字体):-nf/-tf 参数

参数列表

loraserif
ptserif
georgiaserif
cardoserif
crimsonserif
ebserif
merriserif
neutonserif
goudyserif

命令示例

恢复默认主题

1
cmd复制代码jt -r

我的主题

1
r复制代码jt -t grade3 -f consolamono -fs 140 -altp -tfs 13 -nfs 115 -ofs 14 -cellw 80% -T

分析

  • jt -t chesterish:选择皮肤(chesterish)
  • -f consolamono:代码的字体(consolamono)
  • -fs 140:代码字体大小(140)
  • -altp:Alt键提示布局(默认)
  • -tfs 13:Text/MD 单元格字体大小(13)
  • -nfs 115:Notebook 字体大小(115)
  • -ofs 14:输出面积字形大小(14)
  • -cellw 80%:单元格的宽度(80%)
  • -T:工具栏可见

效果如下,个人感觉还不错


这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。更有整理的近千套简历模板,几百册电子书等你来领取哦!

本文转载自: 掘金

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

Redis中三种特殊数据类型详解

发表于 2021-11-17

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

Redis除了5种基础数据类型,还有三种特殊的数据类型,分别是 HyperLogLogs(基数统计), Bitmaps (位图) 和 geospatial (地理位置)。

HyperLogLogs(基数统计)

Redis 2.8.9 版本更新了 Hyperloglog 数据结构!

  • 什么是基数?

举个例子,A = {1, 2, 3, 4, 5}, B = {3, 5, 6, 7, 9};那么基数(不重复的元素)= 1, 2, 4, 6, 7, 9; (允许容错,即可以接受一定误差)

  • HyperLogLogs 基数统计用来解决什么问题?

这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等。

  • 它的优势体现在哪?

一个大型的网站,每天 IP 比如有 100 万,粗算一个 IP 消耗 15 字节,那么 100 万个 IP 就是 15M。而 HyperLogLog 在 Redis 中每个键占用的内容都是 12K,理论存储近似接近 2^64 个值,不管存储的内容是什么,它一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的基数并不一定准确,是一个带有 0.81% 标准错误的近似值(对于可以接受一定容错的业务场景,比如IP数统计,UV等,是可以忽略不计的)。

  • 相关命令使用
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码127.0.0.1:6379> pfadd key1 a b c d e f g h i	# 创建第一组元素
(integer) 1
127.0.0.1:6379> pfcount key1 # 统计元素的基数数量
(integer) 9
127.0.0.1:6379> pfadd key2 c j k l m e g a # 创建第二组元素
(integer) 1
127.0.0.1:6379> pfcount key2
(integer) 8
127.0.0.1:6379> pfmerge key3 key1 key2 # 合并两组:key1 key2 -> key3 并集
OK
127.0.0.1:6379> pfcount key3
(integer) 13

Bitmap (位存储)

Bitmap 即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。

  • 用来解决什么问题?

比如:统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps!

如果存储一年的打卡状态需要多少内存呢? 365 天 = 365 bit 1字节 = 8bit 46 个字节左右!

  • 相关命令使用

使用bitmap 来记录 周一到周日的打卡! 周一:1 周二:0 周三:0 周四:1 ……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0

查看某一天是否有打卡!

1
2
3
4
bash复制代码127.0.0.1:6379> getbit sign 3
(integer) 1
127.0.0.1:6379> getbit sign 5
(integer) 0

统计操作,统计 打卡的天数!

1
2
bash复制代码127.0.0.1:6379> bitcount sign # 统计这周的打卡记录,就可以看到是否有全勤!
(integer) 3

geospatial (地理位置)

Redis 的 Geo 在 Redis 3.2 版本就推出了! 这个功能可以推算地理位置的信息:两地之间的距离,,方圆几里的人。

geoadd

添加地理位置

1
2
3
4
bash复制代码127.0.0.1:6379> geoadd china:city 118.76 32.04 manjing 112.55 37.86 taiyuan 123.43 41.80 shenyang
(integer) 3
127.0.0.1:6379> geoadd china:city 144.05 22.52 shengzhen 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 3

规则

两级无法直接添加,我们一般会下载城市数据(这个网址可以查询 GEO: www.jsons.cn/lngcode)!%EF%BC%81)

  • 有效的经度从-180度到180度。
  • 有效的纬度从-85.05112878度到85.05112878度。
1
2
3
bash复制代码# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin
(error) ERR invalid longitude,latitude pair 39.900000,116.400000

geopos

获取指定的成员的经度和纬度

1
2
3
4
5
bash复制代码127.0.0.1:6379> geopos china:city taiyuan manjing
1) 1) "112.54999905824661255"
1) "37.86000073876942196"
2) 1) "118.75999957323074341"
1) "32.03999960287850968"

获得当前定位, 一定是一个坐标值!

geodist

如果不存在, 返回空

单位如下

  • m
  • km
  • mi 英里
  • ft 英尺
1
2
3
4
bash复制代码127.0.0.1:6379> geodist china:city taiyuan shenyang m
"1026439.1070"
127.0.0.1:6379> geodist china:city taiyuan shenyang km
"1026.4391"

georadius

附近的人 ==> 获得所有附近的人的地址, 定位, 通过半径来查询

获得指定数量的人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码127.0.0.1:6379> georadius china:city 110 30 1000 km			以 100,30 这个坐标为中心, 寻找半径为1000km的城市
1) "xian"
2) "hangzhou"
3) "manjing"
4) "taiyuan"
127.0.0.1:6379> georadius china:city 110 30 500 km
1) "xian"
127.0.0.1:6379> georadius china:city 110 30 500 km withdist
1) 1) "xian"
2) "483.8340"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord withdist count 2
1) 1) "xian"
2) "483.8340"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"
2) 1) "manjing"
2) "864.9816"
3) 1) "118.75999957323074341"
2) "32.03999960287850968"

参数 key 经度 纬度 半径 单位 [显示结果的经度和纬度] [显示结果的距离] [显示的结果的数量]

georadiusbymember

显示与指定成员一定半径范围内的其他成员

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km
1) "manjing"
2) "taiyuan"
3) "xian"
127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km withcoord withdist count 2
1) 1) "taiyuan"
2) "0.0000"
3) 1) "112.54999905824661255"
2) "37.86000073876942196"
2) 1) "xian"
2) "514.2264"
3) 1) "108.96000176668167114"
2) "34.25999964418929977"

参数与 georadius 一样

geohash(较少使用)

该命令返回11个字符的hash字符串

1
2
3
bash复制代码127.0.0.1:6379> geohash china:city taiyuan shenyang
1) "ww8p3hhqmp0"
2) "wxrvb9qyxk0"

将二维的经纬度转换为一维的字符串, 如果两个字符串越接近, 则距离越近

底层

geo底层的实现原理实际上就是Zset, 我们可以通过Zset命令来操作geo

1
2
bash复制代码127.0.0.1:6379> type china:city
zset

查看全部元素 删除指定的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码127.0.0.1:6379> zrange china:city 0 -1 withscores
1) "xian"
2) "4040115445396757"
3) "hangzhou"
4) "4054133997236782"
5) "manjing"
6) "4066006694128997"
7) "taiyuan"
8) "4068216047500484"
9) "shenyang"
1) "4072519231994779"
2) "shengzhen"
3) "4154606886655324"
127.0.0.1:6379> zrem china:city manjing
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "xian"
2) "hangzhou"
3) "taiyuan"
4) "shenyang"
5) "shengzhen"

本文转载自: 掘金

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

微服务配置Redis多数据源

发表于 2021-11-17

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

背景

在微服务概念中,不同的服务需要使用不同的数据库,包括mysql与redis,但是如果是需要高并发又与这样设计造成冲突,因为直接查询redis会更快一些。

集成Redis多数据源与lettuce连接池

springboot中集成redis多个数据源需要屏蔽starter中带有的原生RedisAutoConfiguration。
并模拟RedisAutoConfiguration,设置多个redisTemplate的bean。并在使用时通过Autowired+Qualifier注入。

依赖添加

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置文件

yaml文件中配置多数据源连接信息,配置一个端口为6379的默认Redis1和一个端口为6380的Redis2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yml复制代码redis:
lettuce1:
host: localhost
password:
port: 6379
timeout: 5000
pool:
maxWaitMillis: 10000
minIdle: 5
maxIdle: 160
maxTotal: 500
lettuce2:
host: localhost
password:
port: 6380
timeout: 5000
pool:
maxWaitMillis: 10000
minIdle: 5
maxIdle: 160
maxTotal: 500

排除RedisAutoConfiguration

不使用默认的springboot进行加载初始化,必须要在注解中排除RedisAutoConfiguration

1
2
3
4
5
6
7
8
java复制代码@Import(value = {
RedisLettuceConfig1.class,
RedisLettuceConfig2.class
})

@SpringBootApplication(exclude = {
RedisAutoConfiguration.class
})

Redis1配置类

配置lettuce连接池、数据源、数据源的连接工厂、RedisTemplate。并设置序列化方式

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
java复制代码@EnableConfigurationProperties({RedisLettuceProperties1.class})
public class RedisLettuceConfig1 {
private static final String REDIS_STANDALONE_CONFIGURATION = "redisStandaloneConfiguration1";
private static final String LETTUCE_CLIENT_CONFIGURATION = "lettuceClientConfiguration1";
public static final String REDIS_CONNECTION_FACTORY = "redisLettuceConnectionFactory1";
public static final String REDIS_TEMPLATE = "redisLettuceTemplate1";

@Autowired
private RedisLettuceProperties1 redisProperties;

public RedisLettuceConfig1() {
}

@Bean(
name = {"redisStandaloneConfiguration1"}
)
public RedisStandaloneConfiguration redisStandaloneConfiguration() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(this.redisProperties.getHost(), this.redisProperties.getPort());
if (!StringUtils.isEmpty(this.redisProperties.getPassword())) {
RedisPassword password = RedisPassword.of(this.redisProperties.getPassword());
configuration.setPassword(password);
}

configuration.setDatabase(this.redisProperties.getDbIndex());
return configuration;
}

@Bean(
name = {"lettuceClientConfiguration1"}
)
public LettuceClientConfiguration lettuceClientConfiguration() {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMinIdle(this.redisProperties.getPool().getMinIdle());
config.setMaxIdle(this.redisProperties.getPool().getMaxIdle());
config.setMaxTotal(this.redisProperties.getPool().getMaxTotal());
config.setMaxWaitMillis((long)this.redisProperties.getPool().getMaxWaitMillis());
config.setTestOnBorrow(this.redisProperties.getPool().getTestOnBorrow());
config.setTestOnReturn(this.redisProperties.getPool().getTestOnReturn());
LettucePoolingClientConfiguration configuration = LettucePoolingClientConfiguration.builder().poolConfig(config).commandTimeout(Duration.ofMillis((long)this.redisProperties.getTimeout())).build();
return configuration;
}

@Bean(
name = {"redisLettuceConnectionFactory1"}
)
public RedisConnectionFactory redisConnectionFactory(@Qualifier("redisStandaloneConfiguration1") RedisStandaloneConfiguration redisStandaloneConfiguration, @Qualifier("lettuceClientConfiguration1") LettuceClientConfiguration lettuceClientConfiguration) {
LettuceConnectionFactory factory = new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration);
return factory;
}

@Bean(
name = {"redisLettuceTemplate1"}
)
public RedisTemplate<String, Object> redisTemplate(@Qualifier("redisLettuceConnectionFactory1") RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
RedisSerializer<Object> objectSerializer = new JdkSerializationRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(objectSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(objectSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}

}

Redis2配置类

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
java复制代码@EnableConfigurationProperties({RedisLettuceProperties2.class})
public class RedisLettuceConfig2 {
private static final String REDIS_STANDALONE_CONFIGURATION = "redisStandaloneConfiguration2";
private static final String LETTUCE_CLIENT_CONFIGURATION = "lettuceClientConfiguration2";
public static final String REDIS_CONNECTION_FACTORY = "redisLettuceConnectionFactory2";
public static final String REDIS_TEMPLATE = "redisLettuceTemplate2";
@Autowired
private RedisLettuceProperties2 RedisLettuceProperties;

public RedisLettuceConfig2() {
}

@Bean(
name = {"redisStandaloneConfiguration2"}
)
public RedisStandaloneConfiguration redisStandaloneConfiguration() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(this.RedisLettuceProperties.getHost(), this.RedisLettuceProperties.getPort());
if (!StringUtils.isEmpty(this.RedisLettuceProperties.getPassword())) {
RedisPassword password = RedisPassword.of(this.RedisLettuceProperties.getPassword());
configuration.setPassword(password);
}

configuration.setDatabase(this.RedisLettuceProperties.getDbIndex());
return configuration;
}

@Bean(
name = {"lettuceClientConfiguration2"}
)
public LettuceClientConfiguration lettuceClientConfiguration() {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMinIdle(this.RedisLettuceProperties.getPool().getMinIdle());
config.setMaxIdle(this.RedisLettuceProperties.getPool().getMaxIdle());
config.setMaxTotal(this.RedisLettuceProperties.getPool().getMaxTotal());
config.setMaxWaitMillis((long)this.RedisLettuceProperties.getPool().getMaxWaitMillis());
config.setTestOnBorrow(this.RedisLettuceProperties.getPool().getTestOnBorrow());
config.setTestOnReturn(this.RedisLettuceProperties.getPool().getTestOnReturn());
LettucePoolingClientConfiguration configuration = LettucePoolingClientConfiguration.builder().poolConfig(config).commandTimeout(Duration.ofMillis((long)this.RedisLettuceProperties.getTimeout())).build();
return configuration;
}

@Bean(
name = {"redisLettuceConnectionFactory2"}
)
public RedisConnectionFactory redisConnectionFactory(@Qualifier("redisStandaloneConfiguration2") RedisStandaloneConfiguration redisStandaloneConfiguration, @Qualifier("lettuceClientConfiguration2") LettuceClientConfiguration lettuceClientConfiguration) {
LettuceConnectionFactory factory = new LettuceConnectionFactory(redisStandaloneConfiguration, lettuceClientConfiguration);
return factory;
}


@Bean(
name = {"redisLettuceTemplate2"}
)
public RedisTemplate<String, Object> redisTemplate(@Qualifier("redisLettuceConnectionFactory2") RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
RedisSerializer<Object> objectSerializer = new JdkSerializationRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(objectSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(objectSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}

}

测试

引用redisTemplate并指定来自哪个bean

1
2
3
4
5
6
java复制代码    @Autowired
@Qualifier(RedisLettuceConfig1.REDIS_TEMPLATE)
private RedisTemplate redisTemplate;

HashOperations<String, String, String> hashOperations = redisTemplate.opsForHash();
String value = hashOperations.get("users", userId);

小结

springboot配置redis多数据源的过程比较简单,引入依赖包,添加配置,初始化Bean。
需要注意的是Redis有默认的实现类了,所以在装配使用的时候,要加上@Qualifier注解并指定前面Bean注入的名字,不然自动注入后会使用默认的配置,不能使用指定的Redis数据源。

本文转载自: 掘金

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

『十倍程序员』Java五大对象映射框架性能比拼

发表于 2021-11-17

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

1.前言

Hello 大家好,我是l拉不拉米,在上一篇文章《Java五大对象映射框架,总有一款适合你😘》中,给大家分享Java五大映射框架的介绍以及简单使用,今天我们以性能为指标,来给这5个对象映射框架较个高低。

  1. 测试模型

为了能够正确地测试映射,我们需要有源模型和目标模型。我们创建了两个测试模型。

第一个只是一个带有一个 String 字段的简单 POJO,这允许我们在更简单的情况下比较框架,并检查如果我们使用更复杂的 bean 是否有任何变化。

简单的源模型如下所示:

1
2
3
4
java复制代码public class SourceCode { 
String code;
// getter and setter
}

目标模型如下:

1
2
3
4
java复制代码public class DestinationCode { 
String code;
// getter and setter
}

源 bean 的真实示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class SourceOrder { 
private String orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private DeliveryData deliveryData;
private User orderingUser;
private List<Product> orderedProducts;
private Shop offeringShop;
private int orderId;
private OrderStatus status;
private LocalDate orderDate;
// getters and setters
}

目标类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Order { 
private User orderingUser;
private List<Product> orderedProducts;
private OrderStatus orderStatus;
private LocalDate orderDate;
private LocalDate orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private int shopId;
private DeliveryData deliveryData;
private Shop offeringShop;
// getters and setters
}

3.测试结果

使用Java基准测试神器 – JMH(Java Microbenchmark Harness)作为测试工具。

我们为每个转换器创建了一个单独的基准,将 BenchmarkMode 指定为 Mode.All。

3.1.平均时间

JMH 返回了以下平均运行时间的结果(越少越好):

框架名称 平均运行时间 (单位:ms)
MapStruct 10 -5
JMapper 10 -5
Orika 0.001
ModelMapper 0.001
Dozer 0.002

该基准明显地显示了MapStruct和JMapper都有最好的平均工作时间。

3.2.吞吐量

在此模式下,该基准测试返回每秒的操作数。我们收到以下结果(更多更好):

框架名称 吞吐量 (每毫秒操作次数)
MapStruct 133719
JMapper 106978
Orika 1800
ModelMapper 978
Dozer 471

在吞吐量模式下,MapStruct是框架测试中最快的,JMapper次之。

3.3.单发时间

此模式允许测量从它开始到结束的单个操作的时间。该基准得到以下结果(更少更好):

框架名称 单发时间 (每操作毫秒数)
JMapper 0.015
MapStruct 0.450
Dozer 2.094
Orika 2.898
ModelMapper 4.837

我们看到JMapper比MapStruct返回更好的结果。

3.4.采样时间

此模式允许对每个操作的时间进行采样。三种不同百分位数的结果如下:

采样时间 (单位:ms)
框架名称 p0.90 p0.999 p1.0
JMapper 10-4 0.001 2.6
MapStruct 10-4 0.001 3
Orika 0.001 0.010 4
ModelMapper 0.002 0.015 3.2
Dozer 0.003 0.021 25

所有基准测试都显示,Mapstruct和JMapper根据场景不同都是个好选择。

4.结论

通过简单的理想环境下的测试结果,我们可以得出Mapstruct和JMapper性能较接近,但是Mapstruct易用性更强,JMapper更小众,因此,笔者更加推荐使用Mapstruct。

其他三种框架表现和前两名的差距较大,不做推荐。

本文转载自: 掘金

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

1…305306307…956

开发者博客

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