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

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


  • 首页

  • 归档

  • 搜索

MyBatis-Plus码之重器 lambda 表达式使用指

发表于 2021-05-14

一、回顾

现在越来越流行基于 SpringBoot 开发 Web 应用,其中利用 Mybatis 作为数据库 CRUD 操作已成为主流。楼主以 MySQL 为例,总结了九大类使用 Mybatis 操作数据库 SQL 小技巧分享给大家。

  1. 分页查询
  2. 预置 sql 查询字段
  3. 一对多级联查询
  4. 一对一级联查询
  5. foreach 搭配 in 查询
  6. 利用if 标签拼装动态 where 条件
  7. 利用 choose 和 otherwise组合标签拼装查询条件
  8. 动态绑定查询参数:_parameter
  9. 利用 set 配合 if 标签,动态设置数据库字段更新值

01 分页查询

利用 limit 设置每页 offset 偏移量和每页 size 大小。

1
2
3
4
5
6
7
8
sql复制代码select * from sys_user u
LEFT JOIN sys_user_site s ON u.user_id = s.user_id
LEFT JOIN sys_dept d ON d.dept_id = s.dept_id
LEFT JOIN sys_emailinfo e ON u.user_id = e.userid AND e.MAIN_FLAG = 'Y'
<where>
<include refid="userCondition"/>
</where>
limit #{offset}, #{limit}

02 预置 sql 查询字段

1
2
3
bash复制代码<sql id="columns">
id,title,content,original_img,is_user_edit,province_id,status,porder
</sql>

查询 select 语句引用 columns:

1
2
3
4
5
6
7
bash复制代码<select id="selectById" resultMap="RM_MsShortcutPanel">
seelct
<include refid="columns"/>
from cms_self_panel
where
id = #{_parameter}
</select>

03 一对多级联查询

利用 mybatis 的 collection 标签,可以在每次查询文章主体同时通过 queryparaminstancelist 级联查询出关联表数据。

1
2
3
4
ini复制代码<resultMap id="BaseResultMap" type="com.unicom.portal.pcm.entity.ArticleEntity">
<id column="id" jdbcType="BIGINT" property="id"/>
<collection property="paramList" column="id" select="queryparaminstancelist"/>
</resultMap>

queryparaminstancelist 的 sql 语句

1
2
3
csharp复制代码<select id="queryparaminstancelist" resultMap="ParamInstanceResultMap">
select * from `cms_article_flow_param_instance` where article_id=#{id}
</select>

04 一对一级联查询

利用 mybatis 的 association 标签,一对一查询关联表数据。

1
2
3
ini复制代码<resultMap id="BaseResultMap" type="com.unicom.portal.pcm.entity.ArticleEntity">
<association property="articleCount" javaType="com.unicom.portal.pcm.entity.MsArticleCount"/>
</resultMap>

查询sql语句:

MsArticlecount 实体对象的属性值可以从 上面的 select 后的 sql 字段进行匹配映射获取。

05 foreach 搭配 in 查询

利用 foreach 遍历 array 集合的参数,拼成 in 查询条件

1
2
3
perl复制代码<foreach collection="array" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>

06 利用 if 标签拼装动态 where 条件

1
2
3
4
5
6
7
8
9
10
csharp复制代码select r.*, (select d.org_name from sys_dept d where d.dept_id = r.dept_id) deptName from sys_role r
<where>
r.wid = #{wid}
<if test="roleName != null and roleName.trim() != ''">
and r.`role_name` like concat('%',#{roleName},'%')
</if>
<if test="status != null and status.trim() != ''">
and r.`status` = #{status}
</if>
</where>

07 利用 choose 和 otherwise 组合标签拼装查询条件

1
2
3
4
5
6
7
8
vbnet复制代码<choose>
<when test="sidx != null and sidx.trim() != ''">
order by r.${sidx} ${order}
</when>
<otherwise>
order by r.role_id asc
</otherwise>
</choose>

08 隐形绑定参数:_parameter

_parameter 参数的含义

“

当 Mapper、association、collection 指定只有一个参数时进行查询时,可以使用 _parameter,它就代表了这个参数。

另外,当使用 Mapper指定方法使用 @Param 的话,会使用指定的参数值代替。

1
2
3
4
5
6
7
bash复制代码SELECT id, grp_no grpNo, province_id provinceId, status FROM tj_group_province
<where>
...
<if test="_parameter!=null">
and grp_no = #{_parameter}
</if>
</where>

09 利用 set 配合 if 标签,动态设置数据库字段更新值

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码<update id="updateById">
UPDATE cms_label
<set>
<if test="labelGroupId != null">
label_group_id = #{labelGroupId},
</if>
dept_id = #{deptId},
<if test="recommend != null">
is_recommend = #{recommend},
</if>
</set>
WHERE label_id = #{labelId}
</update

二、Mybatis-Plus Lambda 表达式理论篇

背景

如果 Mybatis-Plus 是扳手,那 Mybatis Generator 就是生产扳手的工厂。

MyBatis 是一种操作数据库的 ORM 框架,提供一种 Mapper 类,支持让你用 java 代码进行增删改查的数据库操作,省去了每次都要手写 sql 语句的麻烦。但是有一个前提,你得先在 xml 中写好 sql 语句,也是很麻烦的。

题外话:Mybatis 和 Hibernate 的比较

  • Mybatis 是一个半 ORM 框架;Hibernate 是一个全 ORM 框架。Mybatis 需要自己编写 sql 。
  • Mybatis 直接编写原生 sql,灵活度高,可以严格控制 sql 执行性能;Hibernate的自动生成 hql,因为更好的封装型,开发效率提高的同时,sql 语句的调优比较麻烦。
  • Hibernate的 hql 数据库移植性比 Mybatis 更好,Hibernate 的底层对 hql 进行了处理,对于数据库的兼容性更好,
  • Mybatis 直接写的原生 sql 都是与数据库相关,不同数据库 sql 不同,这时就需要多套 sql 映射文件。
  • Hibernate 在级联删除的时候效率低;数据量大, 表多的时候,基于关系操作会变得复杂。
  • Mybatis 和 Hibernate 都可以使用第三方缓存,而 Hibernate 相比 Mybatis 有更好的二级缓存机制。

为什么要选择 Lambda 表达式?

Mybatis-Plus 的存在就是为了稍稍弥补 Mybatis 的不足。

在我们使用 Mybatis 时会发现,每当要写一个业务逻辑的时候都要在 DAO 层写一个方法,再对应一个 SQL,即使是简单的条件查询、即使仅仅改变了一个条件都要在 DAO层新增一个方法,针对这个问题,Mybatis-Plus 就提供了一个很好的解决方案:lambda 表达式,它可以让我们避免许多重复性的工作。

想想 Mybatis 官网提供的 CRUD 例子吧,基本上 xml 配置占据了绝大部分。而用 Lambda 表达式写的 CRUD 代码非常简洁,真正做到零配置,不需要在 xml 或用注解(@Select)写大量原生 SQL 代码。

1
2
3
4
5
ini复制代码LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(UserEntity::getSex, 0L)
.like(UserEntity::getUserName, "dun");
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("like全包含关键字查询::" + u.getUserName()));

lambda 表达式的理论基础

Java中的 lambda 表达式实质上是一个匿名方法,但该方法并非独立执行,而是用于实现由函数式接口定义的唯一抽象方法。

使用 lambda 表达式时,会创建实现了函数式接口的一个匿名类实例,如 Java8 中的线程 Runnable 类实现了函数接口:@FunctionalInterface。

1
2
3
4
csharp复制代码@FunctionalInterface
public interface Runnable {
public abstract void run();
}

平常我们执行一个 Thread 线程:

1
2
3
4
5
6
csharp复制代码new Thread(new Runnable() {
@Override
public void run() {
System.out.println("xxxx");
}
}).start();

如果用 lambda 会非常简洁,一行代码搞定。

1
scss复制代码 new Thread(()-> System.out.println("xxx")).start();

所以在某些场景下使用 lambda 表达式真的能减少 java 中一些冗长的代码,增加代码的优雅性。

lambda 条件构造器基础类:包装器模式(装饰模式)之 AbstractWrapper AbstractWrapper 条件构造器说明

  1. 出现的第一个入参 boolean condition 表示该条件是否加入最后生成的 sql 中,例如:query.like(StringUtils.isNotBlank(name), Entity::getName, name) .eq(age!=null && age >= 0, Entity::getAge, age)
  2. 代码块内的多个方法均为从上往下补全个别 boolean 类型的入参,默认为 true
  3. 出现的泛型 Param 均为 Wrapper 的子类实例(均具有 AbstractWrapper 的所有方法)
  4. 方法在入参中出现的 R 为泛型,在普通 wrapper 中是 String ,在 LambdaWrapper 中是函数(例:Entity::getId,Entity 为实体类,getId为字段id的getMethod)
  5. 方法入参中的 R column 均表示数据库字段,当 R 具体类型为 String 时则为数据库字段名(字段名是数据库关键字的自己用转义符包裹!)!而不是实体类数据字段名!!!,另当 R 具体类型为 SFunction 时项目 runtime 不支持 eclipse 自家的编译器!
  6. 使用普通 wrapper,入参为 Map 和 List 的均以 json 形式表现!
  7. 使用中如果入参的 Map 或者 List为空,则不会加入最后生成的 sql 中!

警告:

不支持以及不赞成在 RPC 调用中把 Wrapper 进行传输。

“

Wrapper 很重 传输 Wrapper 可以类比为你的 controller 用 map 接收值(开发一时爽,维护火葬场) 正确的 RPC 调用姿势是写一个 DTO 进行传输,被调用方再根据 DTO 执行相应的操作 我们拒绝接受任何关于 RPC 传输 Wrapper 报错相关的 issue 甚至 pr。

AbstractWrapper 内部结构

从上图,我们了解到 AbstractWrapper 的实际上实现了五大接口:

  • SQL 片段函数接口:ISqlSegment
1
2
3
4
5
6
7
csharp复制代码@FunctionalInterface
public interface ISqlSegment extends Serializable {
/**
* SQL 片段
*/
String getSqlSegment();
}
  • 比较值接口 Compare<Children, R>,如 等值 eq、不等于:ne、大于 gt、大于等于:ge、小于 lt、小于等于 le、between、模糊查询:like 等等
  • 嵌套接口 Nested<Param, Children> ,如 and、or
  • 拼接接口 Join,如 or 、exists
  • 函数接口 Func<Children, R>,如 in 查询、groupby 分组、having、order by排序等

常用的 where 条件表达式 eq、like、in、ne、gt、ge、lt、le。

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
scss复制代码@Override
public Children in(boolean condition, R column, Collection<?> coll) {
return doIt(condition, () -> columnToString(column), IN, inExpression(coll));
}

public Children notIn(boolean condition, R column, Collection<?> coll)

public Children inSql(boolean condition, R column, String inValue)

public Children notInSql(boolean condition, R column, String inValue)

public Children groupBy(boolean condition, R... columns)

public Children orderBy(boolean condition, boolean isAsc, R... columns)

public Children eq(boolean condition, R column, Object val)

public Children ne(boolean condition, R column, Object val)

public Children gt(boolean condition, R column, Object val)

public Children ge(boolean condition, R column, Object val)

public Children lt(boolean condition, R column, Object val)

public Children le(boolean condition, R column, Object val)

...

/**
* 普通查询条件
*
* @param condition 是否执行
* @param column 属性
* @param sqlKeyword SQL 关键词
* @param val 条件值
*/
protected Children addCondition(boolean condition, R column, SqlKeyword sqlKeyword, Object val) {
return doIt(condition, () -> columnToString(column), sqlKeyword, () -> formatSql("{0}", val));
}

SQL 片段函数接口

lambda 这么好用的秘诀在于 SQL 片段函数接口:ISqlSegment,我们在 doIt 方法找到 ISqlSegment 对象参数,翻开 ISqlSegment 源码,发现它真实的庐山真面目,原来是基于 Java 8 的函数接口 @FunctionalInterface 实现!

ISqlSegment 就是对 where 中的每个条件片段进行组装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码/**
* 对sql片段进行组装
*
* @param condition 是否执行
* @param sqlSegments sql片段数组
* @return children
*/
protected Children doIt(boolean condition, ISqlSegment... sqlSegments) {
if (condition) {
expression.add(sqlSegments);
}
return typedThis;
}

@FunctionalInterface
public interface ISqlSegment extends Serializable {

/**
* SQL 片段
*/
String getSqlSegment();
}

从 MergeSegments 类中,我们找到 getSqlSegment 方法,其中代码片段

1
scss复制代码sqlSegment = normal.getSqlSegment() + groupBy.getSqlSegment() + having.getSqlSegment() + orderBy.getSqlSegment()

这段代码表明,一条完整的 where 条件 SQL 语句,最终由 normal SQL 片段,groupBy SQL 片段,having SQL 片段,orderBy SQL 片段拼接而成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码@Getter
@SuppressWarnings("serial")
public class MergeSegments implements ISqlSegment {

private final NormalSegmentList normal = new NormalSegmentList();
private final GroupBySegmentList groupBy = new GroupBySegmentList();
private final HavingSegmentList having = new HavingSegmentList();
private final OrderBySegmentList orderBy = new OrderBySegmentList();

@Getter(AccessLevel.NONE)
private String sqlSegment = StringPool.EMPTY;
@Getter(AccessLevel.NONE)
private boolean cacheSqlSegment = true;

public void add(ISqlSegment... iSqlSegments) {
List<ISqlSegment> list = Arrays.asList(iSqlSegments);
ISqlSegment firstSqlSegment = list.get(0);
if (MatchSegment.ORDER_BY.match(firstSqlSegment)) {
orderBy.addAll(list);
} else if (MatchSegment.GROUP_BY.match(firstSqlSegment)) {
groupBy.addAll(list);
} else if (MatchSegment.HAVING.match(firstSqlSegment)) {
having.addAll(list);
} else {
normal.addAll(list);
}
cacheSqlSegment = false;
}

@Override
public String getSqlSegment() {
if (cacheSqlSegment) {
return sqlSegment;
}
cacheSqlSegment = true;
if (normal.isEmpty()) {
if (!groupBy.isEmpty() || !orderBy.isEmpty()) {
sqlSegment = groupBy.getSqlSegment() + having.getSqlSegment() + orderBy.getSqlSegment();
}
} else {
sqlSegment = normal.getSqlSegment() + groupBy.getSqlSegment() + having.getSqlSegment() + orderBy.getSqlSegment();
}
return sqlSegment;
}
}

三、Mybatis-Plus Lambda 表达式实战

01 环境准备

1. Maven 依赖

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
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>

2. 实体(表)以及 Mapper 表映射文件

  • Base 实体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@SuperBuilder(toBuilder = true)
@Data
public class BaseEntity {

@TableField(value = "created_tm", fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdTm;

@TableField(value = "created_by", fill = FieldFill.INSERT)
private String createdBy;

@TableField(value = "modified_tm", fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime modifiedTm;

@TableField(value = "modified_by", fill = FieldFill.INSERT_UPDATE)
private String modifiedBy;
}
  • 用户账号实体:UserEntity
1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@SuperBuilder(toBuilder = true)
@Data
@TableName("sys_user")
public class UserEntity extends BaseEntity{
private Long userId;
private String userName;
private Integer sex;
private Integer age;
private String mobile;
}

Mapper 操作类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码List<UserDTO> selectUsers();

UserEntity selectByIdOnXml(long userId);

@Results(id = "userResult", value = {
@Result(property = "user_id", column = "userId", id = true),
@Result(property = "userName", column = "user_name"),
@Result(property = "sex", column = "sex"),
@Result(property = "mobile", column = "mobile"),
@Result(property = "age", column = "age")
})
@Select("select * from sys_user where user_id = #{id}")
UserEntity selectByIdOnSelectAnnotation(@Param("id") long id);

@SelectProvider(type = UserSqlProvider.class, method = "selectById")
@ResultMap("BaseResultMap")
UserEntity selectByIdOnSelectProviderAnnotation(long id);

@Select("select * from sys_user where user_id = #{id} and user_name=#{userName}")
@ResultMap("BaseResultMap")
UserEntity selectByIdOnParamAnnotation(@Param("id") long id, @Param("userName") String uerName);

Mapper 表映射文件

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
ini复制代码<mapper namespace="com.dunzung.mybatisplus.query.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.dunzung.mybatisplus.query.entity.UserEntity">
<id column="user_id" property="userId"/>
<result column="user_name" property="userName"/>
<result column="sex" property="sex"/>
<result column="age" property="age"/>
<result column="mobile" property="mobile"/>
</resultMap>

<resultMap id="RelationResultMap" type="com.dunzung.mybatisplus.query.entity.UserDTO" extends="BaseResultMap">
<association property="card" column="{userId,user_id}"
select="com.dunzung.mybatisplus.query.mapper.CardMapper.selectCardByUserId"/>
<collection property="orders" column="{userId,user_id}"
select="com.dunzung.mybatisplus.query.mapper.OrderMapper.selectOrders"/>
</resultMap>

<select id="selectUsers" resultMap="RelationResultMap">
select * from sys_user
</select>

<select id="selectByIdOnXml" resultMap="BaseResultMap">
select * from sys_user where user_id = #{userId}
</select>

</mapper>
  • 订单实体:OrderEntity
1
2
3
4
5
6
7
kotlin复制代码@Data
@TableName("sys_user_card")
public class CardEntity {
private Long cardId;
private String cardCode;
private Long userId;
}

Mapper 操作类

1
2
3
java复制代码@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {
}

Mapper 表映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码<mapper namespace="com.dunzung.mybatisplus.query.mapper.OrderMapper">
<resultMap id="BaseResultMap" type="com.dunzung.mybatisplus.query.entity.OrderEntity">
<id column="order_id" property="orderId"/>
<result column="order_name" property="orderName"/>
<result column="user_id" property="userId"/>
<result column="price" property="price"/>
<result column="created_tm" property="createdTm"/>
</resultMap>

<select id="selectOrders" resultMap="BaseResultMap">
select * from biz_order where user_id = #{userId}
</select>

</mapper>
  • 身份证实体:CardEntity
1
2
3
4
5
6
7
8
9
kotlin复制代码@Data
@TableName("biz_order")
public class OrderEntity {
private Long orderId;
private String orderName;
private Integer userId;
private Date createdTm;
private Integer price;
}

Mapper 操作类

1
2
3
java复制代码@Mapper
public interface CardMapper extends BaseMapper<CardEntity> {
}

Mapper 表映射文件

1
2
3
4
5
6
7
8
9
10
ini复制代码<mapper namespace="com.dunzung.mybatisplus.query.mapper.CardMapper">
<resultMap id="BaseResultMap" type="com.dunzung.mybatisplus.query.entity.CardEntity">
<id column="card_id" property="cardId"/>
<result column="card_code" property="cardCode"/>
<result column="user_id" property="userId"/>
</resultMap>
<select id="selectCardByUserId" resultMap="BaseResultMap">
select * from sys_user_card where user_id = #{userId}
</select>
</mapper>

02 Lambda 基础篇

lambda 构建复杂的查询条件构造器:LambdaQueryWrapper

LambdaQueryWrapper 四种不同的 lambda 构造方法

  • 方式一 使用 QueryWrapper 的成员方法方法 lambda 构建 LambdaQueryWrapper
1
ini复制代码LambdaQueryWrapper<UserEntity> lambda = new QueryWrapper<UserEntity>().lambda();
  • 方式二 直接 new 出 LambdaQueryWrapper
1
ini复制代码LambdaQueryWrapper<UserEntity> lambda = new  LambdaQueryWrapper<>();
  • 方式三 使用 Wrappers 的静态方法 lambdaQuery 构建 LambdaQueryWrapper 推荐
1
ini复制代码LambdaQueryWrapper<UserEntity> lambda = Wrappers.lambdaQuery();
  • 方式四:链式查询
1
2
sql复制代码List<UserEntity> users = new LambdaQueryChainWrapper<UserEntity>(userMapper)
.like(User::getName, "雨").ge(User::getAge, 20).list();

笔者推荐使用 Wrappers 的静态方法 lambdaQuery 构建 LambdaQueryWrapper 条件构造器。

Debug 调试

为了 Debug 调试方便,需要在 application.yml 启动文件开启 Mybatis-Plus SQL 执行语句全栈打印:

1
2
3
4
yaml复制代码#mybatis
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

执行效果如下:

1 等值查询:eq

1
2
3
4
5
6
7
8
9
perl复制代码@Test
public void testLambdaQueryOfEq() {
//eq查询
//相当于 select * from sys_user where user_id = 1
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(UserEntity::getUserId, 1L);
UserEntity user = userMapper.selectOne(lqw);
System.out.println("eq查询::" + user.getUserName());
}

eq 查询等价于原生 sql 的等值查询。

1
csharp复制代码select * from sys_user where user_id = 1

2 范围查询 :in

1
2
3
4
5
6
7
8
ini复制代码@Test
public void testLambdaQueryOfIn() {
List<Long> ids = Arrays.asList(1L, 2L);
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.in(UserEntity::getUserId, ids);
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("in查询::" + u.getUserName()));
}

in 查询等价于原生 sql 的 in 查询

1
csharp复制代码select * from sys_user where user_id in (1,2)

3 通配符模糊查询:like

1
2
3
4
5
6
7
8
ini复制代码@Test
public void testLambdaQueryOfLikeAll() {
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(UserEntity::getSex, 0L)
.like(UserEntity::getUserName, "dun");
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("like全包含关键字查询::" + u.getUserName()));
}

like 查询等价于原生 sql 的 like 全通配符模糊查询。

1
sql复制代码select * from sys_user where sex = 0 and user_name like '%dun%'

4 右通配符模糊查询:likeRight

1
2
3
4
5
6
7
8
ini复制代码@Test
public void testLambdaQueryOfLikeRight() {
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(UserEntity::getSex, 0L)
.likeRight(UserEntity::getUserName, "dun");
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("like Right含关键字查询::" + u.getUserName()));
}

likeRight 查询相当于原生 sql 的 like 右通配符模糊查询。

1
sql复制代码select * from sys_user where sex = 0 and user_name like 'dun%'

5 左通配符模糊查询:likeLeft

1
2
3
4
5
6
7
8
ini复制代码@Test
public void testLambdaQueryOfLikeLeft() {
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(UserEntity::getSex, 0L)
.likeLeft(UserEntity::getUserName, "zung");
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("like Left含关键字查询::" + u.getUserName()));
}

likeLeft 查询相当于原生 sql 的 like 左通配符模糊查询。

1
sql复制代码select * from sys_user where sex = 0 and user_name like '%zung'

6 条件判断查询

条件判断查询类似于 Mybatis 的 if 标签,第一个入参 boolean condition 表示该条件是否加入最后生成的 sql 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码@Test
public void testLambdaQueryOfBoolCondition() {
UserEntity condition = UserEntity.builder()
.sex(1)
.build();
//eq 或 like 条件判断查询
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(condition.getSex() != null, UserEntity::getSex, 0L)
// 满足 bool 判断,是否进查询按字段 userName 查询
.like(condition.getUserName() != null, UserEntity::getUserName, "dun");
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("like查询::" + u.getUserName()));
}

7 利用 or 和 and 构建复杂的查询条件

1
2
3
4
5
6
7
8
9
less复制代码@Test
public void testLambdaQueryOfOr_And() {
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.eq(UserEntity::getSex, 0L)
.and(wrapper->wrapper.eq(UserEntity::getUserName,"dunzung")
.or().ge(UserEntity::getAge, 50));
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("like查询::" + u.getUserName()));
}

上面实例查询等价于原生 sql 查询:

1
csharp复制代码select * from sys_user where sex = 0 and (use_name = 'dunzung' or age >=50)

8 善于利用分页利器 PageHelpler

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码@Test
public void testLambdaPage() {
//PageHelper分页查询
//相当于 select * from sys_user limit 0,2
int pageNumber = 0;
int pageSize = 2;
PageHelper.startPage(pageNumber + 1, pageSize);
LambdaQueryWrapper<UserEntity> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(UserEntity::getAge)
.orderByDesc(UserEntity::getMobile);
List<UserEntity> userList = userMapper.selectList(lqw);
userList.forEach(u -> System.out.println("page分页查询::" + u.getUserName()));
}

上面实例查询等价于原生 sql 分页查询:

1
sql复制代码select * from sys_user order by age desc,mobile desc limit 0,2

另外,Mybatis-Plus 自带分页组件,BaseMapper 接口提供两种分页方法来实现物理分页。

  • 第一个返回实体对象允许 null
  • 第二个人返回 map 对象多用于在指定放回字段时使用,避免为指定字段 null 值出现
1
2
less复制代码IPage<T> selectPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);

注意,Mybatis-Plus 自带分页组件时,需要配置 PaginationInterceptor 分页插件。

1
2
3
4
typescript复制代码@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}

9 更新条件构造器:LambdaUpdateWrapper

1
2
3
4
5
6
7
8
csharp复制代码@Test
public void testLambdaUpdate() {
LambdaUpdateWrapper<UserEntity> luw = Wrappers.lambdaUpdate();
luw.set(UserEntity::getUserName, "dunzung01")
.set(UserEntity::getSex, 1);
luw.eq(UserEntity::getUserId, 1);
userMapper.update(null, luw);
}

03 进阶篇

1. Association

Association 标签适用于表和表之间存在一对一的关联关系,如用户和身份证存在一个人只会有一个身份证号,反过来也成立。

1
2
3
4
5
csharp复制代码@Test
public void testOnAssociationTag() {
List<UserDTO> userList = userMapper.selectUsers();
userList.forEach(u -> System.out.println(u.getUserName()));
}

XML配置

1
2
3
4
ini复制代码<resultMap id="RelationResultMap" type="com.dunzung.mybatisplus.query.entity.UserDTO" extends="BaseResultMap">
<association property="card" column="{userId,user_id}"
select="com.dunzung.mybatisplus.query.mapper.CardMapper.selectCardByUserId"/>
</resultMap>

2. Collection

Collection 标签适用于表和表之间存在一对多的关联关系,如用户和订单存在一个人可以购买多个物品,产生多个购物订单。

1
2
3
4
5
csharp复制代码@Test
public void testOnCollectionTag() {
List<UserDTO> userList = userMapper.selectUsers();
userList.forEach(u -> System.out.println(u.getUserName()));
}

XML配置

1
2
3
4
5
ini复制代码<resultMap id="RelationResultMap" type="com.dunzung.mybatisplus.query.entity.UserDTO" extends="BaseResultMap">
<collection property="orders"
column="{userId,user_id}"
select="com.dunzung.mybatisplus.query.mapper.OrderMapper.selectOrders"/>
</resultMap>

注意 Association 和 Collection 先后关系,在编写 ResultMap 时,association 在前,collection 标签在后。

1
2
3
4
5
6
ini复制代码 <resultMap id="RelationResultMap" type="com.dunzung.mybatisplus.query.entity.UserDTO" extends="BaseResultMap">
<association property="card" column="{userId,user_id}"
select="com.dunzung.mybatisplus.query.mapper.CardMapper.selectCardByUserId"/>
<collection property="orders" column="{userId,user_id}"
select="com.dunzung.mybatisplus.query.mapper.OrderMapper.selectOrders"/>
</resultMap>

如果二者颠倒顺序会提示错误。

3. 元对象字段填充属性值:MetaObjectHandler

MetaObjectHandler元对象字段填充器的填充原理是直接给 entity 的属性设置值,提供默认方法的策略均为:

“

如果属性有值则不覆盖,如果填充值为 null 则不填充,字段必须声明 TableField 注解,属性 fill 选择对应策略,该声明告知 Mybatis-Plus 需要预留注入 SQL字段。 TableField 注解则是指定该属性在对应情况下必有值,如果无值则入库会是 null。

自定义填充处理器 MyMetaObjectHandler 在 Spring Boot 中需要声明 @Component 或 @Bean 注入,要想根据注解 FieldFill.xxx,如:

1
2
3
4
5
6
less复制代码@TableField(value = "created_tm", fill = FieldFill.INSERT)
private LocalDateTime createdTm;

@TableField(value = "modified_tm", fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime modifiedTm;

和字段名以及字段类型来区分必须使用父类的 setInsertFieldValByName 或者 setUpdateFieldValByName 方法,不需要根据任何来区分可以使用父类的 setFieldValByName 方法 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码/**
* 属性值填充 Handler
*
* @author 猿芯
* @since 2021/3/30
*/
@Component
public class FillMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
this.setInsertFieldValByName("createdTm", LocalDateTime.now(), metaObject);
this.setInsertFieldValByName("createdBy", MvcContextHolder.getUserName(), metaObject);
this.setFieldValByName("modifiedTm", LocalDateTime.now(), metaObject);
this.setFieldValByName("modifiedBy", MvcContextHolder.getUserName(), metaObject);
}

@Override
public void updateFill(MetaObject metaObject) {
this.setUpdateFieldValByName("modifiedTm", LocalDateTime.now(), metaObject);
this.setUpdateFieldValByName("modifiedBy", MvcContextHolder.getUserName(), metaObject);
}
}

一般 FieldFill.INSERT 用父类的 setInsertFieldValByName 方法更新创建属性(创建人、创建时间)值;FieldFill.INSERT_UPDATE 用父类的 setUpdateFieldValByName 方法更新修改属性(修改人、修改时间)值;如果想让诸如 FieldFill.INSERT 或 FieldFill.INSERT_UPDATE 任何时候不起作用,用父类的 setFieldValByName 设置属性(创建人、创建时间、修改人、修改时间)值即可。

4. 自定义SQL

使用 Wrapper 自定义 SQL 需要 mybatis-plus 版本 >= 3.0.7 ,param 参数名要么叫 ew,要么加上注解 @Param(Constants.WRAPPER) ,使用 ${ew.customSqlSegment} 不支持 Wrapper 内的 entity生成 where 语句。

注解方式

1
2
less复制代码@Select("select * from mysql_data ${ew.customSqlSegment}")
List<MysqlData> getAll(@Param(Constants.WRAPPER) Wrapper wrapper);

XML配置

1
2
3
4
sql复制代码List<MysqlData> getAll(Wrapper ew);
<select id="getAll" resultType="MysqlData">
SELECT * FROM mysql_data ${ew.customSqlSegment}
</select>

四、Mybatis-Plus lambda 表达式的优势与劣势

通过上面丰富的举例详解以及剖析 lambda 底层实现原理,想必大家会问:” lambda 表达式似乎只支持单表操作?”

据我对 Mybatis-Plus 官网的了解,目前确实是这样。依笔者实际运用经验来看,其实程序员大部分开发的功能基本上都是针对单表操作的,Lambda 表达式的优势在于帮助开发者减少在 XML 编写大量重复的 CRUD 代码,这点是非常重要的 nice 的。很显然,Lambda 表达式对于提高程序员的开发效率是不言而喻的,我想这点也是我作为程序员非常喜欢 Mybatis-Plus 的一个重要原因。

但是,如果涉及对于多表之间的关联查询,lambda 表达式就显得力不从心了,因为 Mybatis-Plus 并没有提供类似于 join 查询的条件构造器。

lambda 表达式优点:

  1. 单表操作,代码非常简洁,真正做到零配置,如不需要在 xml 或用注解(@Select)写大量原生 SQL 代码
  2. 并行计算
  3. 预测代表未来的编程趋势

lambda 表达式缺点:

  1. 单表操作,对于多表关联查询支持不好
  2. 调试困难
  3. 底层逻辑复杂

五、总结

Mybatis-Plus 推出的 lambda 表达式致力于构建复杂的 where 查询构造器式并不是银弹,它可以解决你实际项目中 80% 的开发效率问题,但是针对一些复杂的大 SQL 查询条件支持地并不好,例如一些复杂的 SQL 报表统计查询。

所以,笔者推荐单表操作用 lambda 表达式,查询推荐用 LambdaQueryWrapper,更新用 LambdaUpdateWrapper;多表操作还是老老实实写一些原生 SQL ,至于原生 SQL 写在哪里? Mapper 文件或者基于注解,如 @Select 都是可以的。

参考

  • mp.baomidou.com/guide/wrapp…
  • www.jianshu.com/p/613a6118e…
  • blog.csdn.net/Solitude_w/…
  • blog.csdn.net/weixin_4447…
  • blog.csdn.net/weixin_4449…

作者:猿芯
来源:www.toutiao.com/i6951307172…

本文转载自: 掘金

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

今日头条 ANR 优化实践系列 - 告别 SharedPre

发表于 2021-05-14

简述

前面系列文章中介绍了安卓系统ANR设计原理以及我们在实际工作中对ANR进行监控得到的一些方案,基于这些常规的监控治理,ANR问题得到了有效的抑制。但是有些系统组件的设计初衷与开发人员在实际使用过程中的背离,导致的问题亟待解决。当前文章针对实际开发过程中滥用sp导致的ANR问题,如何从系统层面跳过Google设计缺陷,规避ANR问题。

Google在设计之初为了方便开发者,实现了一套轻量级的数据持久化方案——SharedPreference(以下简称sp),因为其简便的API,方便的使用方式,得到开发者的青睐,对其依赖越来越重。在应用版本不断迭代过程中发现Google说的轻量级的数据存储是有原因的,越是重量级的应用出现的ANR问题越严重。本文从源码层面分析sp文件的加载解析和写入过程出发,分析导致ANR问题的原因分析以及相关的优化解决方案。

SP导致ANR原因分析

经常会遇到两类关于SharedPreference问题,以下分别介绍导致这两类ANR问题的原因和优化方案。

问题一:sp创建以后,会单独的使用一个线程来加载解析对应的sp文件。但是当UI线程尝试访问sp中内容时,如果sp文件还未被完全加载解析到内存,此时UI线程会被block,直到sp文件被完全加载到内存中为止。具体ANR线程堆栈如下:

主要原因是sp文件未被加载或解析到内存中,此时无法直接使用sp提供的接口。sp被创建的时候会同时启动一个线程加载对应的sp文件,执行startLoadFromDisk();

在startLoadFromDisk()时,标记sp不可使用状态,后期无论是尝试读数据或者写数据,读写线程都会被直接block,直到sp文件被全部加载解析完毕。

线程在读或写时,都会走到awaitLoadedLocked()逻辑,在上图的mLoaded为false即sp文件尚未加载解析到内存,此时读写线程会直接被block在mLock锁上,直到loadFromDisk()方法执行完毕。

sp文件完全加载解析到内存中,直接唤起所有在等待在当前sp的读写线程。

问题二:Google系统为了确保数据的跨进程完整性,前期应用可以使用sp来做跨进程通信,在组件销毁或其他生命周期的同时为了确保当前这个写入任务必须在当前这个组件的生命周期完成写入,此时主线程会在组件销毁或者组件暂停的生命周期内等待sp完全写入到对应的文件中,如下图UI线程被block在了QueuedWork.waitToFinish()处,接下来基于源码从apply开始到最后写入文件整体流程梳理找出问题根源。

具体需要等待文件写入的消息在AcitivtyThread的H中,具体消息类型如下:

1
2
3
4
5
arduino复制代码public static final int SERVICE_ARGS = 115;
public static final int STOP_SERVICE = 116;
public static final int PAUSE_ACTIVITY = 101;
public static final int STOP_ACTIVITY_SHOW = 103;
public static final int SLEEPING = 137;

由于Google官方设计之初是轻量级的数据存储方式,这种等待行为不会有什么问题,但是实际使用过程中由于sp过度使用,这个等待的时间被不可控的拉长,直到最后出现ANR,这种问题越在业务繁重的应用上体现越明显。具体问题堆栈如下,全是系统堆栈,接下来从waitToFinish入手分析,剖析这个ANR的根源。具体ANR堆栈如下:

前期sp接口只有commit接口,接口同步写入文件,这个接口直接影响开发者使用,于是Google官方对外提供了异步的apply接口,由于开发者认为这个异步是真正意义上的异步,大规模的使用sp的appy接口,就是这种apply的实现方式导致了业务量大的APP深受这个apply设计缺陷导致的ANR问题影响。

apply接口整体的详细设计思路如下图(基于Android8.0及以下版本分析):

整体的思路简单梳理如下:

  1. sp.apply(),写入内存同时得到需要同步写入文件的数据集合MemoryCommitResult:

  1. 将MemoryCommitResult封装成Runnable抛到子线程queued-work-looper中;
  2. 在子线程中将MemoryCommitResult中的mapToWriteToDisk对应的key-value写入到文件中去;
  3. 文件写入完成以后,会执行MemoryCommitResult的setDiskWriteResult方法,关键的步骤writtenToDiskLatch.countDown() 出现了;
  4. 如下当主线中执行到QueuedWork.waitToFinish()的时候;

  1. 主线程到底在干什么,这个时候得从QueuedWork.add(Runnable finisher)入手,具体Runnable如下图,这个地方就是啥也没干,直接等在了mcr.writtenToDiskLatch.await()上,这里大家应该有点印象,就是步骤4中子线程在写完文件以后直接释放的那个锁

结论:尽管整体API的流程分析异常的复杂,把一个runnable封装了一层又一层,从这个线程抛到那个线程,子线程执行完写入文件以后会释放锁,主线程执行到某些地方得等待子线程把写入文件的行为执行完毕,但是整体的思路还是比较简单的。造成这个问题的根源就是太多pending的apply行为没有写入到文件,主线程在执行到指定消息的时候会有等待行为,等待时间过长就会出现ANR。

尽管Google官方在Android8.0及以后版本对sp写入逻辑进行优化,期望是在上述步骤6中UI线程不是傻傻的等,而是帮助子线程一起写入,但是由于是主线程保守协助,并没有很好的解决这个问题。

解决方案

问题一:针对加载很慢的问题,一般使用的比较多的是采用预加载的方式来触发到这个sp文件的加载和解析,这样在真正使用的时候大概率sp已经加载解析完毕了;真正需要处理的是核心场景的sp一定不能太大,Google官方的声明还是有必要遵守一下,轻量级的数据持久化存储方式,不要存太多数据,避免文件过大,导致前期的加载解析耗时过久。

问题二:至于Google为什么要这么设计,提出了自己的几个猜想:

  1. Google希望做到数据可能尽可能及时的写入文件,但是这样等待没有任何意义,主线程直接等待并不会提升写入的效率;
  2. 期望sp实时写入文件,以方便跨进程的时候可以实时的访问到sp内的文件,这种异步写入方式本身就没办法确保实时性;
  3. 可能是在组件发生状态切换的时候,这个时候如果进程内没有啥组件,进程的优先级可能降低,存在进程会在系统资源吃紧的时候被系统干掉,这种概率极低,几乎可以忽略不计;
  4. 感觉最大的可能性就是Google官方当时是为了从commit无缝的切换到apply,依然模拟原来的commit行为,只是将原来的每次写入文件一次改成多次commit行为最后一次性apply在主线程等待所有的写入行为一次性全部写入。

通过以上假设,发现这里的主线程等待子线程写入根本没有什么意义,因此希望可以通过一些必要的手段跳过这种无用的等待行为,在研究了所有的SharedPreference相关的逻辑后找到以下入手点。以下是8.0以下版本的优化策略,8.0及以上版本处理方式类似:

如果需要主线程在waitToFinish的时候直接跳过去,让toTinish.run()不执行,显然不可能,如果能让sPendingWorkFinishers.poll()返回为null,则这里的等待行为直接就跳过去了,sPendingWorkFinishers是个ConcurrentLinkedQueue集合,可以直接动态代理这个集合,覆写poll方法,让其永远返回null,这个时候UI永远不会等待子线程写入文件完毕,事实证明这种方式简单有效。

针对这种写入等待的ANR问题,还有一种就是全局替换写入方式,通过插桩的方式,替换所有的API实现,采用其他的存储方式,这种方式修复成本和风险较大,但是后期可以随机的替换存储方式,使用比较灵活。

方案收益

通过在字节系多个产品的验证,方案稳定有效,相应堆栈导致的ANR问题消灭殆尽,ANR收益明显,相应的界面跳转等场景流畅性得到了明显的改善。

展望

Google 新增加了一个新 Jetpack 的成员 DataStore,未来可能用来替换 SharedPreferences, DataStore 应该是开发者期待已久的库,DataStore 是基于 Flow 实现的,一种新的数据存储方案。详细介绍网上有很多参考资料。

优化实践更多参考

今日头条ANR 优化实践系列(1)-设计原理及影响因素

今日头条ANR 优化实践系列(2)-监控工具与分析思路

今日头条ANR 优化实践系列(3)-实例剖析集锦

今日头条ANR 优化实践系列(4)- Barrier 导致主线程假死

Android 平台架构团队

我们是字节跳动 Android 平台架构团队,以服务今日头条为主,面向 GIP,同时服务公司其他产品,在产品性能稳定性等用户体验,研发流程,架构方向上持续优化和探索,满足产品快速迭代的同时,追求极致的用户体验。

如果你对技术充满热情,想要迎接更大的挑战和舞台,欢迎加入我们,北京,深圳均有岗位,感兴趣发送邮箱:tech@bytedance.com ,邮件标题:姓名 - GIP - Android 平台架构。


欢迎关注 「字节跳动技术团队」

本文转载自: 掘金

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

ConcurrentHashMap的扩容方法transfer

发表于 2021-05-14

主要细节问题:

  1. 什么时候触发扩容?扩容阈值是多少?
  2. 扩容时的线程安全怎么做的?
  3. 其他线程怎么感知到扩容状态,从而一起进行扩容?
  4. 多个线程一起扩容时,怎么拆分任务,是不是任务粒度越小越好?
  5. ConcurrentHashMap.get(key)方法是没有加锁的,怎么保证在这个扩容过程中,其他线程的get(key)方法能获取到正确的值,不出现线程安全问题?

魔鬼在细节里,一起看下源码,然后回答下上面的细节问题,先看下触发扩容的代码,在往map中put新数据后会调用这个addCount(long x, int check)方法,计算当前map的容量,当容量达到扩容阈值时会触发扩容逻辑。

触发扩容源码:

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复制代码private final void addCount(long x, int check) {
// 借用Longadder的设计思路来统计map的当前容量,减少锁竞争,详细见下面的分析
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// s是当前的容量,sizeCtl是扩容阈值,当当前容量大于扩容阈值时,触发扩容,在扩容逻辑被触发前sizeCtl是正数,表示的下次触发扩容的阈值,
// 而在进入扩容逻辑后,sizeCtl会变成负数,并且sizeCtl是32位的int类型,高16位是扩容的邮戳,低16位是同时进行扩容时的线程数,在某个线程进入扩容时会修改sizeCtl值,在下面的代码里能看到这个修改逻辑
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 生成当前扩容流程邮戳,n是当前数组的长度,当n相同时,邮戳肯定也是一样的,计算逻辑详见下面分析1
int rs = resizeStamp(n);
// sc小于0 标识已经在扩容中,因为在触发扩容时会通过CAS修改sc这个值
// sc值是int类型,32位,低16位记录了当前进行扩容的线程数,高16位就是邮戳,这个逻辑见下面分析2
if (sc < 0) {
// (sc >>> RESIZE_STAMP_SHIFT) != rs 说明不是同一个扩容流程,放弃扩容
// sc == rs + 1 || sc == rs + MAX_RESIZERS 这个条件是个bug,应该写成(sc >>> RESIZE_STAMP_SHIFT) == rs + 1 || (sc >>> RESIZE_STAMP_SHIFT) == rs + MAX_RESIZERS
// 在jdk12中已被修复,oracle官网修复链接是 https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
// (nt = nextTable) == null 有线程去扩容时,必然会生成nextTable,所以这里不需要处理
// transferIndex <= 0 transferIndex是标记当前还未迁移的桶的下标,如果小于等于0,则表示已经迁移完,不需要做处理
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 在进入扩容流程后会将sizeCtl值+1,sizeCtl的低16位表示当前并发扩容的线程数
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// sc大于0 则表示没有进入扩容逻辑,则通过CAS将sizeCtl值修改成 (rs << RESIZE_STAMP_SHIFT) + 2
// 这里的rs就是上面生成的扩容邮戳,这里会将rs向左位移16位,这样低16位用来记录并发扩容的线程数,高16位用来表示扩容邮戳,至于为什么要+2,我也没有理解...
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
  • 容量计算逻辑:
    在计算map容量的逻辑里借用了LongAdder的思想,如果ConcurrentHashMap里用一个size来记录当前容量的话,那么在并发put时,所有的线程都回去竞争修改这个变量,竞争会非常激烈,性能低下,那么LongAdder的思路是降低锁的粒度,我维护一个Long的数组,多个线程并发修改时,选取数组中没有被占用的Long进行加减,最后计算结果时我将数组内的数字加起来近就行了,这样就提升了数倍的吞吐,减少锁竞争的改了,所以这里也是一样维护了一个CounterCell数组,CounterCell类里就是一个long属性,当调用size()方法获取当前容量时,只需要将这个数组里的所有值加起来就行了,CounterCell代码如下:
1
2
3
4
java复制代码static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
  • 扩容邮戳计算逻辑:
1
2
3
4
5
6
java复制代码int rs = resizeStamp(n); 


static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

Integer.numberOfLeadingZeros(n)是计算n的二进制下高位连续0的个数,比如n转成二进制后是0000 0000 0000 1000 1111 0101 0110 1101,那么得到的结果就是12,因为高位是12个连续的0,而(1 << (RESIZE_STAMP_BITS - 1))中RESIZE_STAMP_BITS的值是16,所以最后的结果转成二进制是1000 0000 0001 1100

这里有三个关键点:

  1. sizeCtl在没有触发扩容时,是用来表示扩容阈值的,这时候sizeCtl是个正数,当map内数据数量达到这个阈值时,会触发扩容逻辑
  2. 当某个线程触发扩容时,会通过CAS修改sizeCtl值,修改的逻辑是将上面生成的扩容邮戳向左位移16位,然后+2,这时候由于符号位是1(因为邮戳的算法决定了把邮戳向左位移16位后,符号位是1),所以sizeCtl一定是个负数,也正是由于是cas操作,所以只会有一个线程cas成功并开启扩容流程,不会有多个扩容流程被开启。
  3. 当sizeCtl为负数时,说明在扩容中,这时候其他线程可以一起扩容,需要先通过cas将sizeCtl+1,这样可以通过sizeCtl的低16位来判断有多少并发线程在一起做扩容,从而判断哪个线程最后完成扩容,然后做收尾工作,这个收尾工作包括将当前对象的table指向新表,将sizeCtl重新设置成表示扩容阈值的正数等

下面看下扩容源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
java复制代码// tab是旧表,nextTab是一个两倍容量的空的新表
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// stide是步长的意思,会将旧表按照这个步长进行分段,在并发扩容时,每个线程只负责自己段内数据的转移
int n = tab.length, stride;
// NCPU是当前系统的内核数,如果内核数只有一个,那么就不需要进行并发扩容,因为扩容是个纯计算密集型的逻辑,只有一个核心的时候反而得不偿失,因为无法真正的并发,反而会额外付出线程上下文切换的开销
// 这里步长最小是16,也就是说每个线程最少要负责16个桶的数据迁移,这个值设置的太小会导致并发线程数增多,从而导致线程间的竞争变大,这个竞争是只下面的一些CAS逻辑,比如对transferIndex、sizeCtl变量的cas操作
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 如果新表没有初始化,则新建一个双倍容量的新表
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 设置【开始数据转移】的桶的下标,从尾部的桶开始往头部逐个处理,将transferIndex设置为老表的length(比最后一个桶的下标大1,所以后面的代码会-1)
transferIndex = n;
}
int nextn = nextTab.length;
// 生成用于表示【扩容中】状态的节点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 线程每次根据步长从数组上截取一段桶,如果线程处理完自己截取的一段内的桶后,还有未处理的数据,则需要重新从数组上截取一段来处理
// true则标识当前线程需要继续在老表的数组上截取新的一段桶来处理数据(可能没有线程来帮忙,就只能自己一个人干完了)
boolean advance = true;
// 标记是否已结束扩容,做收尾工作
boolean finishing = false; // to ensure sweep before committing nextTab
// i是当前线程需要转移的桶的下标,bound是当前线程负责的桶的最小下标
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 这个while的逻辑就是为了检查当前线程负责的段内的桶是否都处理完毕,如果处理完毕,则看下老表的数据里是否还有未处理的桶,如果有未处理的桶,则再次截取一段来处理
while (advance) {
int nextIndex, nextBound;
// 如果下一个桶的下标(--i是下一个需要操作的桶的下标)还在自己负责的段内,就不需要截取新段了,就继续处理下一个桶的数据
// 如果已经结束,则不需要继续截取新的段
if (--i >= bound || finishing)
advance = false;
// transferIndex用来表示这个下标及其后面的桶都已经被其他线程处理了,新的线程需要从transferIndex往前截取自己需要负责的桶,如果transferIndex小于等于0说明桶都已经转移完毕,不需要再处理了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 以nextIndex(在上面已经赋值为transferIndex)为起始位置,往数组头部方向截取相应步长的段来转移数据,通过cas将transferIndex设置到新的下标
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// cas成功后,设置当前线程负责的下标边界(比如负责下标32到48的桶,那么这个bound就是32)
bound = nextBound;
// cas成功后,设置当前线程开始处理的桶的下标(比如负责下标32到48的桶,那么这个i就是48)
// transferIndex默认是从tab.length开始取值,所以要减1来表示正确的下标
i = nextIndex - 1;
// cas成功则表示当前线程已经成功截取了自己需要负责的一段数据了,不需要再往前截取了
advance = false;
}
}
// i是需要转移的桶的下标,n是老表的容量
// i<0说明旧表中的桶都已经转移完毕
// i>=n|| i + n >= nextn 不是很明白这个判断条件,正常情况下,i作为开始转移的桶的下标肯定会小于老表的容量的,因为转移的是老表内的桶
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 判断是否已经完成扩容,已完成扩容则做收尾逻辑
if (finishing) {
// 完成扩容后,将引用设置为null
nextTable = null;
// 将table引用指向新表,这里的table是个volatile变量,所以这个赋值操作对其他线程是可见的
table = nextTab;
// 设置新的扩容阈值,将阈值设置为新容量的3/4
// 这里的n是老表的容量,因为是双倍扩容,所以新表容量是2n,下面计算的结果是2n-0.5n = 1.5n,也就是新表容量的3/4
sizeCtl = (n << 1) - (n >>> 1);
// 返回结果,扩容结束
return;
}
// 在扩容开始时,会将sizeCtl设置成一个负数,每次有新的线程并发扩容时,会将sizeCtl+1,而当有线程处理完扩容逻辑后,再减1,以此来判断是否是最后一个线程
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// cas成功,则判断当前线程是不是最后一个完成扩容的线程,由最后一个完成扩容逻辑的线程将finishing和advance设为true,重新循环到上面的if(finishing)里的收尾逻辑
// 这里减2是因为在执行扩容的入口处,第一个触发扩容的线程会负责将sc加2,至于为什么第一个扩容的线程要加2,而不是加1,这个没理解
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果是空桶,则在老表对应的下标出放一个ForwardingNode,在有别的线程往这个空桶写数据时会感知到扩容过程,一起来扩容
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// ForwardingNode节点的hash值就是MOVED(在ForwardingNode的构造方法里会设置hash值为MOVED),说明已经有线程处理了这个桶内的数据
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 在桶上加锁,防止对同一个桶数据并发操作
synchronized (f) {
// 有点双重校验锁的味道,防止获得锁后,该桶内数据被别的线程插入了新的数据,因为这个f是在未加锁之前获取的node对象,在这期间,可能这个下标处插入了新数据
// 比如有别的线程调用了put方法往这个桶内链表插入新节点,这时候这个桶的node就变成了新插入的数据node(put操作会生成新的node,并将新node的next引用指向原node)
// 如果不做这层校验,会导致新加入到桶内的数据没有被处理,导致数据丢失
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// fh>=0表示是正常的链表
if (fh >= 0) {
// 这里需要注意的是在put操作里是通过hash&(n-1)来选取下标位置,表容量n都是2的幂,所以这里hash&n的结果只有两个要么是n要么是0
// 值为0时的节点在新表的i下标出,而值为n的节点需要迁移到新表的i+n下标下,因为是双倍扩容
// 所以老表下标为i的桶内的数据在迁移rehash时,一半仍然在下标为i的桶内,另一半在i+n的桶内,不会出现第三种情况
int runBit = fh & n;
Node<K,V> lastRun = f;
// 这个循环的目的有两个
// 1、遍历出整个链表尾部不需要改变next指针的最长链,这样可以将这个链整个搬到新桶内,不用再逐个遍历了
// 2、由于是将老的完整节点链条搬到新桶内,所以也就不需要创建新的node节点,减少迁移过程中的gc压力
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
// 逐个遍历节点,0表示仍然放到下标为i的桶内的链表
// 这里每次都是生成新的node对象而不修改原node对象的next指针,这也是get()方法不用加锁的关键所在
// 但是会带来gc压力,所以才有上面的那次遍历,希望减少对象的创建
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
// 否则就是放到下标为i+n的桶内的链表
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 设置新表的下标为i的桶内数据
setTabAt(nextTab, i, ln);
// 设置新表的下标为i+n的桶内数据
setTabAt(nextTab, i + n, hn);
// 将老表下标为i的桶内放上ForwardingNode对象,用来标识当前处于扩容过程
setTabAt(tab, i, fwd);
// 处理完后,将这个字段设置为true,以便走到上面的while(advance)里检查当前线程负责的数据是否处理完成,并且查看是否需要截取新段
advance = true;
}
// 红黑树结构的迁移,逻辑与链表差不多,也是将整棵树拆成两颗,一棵树放到下标为i的桶内,一棵放到下标i+n的桶内
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}

看到这里可以回答上面的问题:

  1. 什么时候触发扩容?扩容阈值是多少?

在每次往map中put数据时会重新计算map中的size,如果达到扩容阈值就会触发扩容逻辑,扩容因子是0.75,即:当容量达到总容量的3/4时,触发扩容
2. 扩容时的线程安全怎么做的?
这个要细化下线程安全的场景,分为下面几种:

* 扩容线程与扩容线程间的并发场景
扩容线程和扩容线程间在进行任务分配时,是从数组尾部往头部以桶为单位截取,并且用来标记已分配区域的指针`transferIndex`是`volatile`修饰的,所以线程间是可见的,通过cas来修改`transferIndex`值,保证线程间没有重复的桶
![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/8c2b1842d408c8e05732f9bd96044e1aed7738a224ebb74e4b794bea811185b6)
* 扩容线程与写线程的并发场景
这里要分两种情况:
一、在触发扩容流程时,需要通过CAS将sizeCtl从正数改成负数,并且+2,这样只会有一个线程cas成功,避免其他的写线程也触发扩容流程。
二、怎么避免其他写线程往处于扩容中、扩容完毕的桶里写数据,因为扩容线程是遍历桶内的链表或者B树来rehash,如果往已经遍历的链表或者B树中插入新数据,扩容线程是无法感知到的,会导致新表中没有这些数据,这个要结合`put(k,v)`方法来说,对于空桶来说,不管是put操作还是扩容操作,都是通过cas操作来往空桶中添加数据,所以在出现并发往空桶写时,只会有一个线程成功,而不管是put的线程失败还是扩容的线程失败时,都会重新获取里面的值,再重新触发对应的put或者扩容逻辑,避免并发问题,`put(k,v)`代码截图如下:
![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/81f8aecf596a898c4d192402ab3e3b8b483299612bc5e2e32cf31e8497cb4960)
对于有数据的桶,put操作和扩容操作都是通过`synchronized`在桶上加锁来避免并发写问题。
* 扩容线程与读线程之间的并发场景
这个在第5个问题里解答
  1. 其他线程怎么感知到扩容状态,从而一起进行扩容?
    在对某个桶进行扩容时候,在完成扩容后会生成一个ForwardingNode放在老表的对应下标的位置下,当有其他线程修改这个桶内数据时,如果发现这个类型的节点,就会一起进行扩容,put(k,v)代码截图如下:
    在这里插入图片描述
  2. 多个线程一起扩容时,怎么拆分任务,是不是任务粒度越小越好?
    扩容任务是一个纯计算逻辑的任务,所以会根据机器内核数来决定,如果只有一个核,则由一个线程处理完就行了,这时候引入多线程扩容反而会引入上线文切换开销,同时源码里设置的每个线程负责的桶的数量最少是16,因为粒度太小的话,会导致线程的cas操作竞争太多,比如对transferIndex、sizeCtl变量的cas操作
  3. ConcurrentHashMap.get(key)方法是没有加锁的,怎么保证在这个扩容过程中,其他线程的get(key)方法能获取到正确的值,不出现线程安全问题?
    线程安全场景是,get(key)在获取下标后,数组可能已经扩容,数据被rehash,这时候通过老的下标可能会取不到值,这里是用读写分离的思路解决,这个读写分离在迁移桶内数据过程中、迁移桶内数据完毕、整个扩容完成 三个阶段都能体现出来:
* 在转移桶内数据时,不移动桶内数据并且不修改桶内数据的next指针,而是new一个新的node对象放到新表中,这样不会导致读取数据的线程在遍历链表时候因为next引用被更改而查询不到数据;
* 在桶内数据迁移完后,在原table的桶内放一个`ForwardingNode`节点,通过这个节点的`find(k)`方法能获取到对应的数据;
* 在整个库容完成后,将新表引用赋值给`volatile`的变量table,这样更新引用的动作对其他线程可见;从而保证在这三个过程中都能读取到正确的值。

本文转载自: 掘金

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

盘点 JPA SQL 解析 | Java Debug 笔

发表于 2021-05-13

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

JPA 使用过程中 , 经常会出现解析的异常 ,通过以下流程 , 有利于找到对应的节点

1.1 前置知识点

EntityManager

EntityManager 实例与持久性上下文关联。

持久性上下文是一组实体实例,其中对于任何持久性实体标识,都存在唯一的实体实例。在持久化上下文中,管理实体实例及其生命周期。EntityManager API 用于创建和删除持久性实体实例,根据实体的主键查找实体,以及查询实体。

persistent 简介

persistent 是持久化上下文 , 用于将事务持久化

持久性上下文处理一组实体,这些实体包含要在某个持久性存储中持久化的数据(例如数据库)。特别是,上下文知道一个实体在上下文和底层持久性存储方面可能具有的不同状态(例如托管、分离)。

persistent 功能

  • 持久性上下文通常通过 EntityManager 访问
  • 每个EntityManager实例都与一个持久化上下文相关联。
  • 在持久化上下文中,管理实体实例及其生命周期。
  • 持久化上下文定义了一个作用域,特定实体实例在这个作用域下被创建、持久化和删除。
  • 持久化上下文类似于包含一组持久化实体的缓存,因此一旦事务完成,所有持久化对象都将从EntityManager的持久化上下文中分离出来,不再进行管理。

persistent 的分类

  • Transaction-scoped persistence context
  • Extended-scoped persistence context

二 . 逻辑流程

2.1 SQL 解析主流程

主流程可以分为几个部分 :

Execute Action 入口流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码// Step 1 : ExecutableList 处理
C51- ActionQueue
F51_01- LinkedHashMap<Class<? extends Executable>,ListProvider>
M51_01- executeActions() : 执行所有当前排队的操作
FOR- 循环所有的 ListProvider
- 通过当前 ListProvider 获取 ExecutableList , 调用 M51_02
M51_02- executeActions(ExecutableList<E> list)
FOR- 循环list , 调用 ExecutableList.execute() -> M52_01


// Step 2 : Entity 实体类的处理
C52- EntityInsertAction
M52_01- execute()
// Step 1 : 参数准备
- EntityPersister persister = getPersister();
- SharedSessionContractImplementor session = getSession();
- Object instance = getInstance();
- Serializable id = getId();
// Step 2 : 执行 insert 流程
- persister.insert( id, getState(), instance, session ) -> M53_01
// Step 3 : insert 操作的执行
- PersistenceContext persistenceContext = session.getPersistenceContext();
- EntityEntry entry = persistenceContext.getEntry( instance )
- entry.postInsert( getState() )
//........ 后续操作在操作流程中再详细看

M54_01M54_02M55_02M54_03M56_01statement 请求流程statement 创建返回 statement对 statement 再次处理注册JDBC语句M54_01M54_02M55_02M54_03M56_01

Insert 操作准备流程 :

注意其中的核心逻辑 M54_01

其中主要是 Statement 的处理 , 请求和创建 :

  • AbstractEntityPersister # insert -> M53_01
  • AbstractEntityPersister # insert -> M53_02
  • StatementPreparerImpl # prepareStatement -> M54_01
  • StatementPreparerImpl # buildPreparedStatementPreparationTemplate -> M54_02
  • HikariProxyConnection # prepareStatement
  • ProxyConnection # prepareStatement
  • ConnectionImpl # prepareStatement -> M55_01
  • ConnectionImpl # clientPrepareStatement -> M55_02

postProcess 处理流程 , statement 注册

  • StatementPreparerImpl # postProcess -> M54_03 (M54_01 -> M54_03 )
  • ResourceRegistryStandardImpl # register -> M56_01
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
java复制代码// Step 1 : 核心中间处理器    
C53- AbstractEntityPersister
M53_01- insert(Serializable id, Object[] fields, Object object, SharedSessionContractImplementor session)
1- session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql, callable ) -> M54_01
?- 构建 PreparedStatement
2- expectation.verifyOutcome(session.getJdbcCoordinator().getResultSetReturn().executeUpdate( insert ), insert, -1);
M53_02- insert(Serializable id,Object[] fields,boolean[] notNull,int j,String sql,Object object,SharedSessionContractImplementor session)
1- session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql, callable )
?- 此处获取 statement


// Step 2 : statement 执行语句
C54- StatementPreparerImpl
M54_01- prepareStatement(String sql, final boolean isCallable) -> M54_02
- PreparedStatement preparedStatement = doPrepare() : 构建 statement
- 执行 postProcess -> M54_03
M54_02- buildPreparedStatementPreparationTemplate -> M55_01
M54_03- postProcess
- jdbcCoordinator.getResourceRegistry().register( preparedStatement, true )

// ConnectionImpl 执行数据库连接
C55- ConnectionImpl
M55_01- prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
?- 其中包括 sql 类型 , 返回类型 - >M55_02
M55_02- clientPrepareStatement
- ClientPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database) -> PS:M55_02_01

// PS:M55_02_01
com.mysql.cj.jdbc.ClientPreparedStatement: insert into user (isactive, orgid, remark, userlink, username, usertype, userid) values (** NOT SPECIFIED **, ** NOT SPECIFIED **, ** NOT SPECIFIED **, ** NOT SPECIFIED **, ** NOT SPECIFIED **, ** NOT SPECIFIED **, ** NOT SPECIFIED **)

C56- ResourceRegistryStandardImpl
M56_01- register(Statement statement, boolean cancelable)
- Set<ResultSet> previousValue = xref.putIfAbsent( statement, Collections.EMPTY_SET );

// 最终处理类
C57- ResultSetReturnImpl
M57_01- executeUpdate(PreparedStatement statement) : 执行 update 操作
- jdbcCoordinator.getJdbcSessionOwner().getJdbcSessionContext().getObserver().jdbcExecuteStatementStart()

executeUpdate 处理流程

  • ResultSetReturnImpl # executeUpdate
  • HikariProxyPreparedStatement # executeUpdate
  • ProxyPreparedStatement # executeUpdate
  • ClientPreparedStatement # executeUpdate
  • ClientPreparedStatement # executeLargeUpdate
  • ClientPreparedStatement # executeUpdateInternal
  • ClientPreparedStatement # fillSendPacket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码C58- SessionEventListenerManagerImpl
M58_01- jdbcExecuteStatementStart()
- 发起 SessionEventListener 处理 listener.jdbcExecuteStatementStart()
- 调用 executeUpdate()

C59- ClientPreparedStatement
M59_01- executeUpdate() --> 核心调用为 M59_02
M59_02- executeUpdateInternal(QueryBindings<?> bindings, boolean isReallyBatch)
- Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket(bindings); : 此时已经生成一个 Message 对象

// 最终的处理流程 , SQL 的封装
C60- AbstractPreparedQuery
M60_01- fillSendPacket(QueryBindings<?> bindings)
?- bindValues 核心逻辑 ->

M60_01 源代码 : bindvalue 的绑定处理

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
java复制代码public <M extends Message> M fillSendPacket(QueryBindings<?> bindings) {
synchronized (this) {
// 绑定参数
BindValue[] bindValues = bindings.getBindValues();
// 准备最终返回的对象
NativePacketPayload sendPacket = this.session.getSharedSendPacket();

sendPacket.writeInteger(IntegerDataType.INT1, NativeConstants.COM_QUERY);

boolean useStreamLengths = this.useStreamLengthsInPrepStmts.getValue();

int ensurePacketSize = 0;

String statementComment = this.session.getProtocol().getQueryComment();

byte[] commentAsBytes = null;

if (statementComment != null) {
commentAsBytes = StringUtils.getBytes(statementComment, this.charEncoding);

ensurePacketSize += commentAsBytes.length;
ensurePacketSize += 6; // for /*[space] [space]*/
}

// 统计PacketSize
for (int i = 0; i < bindValues.length; i++) {
if (bindValues[i].isStream() && useStreamLengths) {
ensurePacketSize += bindValues[i].getStreamLength();
}
}

// 检查底层缓冲区是否有足够的空间从当前位置开始存储additionalData字节。
// 如果缓冲区的大小小于所需的大小,那么将以更大的大小重新分配它
if (ensurePacketSize != 0) {
sendPacket.ensureCapacity(ensurePacketSize);
}

if (commentAsBytes != null) {
// 固定长度的字符串有一个已知的硬编码长度
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, Constants.SLASH_STAR_SPACE_AS_BYTES);
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, commentAsBytes);
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, Constants.SPACE_STAR_SLASH_SPACE_AS_BYTES);
}

// 源 SQL 语句 : insert into user (isactive, orgid, remark, userlink, username, usertype, userid) values (?, ?, ?, ?, ?, ?, ?)
byte[][] staticSqlStrings = this.parseInfo.getStaticSql();
// bindValues 是参数列表 -> PS:M60_01_01
for (int i = 0; i < bindValues.length; i++) {
bindings.checkParameterSet(i);

sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, staticSqlStrings[i]);

if (bindValues[i].isStream()) {
streamToBytes(sendPacket, bindValues[i].getStreamValue(), true, bindValues[i].getStreamLength(), useStreamLengths);
} else {
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, bindValues[i].getByteValue());
}
}
// 组合为 NativeSQL 字符数组
// get one byte to string -> insert into user (isactive, orgid, remark, userlink, username, usertype, userid) values (0, '1', 'step1 ', null, 'gang', null, 0)
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, staticSqlStrings[bindValues.length]);

return (M) sendPacket;
}
}


// 数组一 :
for (int j = 0; j < staticSqlStrings.length; j++) {
System.out.println("staticSqlStrings value [" + new String(staticSqlStrings[j]) + "]");
}

// 数组一结果>>>>>>>>>>>>>>>>>>:
staticSqlStrings value [insert into user (isactive, orgid, remark, userlink, username, usertype, userid) values (]
staticSqlStrings value [, ]
staticSqlStrings value [, ]
staticSqlStrings value [, ]
staticSqlStrings value [, ]
staticSqlStrings value [, ]
staticSqlStrings value [, ]
staticSqlStrings value [)]

// 数组二 :
for (int j = 0; j < bindValues.length; j++) {
System.out.println("name [" + i + "]-- value [" + new String(bindValues[j].getByteValue()) + "]");
}

// 数组二结果>>>>>>>>>>>>>>>>>>>
name [0]-- value [0]
name [1]-- value ['1']
name [2]-- value ['step1 ']
name [3]-- value [null]
name [4]-- value ['gang']
name [5]-- value [null]
name [6]-- value [0]

// 总结 : 至此 , JPA 的SQL 解析就完成了 , 可以看到 , 是通过2个数组互相拼装完成后 , 下面我们分析一下其中的几个关键节点

PS:M60_01_01 : bindValue 对应参数

JPA_Param001.jpg

2.2 补充节点 : OriginSQL 的生成

我把 staticSqlStrings 称为 OriginSQL , 把 sendPacket 的 SQL 称为 NativeSQL

OriginSQL 的前世今生

上文从 this.parseInfo.getStaticSql() 中获取的 SQL 实际上已经被切割好了 , 我们从源头看看是什么时候放进去的 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//  先看一下体系
C- ClientPreparedStatement
E- StatementImpl
F- protected Query query;
MC- ClientPreparedStatement(JdbcConnection conn, String sql, String catalog, ParseInfo cachedParseInfo)
- ((PreparedQuery<?>) this.query).checkNullOrEmptyQuery(sql);
- ((PreparedQuery<?>) this.query).setOriginalSql(sql);
- ((PreparedQuery<?>) this.query).setParseInfo(cachedParseInfo != null ? cachedParseInfo : new ParseInfo(sql, this.session, this.charEncoding));

// 可以看到 , 在 ClientPreparedStatement 的构造方法中 , ParseInfo 就生成了 , 所以核心还是在 ParseInfo 对象中


C63- ParseInfo
MC63_01- ParseInfo(String sql, Session session, String encoding, boolean buildRewriteInfo)
?- 在该构造方法中 , 对 sql 进行了解析

来吧 , 非要把这一段代码看一遍 ,按照 Step 过一遍就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
java复制代码 public ParseInfo(String sql, Session session, String encoding, boolean buildRewriteInfo) {

try {
// Step 1 : 保证 sql 不为空
if (sql == null) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("PreparedStatement.61"),
session.getExceptionInterceptor());
}

// Step 2 : 字符编码和时间
this.charEncoding = encoding;
this.lastUsed = System.currentTimeMillis();

// 直译 : 获取标识符引用字符串 , 以下是一个完整体系 , 为了得到 quotedIdentifierChar
String quotedIdentifierString = session.getIdentifierQuoteString();
char quotedIdentifierChar = 0;
if ((quotedIdentifierString != null) && !quotedIdentifierString.equals(" ") && (quotedIdentifierString.length() > 0)) {
quotedIdentifierChar = quotedIdentifierString.charAt(0);
}

// sql 长度 , 部分框架会校验长度来保证安全性 , 像个 sign 试的 , 猜测可能也是这个原因
this.statementLength = sql.length();

ArrayList<int[]> endpointList = new ArrayList<>();
boolean inQuotes = false;
char quoteChar = 0;
boolean inQuotedId = false;
int lastParmEnd = 0;
int i;

// 没有反斜杠转义集
boolean noBackslashEscapes = session.getServerSession().isNoBackslashEscapesSet();

// 发现了一个有趣的地方 , 这里会去查找真正的起点 , 这也意味着实际上我的 SQL 可以有注释 , 而且不会解析 ???
// PS : 想多了 ..... QuerySyntaxException 已经做了拦截
// StringUtils.startsWithIgnoreCaseAndWs(sql, "/*")
// statementStartPos = sql.indexOf("*/");
// StringUtils.startsWithIgnoreCaseAndWs(sql, "--") || StringUtils.startsWithIgnoreCaseAndWs(sql, "#")
this.statementStartPos = findStartOfStatement(sql);

// Step 3 : 字符级逐字处理
// - 扫描操作类型 (Insert / select 等)
// - 扫描 '' "" 字符 ,跳过
// - 扫描 ? 占位符 , 记录位置
// - 扫描 ; 及 \n \r 换行符 , 直接跳出循环
// -
for (i = this.statementStartPos; i < this.statementLength; ++i) {
char c = sql.charAt(i);

if ((this.firstStmtChar == 0) && Character.isLetter(c)) {
// 确定我们正在执行的语句类型 , 即是 insert / update / select 等
this.firstStmtChar = Character.toUpperCase(c);

// 如果不是INSERT语句,则不需要搜索“ON DUPLICATE KEY UPDATE”
if (this.firstStmtChar == 'I') {
this.locationOfOnDuplicateKeyUpdate = getOnDuplicateKeyLocation(sql,
session.getPropertySet().getBooleanProperty(PropertyKey.dontCheckOnDuplicateKeyUpdateInSQL).getValue(),
session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue(),
session.getServerSession().isNoBackslashEscapesSet());
// 相反 , 如果是 insert , 此处需要处理 DUPLICATE KEY
// 当insert已经存在的记录时,执行Update
this.isOnDuplicateKeyUpdate = this.locationOfOnDuplicateKeyUpdate != -1;
}
}

// 当 \\ 时 , 下一个字符为转义字符 , 即转义字符的处理 == 不处理 , 跳过
if (!noBackslashEscapes && c == '\\' && i < (this.statementLength - 1)) {
i++;
continue;
}

//
if (!inQuotes && (quotedIdentifierChar != 0) && (c == quotedIdentifierChar)) {
inQuotedId = !inQuotedId;
} else if (!inQuotedId) {
// 只有在不使用引号标识符时才使用引号
if (inQuotes) {
// 处理 ' 和 " 字符 , 这里也意味着2种字符均可
// 这里可能存在问题 , if 和 else if 条件一样
if (((c == '\'') || (c == '"')) && c == quoteChar) {
if (i < (this.statementLength - 1) && sql.charAt(i + 1) == quoteChar) {
i++;
continue; // inline quote escape
}

inQuotes = !inQuotes;
quoteChar = 0;
} else if (((c == '\'') || (c == '"')) && c == quoteChar) {
inQuotes = !inQuotes;
quoteChar = 0;
}
} else {
// 对 # 号进行处理
if (c == '#' || (c == '-' && (i + 1) < this.statementLength && sql.charAt(i + 1) == '-')) {
// 运行到语句结束,或换行符,以先出现者为准
int endOfStmt = this.statementLength - 1;

for (; i < endOfStmt; i++) {
c = sql.charAt(i);
// 这里时换行符或者结尾
if (c == '\r' || c == '\n') {
break;
}
}

continue;
} else if (c == '/' && (i + 1) < this.statementLength) {
// Comment?
char cNext = sql.charAt(i + 1);

if (cNext == '*') {
i += 2;

for (int j = i; j < this.statementLength; j++) {
i++;
cNext = sql.charAt(j);

if (cNext == '*' && (j + 1) < this.statementLength) {
if (sql.charAt(j + 1) == '/') {
i++;

if (i < this.statementLength) {
c = sql.charAt(i);
}

break; // comment done
}
}
}
}
} else if ((c == '\'') || (c == '"')) {
inQuotes = true;
quoteChar = c;
}
}
}

if (!inQuotes && !inQuotedId) {
// 此处处理 ? 占位符
if ((c == '?')) {
// 记录当前占位符字符
endpointList.add(new int[] { lastParmEnd, i });
// 字符 + 1 , 处理后续字符
lastParmEnd = i + 1;

// 是否在重复密钥更新
// 重复密钥更新的位置
if (this.isOnDuplicateKeyUpdate && i > this.locationOfOnDuplicateKeyUpdate) {
this.parametersInDuplicateKeyClause = true;
}
// 处理结尾符号
} else if (c == ';') {
int j = i + 1;
if (j < this.statementLength) {
for (; j < this.statementLength; j++) {
if (!Character.isWhitespace(sql.charAt(j))) {
break;
}
}
if (j < this.statementLength) {
this.numberOfQueries++;
}
i = j - 1;
}
}
}
}

// 自此 SQL 的扫描已经完成 , 后续进行 SQL 的切割

if (this.firstStmtChar == 'L') {
if (StringUtils.startsWithIgnoreCaseAndWs(sql, "LOAD DATA")) {
this.foundLoadData = true;
} else {
this.foundLoadData = false;
}
} else {
this.foundLoadData = false;
}

endpointList.add(new int[] { lastParmEnd, this.statementLength });
this.staticSql = new byte[endpointList.size()][];
this.hasPlaceholders = this.staticSql.length > 1;

// Step 5 : 此处按照扫描的节点进行字符串切割
for (i = 0; i < this.staticSql.length; i++) {
int[] ep = endpointList.get(i);
int end = ep[1];
int begin = ep[0];
int len = end - begin;

if (this.foundLoadData) {
this.staticSql[i] = StringUtils.getBytes(sql, begin, len);
} else if (encoding == null) {
byte[] buf = new byte[len];

for (int j = 0; j < len; j++) {
buf[j] = (byte) sql.charAt(begin + j);
}

this.staticSql[i] = buf;
} else {
// 切割字符串
this.staticSql[i] = StringUtils.getBytes(sql, begin, len, encoding);
}
}

// PS : 切割的结果即为上文数组一的结果
} catch (StringIndexOutOfBoundsException oobEx) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("PreparedStatement.62", new Object[] { sql }), oobEx,
session.getExceptionInterceptor());
}

if (buildRewriteInfo) {
this.canRewriteAsMultiValueInsert = this.numberOfQueries == 1 && !this.parametersInDuplicateKeyClause
&& canRewrite(sql, this.isOnDuplicateKeyUpdate, this.locationOfOnDuplicateKeyUpdate, this.statementStartPos);
if (this.canRewriteAsMultiValueInsert && session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()) {
buildRewriteBatchedParams(sql, session, encoding);
}
}

}

2.3 Entity 转换为 SQL

即实体类转换为

insert into user (isactive, orgid, remark, userlink, username, usertype, userid) values (?, ?, ?, ?, ?, ?, ?)

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
java复制代码
// 以及还有一个 , 最初 的 sql 是怎么通过 Entity 翻译过来的

C53- AbstractEntityPersister
M53_01- insert(Serializable id, Object[] fields, Object object, SharedSessionContractImplementor session)
P- fields : 实体类值数组
P- object : 当前实体类
M53_03- doLateInit : 初始化 Persister ->


public void insert(Serializable id, Object[] fields, Object object, SharedSessionContractImplementor session) {
// 生成应用内存中任何预插入值
preInsertInMemoryValueGeneration( fields, object, session );

// 获取 Table 数量
final int span = getTableSpan();
if ( entityMetamodel.isDynamicInsert() ) {
// F对于dynamic-insert="true"的情况,需要生成INSERT SQL
boolean[] notNull = getPropertiesToInsert( fields );
for ( int j = 0; j < span; j++ ) {
insert( id, fields, notNull, j, generateInsertString( notNull, j ), object, session );
}
}
else {
// 对于dynamic-insert="false"的情况,使用静态SQL
for ( int j = 0; j < span; j++ ) {
// getSQLInsertStrings()[j] 方法 , 这里直接从获取得属性 sqlInsertStrings -> PS:M53_01_01
insert( id, fields, getPropertyInsertability(), j, getSQLInsertStrings()[j], object, session );
}
}
}

PS:M53_01_01 AbstractEntityPersister 中的默认语句

JPA_AbstractEntityPersister_Module.jpg

从图片中就不难发现 , AbstractEntityPersister 中已经预先生成了相关的属性 , 直接获取

流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码// 这些属性值是在以下流程中生成得 : 
C53- AbstractEntityPersister
M53_03- doLateInit : 初始化 Persister

// 主要流程点 :
C- LocalContainerEntityManagerFactoryBean # afterPropertiesSet
C- AbstractEntityManagerFactoryBean # buildNativeEntityManagerFactory
C- AbstractEntityManagerFactoryBean # createNativeEntityManagerFactory
C- SpringHibernateJpaPersistenceProvider # createContainerEntityManagerFactory
C- SessionFactoryBuilderImpl # build
C- SessionFactoryImpl # init
C- MetamodelImpl # initialize

// 最终流程点 : 执行 AbstractEntityPersister 初始化
C- AbstractEntityPersister # postInstantiate

实际生成方法 源代码

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
java复制代码C53- AbstractEntityPersister
M53_04- generateInsertString

protected String generateInsertString(boolean identityInsert, boolean[] includeProperty, int j) {

// 此处 Insert 对象包含方言和表明
Insert insert = new Insert( getFactory().getDialect() )
.setTableName( getTableName( j ) );

// add normal properties
for ( int i = 0; i < entityMetamodel.getPropertySpan(); i++ ) {
// the incoming 'includeProperty' array only accounts for insertable defined at the root level, it
// does not account for partially generated composites etc. We also need to account for generation
// values
if ( isPropertyOfTable( i, j ) ) {
if ( !lobProperties.contains( i ) ) {
final InDatabaseValueGenerationStrategy generationStrategy = entityMetamodel.getInDatabaseValueGenerationStrategies()[i];
if ( generationStrategy != null && generationStrategy.getGenerationTiming().includesInsert() ) {
if ( generationStrategy.referenceColumnsInSql() ) {
final String[] values;
if ( generationStrategy.getReferencedColumnValues() == null ) {
values = propertyColumnWriters[i];
}
else {
final int numberOfColumns = propertyColumnWriters[i].length;
values = new String[numberOfColumns];
for ( int x = 0; x < numberOfColumns; x++ ) {
if ( generationStrategy.getReferencedColumnValues()[x] != null ) {
values[x] = generationStrategy.getReferencedColumnValues()[x];
}
else {
values[x] = propertyColumnWriters[i][x];
}
}
}
insert.addColumns( getPropertyColumnNames( i ), propertyColumnInsertable[i], values );
}
}
else if ( includeProperty[i] ) {
// 插入 Columns
insert.addColumns(
getPropertyColumnNames( i ),
propertyColumnInsertable[i],
propertyColumnWriters[i]
);
}
}
}
}

// 添加鉴频器 , 简单点说就是为了让一个数据库对象映射为多种不同得 JavaBean
// Discriminator是一种基于单个数据库表中包含的数据的继承策略。
@ https://www.waitingforcode.com/jpa/single-table-inheritance-with-discriminator-in-jpa/read#:~:text=Discriminator%20is%20an%20inheritance%20strategy%20based%20on%20data,this%20type%20of%20inheritance%2C%20JPA%20provides%20two%20annotations%3A


if ( j == 0 ) {
addDiscriminatorToInsert( insert );
}

// 如果需要插入主键 , 则添加
if ( j == 0 && identityInsert ) {
insert.addIdentityColumn( getKeyColumns( 0 )[0] );
}
else {
insert.addColumns( getKeyColumns( j ) );
}

// 插入前缀
if ( getFactory().getSessionFactoryOptions().isCommentsEnabled() ) {
insert.setComment( "insert " + getEntityName() );
}

for ( int i : lobProperties ) {
if ( includeProperty[i] && isPropertyOfTable( i, j ) ) {
// 此属性属于表,将被插入
insert.addColumns(
getPropertyColumnNames( i ),
propertyColumnInsertable[i],
propertyColumnWriters[i]
);
}
}

String result = insert.toStatementString();

// append the SQL to return the generated identifier
if ( j == 0 && identityInsert && useInsertSelectIdentity() ) { //TODO: suck into Insert
result = getFactory().getDialect().getIdentityColumnSupport().appendIdentitySelectToInsert( result );
}

return result;
}

总结

整体有三个节点 :

解析的实际节点 : ClientPreparedStatement # fillSendPacket

SQL 的处理 : ParseInfo

Entity 翻译为 SQL : AbstractEntityPersister # generateInsertString

本文转载自: 掘金

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

C语言实现单链表

发表于 2021-05-13

单链表常规操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码/********************* 单链表的常规操作 ****************************/

LinkList CreateHeadListH(); // 头插法创建单链表
LinkList CreateHeadListT(); // 尾插法创建单链表
int ListEmpty(); // 单链表判空
int ListLength(); // 求单链表长度
void Travel(); // 遍历单链表
int InsertNode(); // 插入结点
int DeleteNode(); // 删除结点
ElemType GetElem(); // 按址查值
int GetLocate(); // 按值查址
int RemoveRepeat(); // 去除重复的值

/*****************************************************************/

定义单链表结构体

单链表是由多个结点链接组成,它的每个结点包含两个域,一个数据域和一个链接域(地址域)。

单链表

  • 数据域 data 用来存放具体的数据。
  • 地址域 next 用来存放下一个节点的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码#include "stdio.h"
#include "malloc.h"


#define TRUE 1
#define FALSE 0

typedef int ElemType; // 单链表存储元素的数据类型

// 定义单链表结构体
typedef struct Node(){
ElemType data; // 单链表结点数据域
struct Node *next; // 单链表结点地址域(指向下一个结点)
}*LinkList, Node;

构造单链表

头插法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码/*
* 头插法创建单链表(带头结点)
* datas 接收数组,用于赋值链表的结点数据
* len datas数组的长度,便于遍历
*/
LinkList CreateHeadListH(ElemType *datas, int len){
// 创建头结点
LinkList head, new_node;
head = (LinkList)malloc(sizeof(Node));
// head -> data = len; // 可以把链表长度存在头结点的数据域中
head -> next = NULL;

// 分配新节点并用头插法链接起来
for(int i=0;i<len;i++){
new_node = (LinkList)malloc(sizeof(Node));
new_node -> data = datas[i];
new_node -> next = head -> next;
head -> next = new_node;
}
return head;
}

头插法构造单链表时一直往单链表的头部插入结点。

尾插法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码/*
* 尾插法创建单链表(带头结点)
* datas 接收数组,用于赋值链表的结点数据
* len datas数组的长度,便于遍历
*/
LinkList CreateHeadListT(ElemType *datas, int len){
// 创建头结点
LinkList head, p, new_node;
head = (LinkList)malloc(sizeof(Node));
head -> next = NULL;
p = head;

// 分配新节点并用尾插法链接起来
for(int i=0;i<len;i++){
new_node = (LinkList)malloc(sizeof(Node));
new_node -> data = datas[i];
new_node -> next = NULL;
p -> next = new_node;
p = new_node;
}
return head;
}

尾插法构造单链表时一直往单链表的尾部插入结点。

单链表的头尾插法详解

为了不让文章篇幅过长,关于单链表头尾插法的更多具体内容请观看我的另一篇博客 单链表的头尾插法详解

单链表判空

1
2
3
4
5
6
7
c复制代码/*
* 单链表判空
* list 接收单链表
*/
int ListEmpty(LinkList list){
return (list == NULL || list -> next == NULL);
}

计算单链表长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码/*
* 计算单链表的长度
* list 接收单链表
*/
int ListLength(LinkList list){
LinkList p = list;
int len = 0;
if(ListEmpty(list)){
return len;
}
p = p -> next;
while(p != NULL){
len ++;
p = p -> next;
}
return len;
}

遍历单链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码/*
* 遍历单链表
* list 单链表
*/
void Travel(LinkList list){
LinkList p = list;
if(!ListEmpty(list)){ // 单链表非空情况下才遍历
p = p -> next; // 因为带头结点单链表所以要先走一步
while(p != NULL){
printf("%d\t", p -> data);
p = p -> next;
}
printf("\n");
}
}

单链表头、尾插法构造效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码int main(int argc, char const *argv[])
{
int datas[] = {2, 4, 6, 8, 10};
// 动态计算datas数组的长度
// 数组长度 = 数组的总空间大小 / 数组中每个元素所占空间大小
int len = sizeof(datas) / sizeof(datas[0]);

printf("头插法构造单链表\n");
LinkList list_h = CreateHeadListH(datas, len);
printf("ListEmpty():%d\n", ListEmpty(list_h)); // 判空
printf("ListLength():%d\n", ListLength(list_h)); // 求长
printf("Travel():");
Travel(list_h); // 遍历

printf("\n尾插法构造单链表\n");
LinkList list_t = CreateHeadListT(datas, len);
printf("ListEmpty():%d\n", ListEmpty(list_t));
printf("ListLength():%d\n", ListLength(list_t));
printf("Travel():");
Travel(list_t);
return 0;
}

因为数组是在连续的地址上存储元素,所以可以动态的计算数组的长度,方便遍历。

输入结果如下:

1
2
3
4
5
6
7
8
9
10
11
C复制代码头插法构造单链表
ListEmpty():0
ListLength():5
Travel():10 8 6 4 2

尾插法构造单链表
ListEmpty():0
ListLength():5
Travel():2 4 6 8 10

请按任意键继续. . .

单链表指定位置插入结点

代码实现

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
c复制代码/*
* 单链表指定位置插入结点
* list 单链表
* data 要插入的结点的数据
* pos 结点插入的位置(逻辑位置(1,2,3,...))
*/
int InsertNode(LinkList list, ElemType data, int pos){
LinkList p = list, new_node;
if(ListEmpty(list)){
printf("空单链表\n");
return FALSE;
}

// 判断插入位置是否合理
if(pos <= 0 || pos > ListLength(list) + 1){
printf("插入位置不合理\n");
return FALSE;
}

// 寻找到要插入位置的前一个结点
for(int i = 0; i < pos - 1 && p != NULL; i++){
p = p -> next;
}

// 准备新结点
new_node = (LinkList)malloc(sizeof(Node));
new_node -> data = data;

// 此时p就是要插入位置的前一个结点,p -> next就是要插入位置的结点
new_node -> next = p -> next;
p -> next = new_node;
return TRUE;
}

详细图解

假设原单链表为:head --> 2 --> 6 ,要插入的结点值为 4,插入位置为 2。

图解单链表插入_01

只需找要插入位置的前一个结点就行,因为插入位置的前一个结点的地址域保存着要插入位置的结点。

此时找到的结点是 new_code1,而 new_code1 -> next 就是结点 new_code2 。

所以我们只要

1
2
c复制代码new_code3 -> next = new_code1 -> next;
new_code1 -> next = new_code3;

先让待插入结点的地址域指向插入位置的结点

后让插入位置的前一个结点的地址域指向待插入结点。

单链表插入图解_02

1, 2, 3代表单链表结点位置

①,②,③代表插入操作的执行步骤顺序

注意:千万不能先让插入位置的前一个结点的地址域指向待插入结点,后让待插入结点的地址域指向插入位置的结点

1
2
3
c复制代码new_code1 -> next = new_code3;
// 此时new_code1 -> next 等于 new_code3, now_code3 -> next = new_code3 没有达到链接
new_code3 -> next = new_code1 -> next;

单链表指定位置删除结点

代码实现

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
c复制代码/*
* 单链表指定位置删除结点
* list 单链表
* *val 用来存储删除结点的数据
* pos 结点删除的位置(逻辑位置(1,2,3,...))
*/
int DeleteNode(LinkList list, ElemType *val, int pos){
LinkList p = list, r;
if(ListEmpty(list)){
printf("空单链表\n");
return FALSE;
}

// 判断删除位置是否合理
if(pos <= 0 || pos > ListLength(list)){
printf("删除位置不合理\n");
return FALSE;
}

// 寻找到要删除结点的前一个位置
for(int i = 0; i < pos - 1 && p != NULL; i++){
p = p -> next;
}

r = p -> next; // 记录要删除的结点
*val = r -> data; // 把删除结点的数据利用指针返回去
p -> next = r -> next; // 把链表重新链接起来
free(r); // 释放删除结点的资源
return TRUE;
}

详细图解

假设原单链表为:head --> 2 --> 4 --> 6 ,删除第 2 个结点。

单链表删除节点图解_01

还是跟插入一样只需找要删除位置的前一个结点就行。

此时找到的结点是 new_code1,而 new_code1 -> next 就是结点 new_code2 ,就是要删除的结点。

1
2
3
c复制代码r = new_code1 - > next;                
new_code1 -> next = r -> next; // new_code1 -> next = new_code2 -> next;
free(r);

先让变量 r 等于要删除的结点 ,

r = new_code1 - > next; --> r = new_code2;

后让删除位置的前一个结点的地址域指向要删除结点的后一个结点。

1
2
3
4
5
c复制代码new_code1 -> next = r -> next;            // 此时r -> next 等于 new_code2
↓↓
new_code1 -> next = new_code2 -> next; // new_code2 -> next 等于 new_code3
↓↓
new_code1 -> next = new_code3;

最后释放删除结点空间 free(r)

单链表删除节点图解_02

删除第二个位置节点后的单链表:head --> 2 --> 6

按址求值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码/*
* 根据指定位置求结点的值(没有找到返回 0 )
* list 单链表
* pos 结点位置(逻辑位置(1,2,3,...))
*/
ElemType GetElem(LinkList list, int pos){
LinkList p = list;
if(ListEmpty(list)){
printf("空单链表\n");
return FALSE;
}
if(pos <= 0 || pos > ListLength(list)){
printf("位置不合理\n");
return FALSE;
}

for(int i = 0; i < pos && p !=NULL; i++){
p = p -> next;
}
return p -> data;
}

按值求址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码/*
* 根据指定的值寻找结点的位置
* (如果有多个值相同返回第一个找到的结点的位置, 没找到则返回 0)
* list 单链表
* data 要查找的值
*/
int GetLocate(LinkList list, ElemType data){
LinkList p = list;
int pos = 0;
if(ListEmpty(list)){
return FALSE;
}
p = p -> next;
while(p != NULL){
pos ++;
if(p -> data == data){
return pos;
}
p = p -> next;
}
return FALSE;
}

单链表去重

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
c复制代码/*
* 去除单链表中重复的值(重复的值只保留一个)
* list 单链表
* 返回值:对单链表进行了去重操作返回 1,否则返回 0
*/
int RemoveRepeat(LinkList list){
LinkList p = list, q, r;
int flag = 0;
if(ListEmpty(list)){
return FALSE;
}

p = p -> next;

while(p != NULL){
q = p;
while(q != NULL && q -> next != NULL){
if(p -> data == q -> next -> data){
r = q -> next; // 记录值相同的结点
q -> next = r -> next;
free(r);
flag = 1;
}else{
q = q -> next;
}
}
p = p -> next;
}
return flag;
}

原理就是每次拿没有比较过的结点跟单例表中的每一个结点进行比较,遇到相同的就删除其中一个结点。

例如:单链表序列为 2 4 2 8 8 6 6 8 12

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
c复制代码首先拿第一个结点 2 跟单链表的其他结点比较
2 4 2 8 8 6 6 8 12
↑
↓↓
遇到相同的就删除
2 4 8 8 6 6 8 12


2比完了一轮去重了2,然后用第二个结点 4 跟单链表的其他没有比较过的结点比较
2 4 8 8 6 6 8 12
↑
↓↓
2 4 8 8 6 6 8 12


4比完了一轮去重了4,然后用第三个结点 8 跟单链表的其他没有比较过的结点比较
2 4 8 8 6 6 8 12
↑
↓↓
2 4 8 6 6 12

循环类推
2 4 8 6 6 12
↑
↓↓
2 4 8 6 12

最后一步
2 4 8 6 12
↑
↓↓
2 4 8 6 12

看看去重效果

1
2
3
4
5
6
7
c复制代码去重前的单链表
ListLength():9
Travel():2 4 2 8 8 6 6 8 12

去重后的单链表
ListLength():5
Travel():2 4 8 6 12

源代码

源代码已上传到 GitHub Data-Structure-of-C,欢迎大家来访。

✍ 码字不易,万水千山总是情,点赞再走行不行,还望各位大侠多多支持❤️

本文转载自: 掘金

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

单链表头尾插法详解

发表于 2021-05-13

单链表结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码#include "stdio.h"
#include "malloc.h"


#define TRUE 1
#define FALSE 0

typedef int ElemType; // 单链表存储元素的数据类型

// 定义单链表结构体
typedef struct Node(){
ElemType data; // 单链表结点数据域
struct Node *next; // 单链表结点地址域(指向下一个结点)
}*LinkList, Node;

头插法构造单链表

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码/*
* 头插法创建单链表(带头结点)
* datas 接收数组,用于赋值链表的结点数据
* len datas数组的长度,便于遍历
*/
LinkList CreateHeadListH(ElemType *datas, int len){
// 创建头结点
LinkList head, new_node;
head = (LinkList)malloc(sizeof(Node));
// head -> data = len; // 可以把链表长度存在头结点的数据域中
head -> next = NULL;

// 分配新节点并用头插法链接起来
for(int i=0;i<len;i++){
new_node = (LinkList)malloc(sizeof(Node));
new_node -> data = datas[i];
new_node -> next = head -> next;
head -> next = new_node;
}
return head;
}

头插法过程

头插法往单链表头部插入,假设

1
c复制代码datas[] = {2, 4, 6};

首先创建头结点

单链表初始头结点

分配第一个新节点时

head 结点的数据域为空 head -> data = NULL, ,地址域为空 head -> next = NULL;

1
2
c复制代码new_code -> data = datas[0];         -->        new_code -> data = 2;
new_code -> next = head -> next; --> new_code -> next = NULL;

单链表插入第一个结点
然后让 head 结点的地址域指向新结点(这里指第一个结点)

1
c复制代码head -> next = new_code1;

最终形成

单链表头插法插入第一个结点后

分配第二个新结点时

head 结点的数据域为空 head -> data = NULL, ,地址域也为第一个结点的地址 head -> next = new_node1;

1
c复制代码new_code -> data = datas[1];        -->        new_code -> data = 4;

让第二结点的地址域指向第一个结点(此时第一个结点位置存在 head 结点的地址域)

1
c复制代码new_code -> next = head -> next;    -->        new_code -> next = new_code1;

单链表头插法插入第二个结点

然后让 head 头结点的地址域指向第二个结点的位置

1
c复制代码head -> next = new_code2;

最终形成

单链表头插法出入第二结点后

分配第三个新结点后

单链表头插法插入第三个结点后

关键代码

头插法每次插入新结点时都是往头结点处插入。

1
2
c复制代码new_node -> next = head -> next;    // 先让新结点地址域指向头结点地址域的结点位置
head -> next = new_node; // 然后让头结点的地址域指向新结点位置

如此循环就形成了头插法构造单链表。

尾插法构造单链表

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码/*
* 尾插法创建单链表(带头结点)
* datas 接收数组,用于赋值链表的结点数据
* len datas数组的长度,便于遍历
*/
LinkList CreateHeadListT(ElemType *datas, int len){
// 创建头结点
LinkList head, p, new_node;
head = (LinkList)malloc(sizeof(Node));
head -> next = NULL;
p = head;

// 分配新节点并用尾插法链接起来
for(int i=0;i<len;i++){
new_node = (LinkList)malloc(sizeof(Node));
new_node -> data = datas[i];
new_node -> next = NULL;
p -> next = new_node;
p = new_node;
}
return head;
}

尾插法过程

尾插法往单链表尾部插入,还是假设单链表的结点数据分别为。

1
c复制代码datas[] = {2, 4, 6};

创建头结点跟头插法是一样的我就不重复叙述了。

分配第一个新结点时

head 结点的数据域为空 head -> data = NULL, ,地址域为空 head -> next = NULL; 变量p等于头结点 p = head;。

先让新结点赋值

1
2
c复制代码new_code -> data = datas[0];        -->        new_code -> data = 2;
new_code -> next = NULL; --> new_code -> next = NULL

后让链表链接起来

1
2
c复制代码p -> next = new_code;
p = new_code;

单链表尾插法插入第一个结点后

一开始 p = head 所以让头结点的地址域指向新结点,,后让 p 等于新结点(此时新结点代表第一个结点)。

分配第二个新结点时

head 结点的数据域为空 head -> data = NULL, ,地址域为空 head -> next = new_node1;

此时变量 p 就等于第一个结点,不再等于头结点。

继续让新结点与单链表链接起来

1
2
c复制代码p -> next = new_code;
p = new_code;

让第一个结点的地址域指向新结点(此时新结点代表第二个结点),让p等于第二个结点。

单链表尾插法插入第二个结点后

分配第三个新结点后

单链表尾插法

关键代码

1
2
c复制代码p -> next = new_code;    // 让p的地址域指向新结点
p = new_code; // 然后让p等于新插入进来的结点(一直等于最后一个结点)

尾插法每次插入新结点时都是往尾结点处插入。

如此循环就形成了尾插法构造单链表。

单链表头尾插法对比

单链表头尾插法对比

同样是数据 datas[] = {2, 4, 6, 8}; 但链接的效果是不一致的,思想也不同。

头插法: head --> 8 --> 6 --> 4 --> 2

结点一直往 单链表头部插入,先进入的数据结点链接在末尾端(刚好逆序),有点像栈的特性,先进后出。

尾插法: head --> 2 --> 4 --> 6 --> 8

结点一直往 单链表尾部插入,先进入的数据结点还是在前驱。

源代码

源代码已上传到 GitHub Data-Structure-of-C,欢迎大家来访。

✍ 码字不易,万水千山总是情,点赞再走行不行,还望各位大侠多多支持❤️

本文转载自: 掘金

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

入职第一天,老板竟让我优化5亿数据量,要凉凉?

发表于 2021-05-13

前段时间hellohello-tom离职了,因为个人原因,在休整一段时间后,重新入职了一家新公司。入职的第一天tom哥就经历了一次生产事故,运维同学告警说线上MYSQL负载压力大,直接就把主库MYSQL压崩了(第一天这可不是好兆头),运维同学紧急进行了主从切换,在事后寻找导致生产事故的原因时,排查到是慢查询导致mysql雪崩的主要原因,在导出慢查询的sql后,项目经理直接说吧这个mysql优化的功能交给新来的tom哥吧,tom哥赶紧打开跳板机进行查看,不看不知道一看吓一跳

入职第一天,老板竟让我优化5亿数据量,要凉凉?

单表的数据量已经达到了5亿级别!,这尼玛肯定是历史问题一直堆积到现在才导致的啊,项目经理直接就把这个坑甩给了tom哥,tom哥心中想,我难道试岗期都过不了么??

好在tom哥身经百战,赶快与项目经理与老同事进行沟通,了解业务场景,才发现导致现在的情况是这样的,tom哥所在的公司是主要做IM社交系统的,这个5亿级别的数据表是关注表,也是俗称的粉丝表,在类似与某些大V、或者是网红,粉丝过百万是非常常见的。在A关注B后会产生一条记录,B关注A时也会产生一条记录,时间积累久了才达到今天这样的数据规模,项目经理慢悠悠的对tom哥说,这个优化不用着急,先出方案吧!

按照tom哥之前经验,单表在达到500W左右的数据就应该考虑分表了,常见分表方案无非就是hash取模,或者range分区这两种方法,但是这次的数据分表与迁移过程难度在于两方面:

1、数据平滑过度,在不停机的情况把单表数据逐步迁移(老板说:敢宕机分分钟损失几千块,KPI直接给你扣成负的)

2、数据分区,采用hash还是range?(暂时不能使用一些分库分表中间件,无奈。)

首先说说hash

入职第一天,老板竟让我优化5亿数据量,要凉凉?

常规我们都是拿用户id进行取模,模到多少直接把数据塞进去就行了,简单粗暴,但是假如说user_id=128与user_id=257再模128后都是对应user_attention_1这个表,他俩也恰好是网红,旗下粉丝过百万,那轻轻松松两个人就能把数据表撑满,其他用户再进来数据的时候无疑user_attention_1这个表还会成为一张大表,这就是典型的数据热点问题,这个方案可以PASS,有的同学说可以user_id和fans_id组合进行取模进行分配,tom哥也考虑过这个问题,虽然这样子数据分配均匀了,但是会有一个致命的问题就是查询问题(因为目前没有做类似mongodb与db2这种高性能查询DB,也没做数据同步,考虑到工作量还是查询现有的分表内的数据),例如业务场景经常用到的查询就是我关注了那些人,那些人关注了我,所以我们的查询代码可能会是这样写的

1
2
3
4
5
csharp复制代码//我关注了谁
select * from user_attention where user_id = #{userId}

//谁关注了我
select * from user_attention where fans_id = #{userId}

在我们进行user_id与fans_id组合后hash后,如果我想查询我关注的人与谁关注我的时候,那我将检索128张表才能得到结果,这个也太恶心了,肯定不可取,并且考虑到以后扩容至少也要影响一半数据,实在不好用,这个方案PASS。

个人整理了一些资料,有需要的朋友可以直接点击领取。

25大Java面试专题(附解析)

从0到1Java学习路线和资料

Java核心知识集

左程云算法

接下来说说range

入职第一天,老板竟让我优化5亿数据量,要凉凉?

Range看起来也很简单,用户id在一定的范围时候就把他路由到一个表中,例如用户id=128,那就在[0,10000]这个区间中对应的是user_attention_0这个表,就直接把数据塞进去就可以了,但是这样同样也会产生热点数据问题,看来简单的水平分区已经不能满足,这个方案也可以pass了,还是要另寻他经啊。

经过tom哥日夜奋战,深思熟虑之后,给出了三个解决方案

先说说第一种方案range+一致性hash环组合(hash环节点10000)

入职第一天,老板竟让我优化5亿数据量,要凉凉?

想采用这个方案主要是因为

1、扩容简单,影响范围小,只涉及hash环上单个节点影响

2、数据迁移简单,每次扩容只需把新增的节点与后置节点进行数据交互

3、查询范围小,按照range与hash关系检索部分表分区

大概思路我们还是先按照user_id进行大概范围划分,但是range之后我后面对应的可能就不是一个表了,而是一个hash环

入职第一天,老板竟让我优化5亿数据量,要凉凉?

在每个range区域后都对应着自己一套的环,我们可以根据实际情况进行扩容,比如在[1,10000]这个范围内只有2个大V,那我们分三个表就够了,预留1500万的数据容量。[10001,20000]中有4个网红和大V,hash环上就给出实际4张表,我们的用户id可以顺时针顺序坐落到第一个物理表,数据进行入库。

凡事有利有弊,方案也要结合工时,实际可行性与技术评审之后才能决定,弊端咱也要列出来

1、设计复杂,需要增加range区域与hash环关系

2、系统内修改波及较多,查询关系复杂,多了一层路由表的概念,虽然尽量把用户数据分配到一个区之内,但是想查询谁关注我,与我关注谁这样的逻辑时还是复杂。

说说第二种方案range+hash取模(hash模300)

入职第一天,老板竟让我优化5亿数据量,要凉凉?

这个其实就比较好理解了,就是一个简单的range+hash取模组合的形式,先range到一定的范围后,在这个范围内进行hash取模找到对应的表进行存储,这个方案比方案一简单点,但是方案一存在的问题他也存在,并且他还有扩容数据影响范围广的问题。但是实现起来就简单不少,从查询方面看根据不同场景可以控制取模的大小范围,根据实际情况每个分区的hash模采用不同的值。

最后一种方案range userId分区

入职第一天,老板竟让我优化5亿数据量,要凉凉?

这个方案是tom哥觉得靠谱性与实施性可能最高的一种,看起来挺像第二种方案的,但是更具体了一点,首先会定义一个中间关系表user_attention_routing

入职第一天,老板竟让我优化5亿数据量,要凉凉?

我们会把用户范围与路由到哪个表做成关系,根据范围区间进行查找,结合现有数据当某个大V,或者网红数据量比较大,我们就给他路由自成一表数据大概是这样的

入职第一天,老板竟让我优化5亿数据量,要凉凉?

例如user_id=256是个大V,就把他单独提出来让他自成一表,在查询范围的时候优先查是否有自己单独对应的路由表,而其他那些零碎用户还是路由到一个统一表内,这时候有的同学会说这样子数据不都又不均匀了么,tom哥也曾这样认为,但是分到绝对的均匀基本不太可能,只能做到相对,尽量把某些大V分出去,不占用公共资源,当某个人突然成为大V后,在吧这个人再单独分出去,不断演变这个过程,保证数据的平衡,并且这样子处理之后很多原来的关联查询其实改动不大了,只要在数据迁移后对原来的所有包含user_attention 进行动态的改造即可(使用个mybatis的拦截器就能搞定)PS:其实分析实际业务场景大部分的关注数据还是来源于那些零碎用户的。

分表方案首先就这样定了,接下来另一个问题就是查询问题,上文说过很多业务查询无非就是谁关注了我,我关注了谁这样的场景,如果继续使用之前的

1
2
3
4
5
csharp复制代码//我关注了谁
select * from user_attention where user_id = #{userId}

//谁关注了我
select * from user_attention where fans_id = #{userId}

这样的方案,当我要查询我的粉丝有哪些时,这样就悲剧了,我还是要检索全表根据fansid找到我所有的粉丝,因为表内只记录了我关注了谁这样的数据,考虑到这样的问题,tom哥决定重新设计数据存储形式,使用空间换时间的思路,原来处理的方式是用户在关注对方的时候产生一条记录,现在处理方式是用户A在关注用户B时写入两条数据,通过字段区分关系,假如user_attention表是这样的

入职第一天,老板竟让我优化5亿数据量,要凉凉?

在用户1关注2后产生两条数据,state(1代表我关注了,0代表我被关注了,2代表咱俩互关),采用这样的数据存储方式后,我所有的查询都可以从user_id进行出发了,不再逆向去推fans_id这样的方式,数据库索引设计上,考虑好user_id、fans_id、state与user_id、state这样的结构即可,是不是感觉很简单,虽然数据量存储变多了,但是查询方便了好多。

分表和查询问题解决了,最后就是要考虑数据迁移的过程了,这一步也非常重要。搞不好就要被扣掉自己的KPI了(步步为营啊)

数据迁移最需要考虑的问题就是个时效性,迁移程序必不可少,如何生产环境正常跑着,迁移脚本线下跑着数据互不影响呢?答案就是经典套路数据双写,因为老的数据不是一下子就迁移到新表内的,现在和user_attention产生的数据还是要保持的,在产生老表数据的同时,根据路由规则,直接存到新表内一份,线下的迁移程序多开几台服务慢慢跑呗,不过可要控制好数据量,别占满io影响生产环境,线下的模拟和演练也是必不可少的,谁都不能保证会不会出啥问题呢。迁移脚本和线上做好user_id和fans_id的唯一索引就行,在某些极端情况下,数据会存在新表内写入数据,但是老表内数据还没更新的可能这个做好版本号控制和日志记录就可以了,这些都比较简单。

当新表数据和老表完全同步时我们就可以把所有系统内波及老表查询的语句都改成新表查询,验证下有没有问题,如果没有问题最后就可以痛快的

1
sql复制代码truncate table user_attention;

终于干掉这个5亿数据量的定时炸弹了。

本文转载自: 掘金

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

Mysql大数据分页查询解决方案

发表于 2021-05-13

1.简介

之前,面阿里的时候,有个面试官问我有没有使用过分页查询,我说有,他说分页查询是有问题的,怎么解决;后来这个问题我没有回答出来;本着学习的态度,今天来解决一下这个问题;

2.分页插件使用

1.pom文件

1
2
3
4
5
xml复制代码        <dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>

2.创建分页配置器

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class PageHelperConfig {
@Bean
public PageHelper pageHelper() {
PageHelper pageHelper = new PageHelper();
Properties p = new Properties();
p.setProperty("offsetAsPageNum", "true");
p.setProperty("rowBoundsWithCount", "true");
p.setProperty("reasonable", "true");
pageHelper.setProperties(p);
return pageHelper;
}
}
  1. 测试代码:
1
2
3
4
5
6
7
8
9
10
java复制代码    @Test
void test() {
PageHelper.startPage(400000,10,"id desc");
List<UploadData> users = userMapper.queryAll();
System.out.println(users.size());
System.out.println(users);
for (UploadData uploadData: users) {
System.out.println(uploadData);
}
}

4.重写sql分析

image.png
debug 后可以查看它是通过重写sql来实现分页功能;
重写后的sql语句为”SELECT * FROM amj_devinfo order by id desc limit ?, ?”;

limit a, b;// 跳过前a条数据,取b条数据;

所以,其实现在问题就是回到了,执行这条sql语句所需要花费多少的问题了;

3.sql测试与分析

1
2
3
4
5
6
sql复制代码select * from amj_devinfo order by id limit 2000, 20;     // 0.027s
select * from amj_devinfo order by id limit 20000, 20; // 0.035s
select * from amj_devinfo order by id limit 200000, 20; // 0.136s
select * from amj_devinfo order by id limit 2000000, 20; // 1.484s

select * from amj_devinfo order by devaddress limit 2000000, 20; // 7.356 全表扫描 + filesort;

结论:如果说,是小的数据量的话,使用该分页完全没问题;当数据量到达两百万的时候,执行时间就得为6.729s了,对于用户来说,这是不可接受的;

3.1 limit现象分析

使用explain对sql先来分析一波;感兴趣的同学可以看看我的另一篇文章 MySQL结合explain分析

结果如下:

针对,select * from amj_devinfo order by id limit 2000, 20来说:
image.png

可以看到,使用的是基于索引树 + 回表的方法来获取数据的,顺序IO查询列数为:2000020;
首先,根据阿里Java开发手册,type为index 就已经不可接受了;最低标准为range;而且,它是order by id 能够使用上主键索引,要是order by ‘其他列(无索引)如devaddress’ 这个时候,就是全表扫描 + filesort,效率更慢;

备注:

1
sql复制代码select * from amj_devinfo order by id limit 2000000, 20;

这条语句是 方案一 :先通过id找到2000000,然后,剩下的20条再全表扫描;还是,方案二: 通过id回表直接找到2000020条,然后,放弃前2000000条;理论上剩下20条进行全表扫描肯定是快很多的;但是,有点尴尬。Mysql选择的其实是方案二;

3.2 解决之道

很显然,现在已经是发现了问题所在,我们需要对其进行解决;我们对下面的sql语句来进行升级;

测试背景:

1.mysql 数据表中有5695594 (五百万)条数据,在devcho中数据相对离散。

2.表的设计如下:

image.png

有需要测试的同学,可以按照我表设计来模拟测试;

1
sql复制代码select * from amj_devinfo where devcho = "77" limit 20000, 10;

3.2.1 对devcho建立索引

很显然,通过sql来查询的话,对devcho建立索引的话,可以把全表扫描升级为基于索引列的扫描;能提升一个量级;

索引建立结果如下:

image.png
执行sql语句:

image.png

执行时间8.415s 这个时间是不可以接收的;

3.2.2 sql执行时间长分析

经过多次测试,发现时间都是很久,那么,就不会是Mysql 刷脏页,而且,数据库空闲,没有别的sql与其竞争磁盘IO 而且,通过MVCC查找数据也不存在锁相关问题;所以,问题肯定是出现在sql语句上;

那么,为什么会出现这个问题呢? – 答案是回表

这条sql语句是怎么执行的呢?

  1. 先基于devcho的索引列,找到devcho=’77’的这一行;
  2. 在通过devcho中存的主键id,然后,回表找所有的数据;找20010条数据;
    这时候,问题就出现了,这个回表的过程是随机IO;这个随机IO效率是很低的;所以,undo log要把随机IO变成顺序IO。这里,就是最大的瓶颈所在;

扫描条数验证:
Handler_read_next: 该选项表明在进行索引扫描时,按照索引从文件数据里取数据的次数;

image.png
回表是sql瓶颈验证:

image.png

查找主键id,不需要回表,发现0.01s就可以搞定;证明了sql导致的回表就是瓶颈所在;

3.2.3 解决之道

我们刚刚发现,因为limit比较笨。select * from amj_devinfo where devcho = "77" limit 20000, 10;需要回表20010次;但是,我们只需要它回表10次啊。所以,我们可以先把符合条件的id找出来;再根据id使用inner join 去进行回表;

sql语句如下:

1
sql复制代码select * from amj_devinfo a INNER JOIN (select id from amj_devinfo where devcho = "77" limit 20000, 10) b on a.id = b.id;
1
js复制代码查询时间:0.025s

这个时候,就可以达到我们的要求了;这个联结是会产生笛卡尔积的。检索出来行的数目是第一个表中的行数乘以第二个表中的行数,以前,感觉挺慢的,这也证明,如果没有文件排序或者临时表的话,效率其实还可以;

4 测试时走过的坑

在测试的时候,其实我犯了两个错,卡了自己好几个小时,证明测试都不对;特此记录一下,给想复现现象的同学提个醒;

  1. 插入百万条数据数据内容相同;
  2. 在执行sql时,格式没有对应上,导致索引失效select * from amj_devinfo where devcho = 77 limit 20000, 10; 77是字符,我输入为整型;

4.1 百万数据内容都一样

1
2
sql复制代码select * from amj_devinfo where devcho = "2212" limit 20000, 10; // 0.042s
select id from amj_devinfo where devcho = "2212" limit 20000, 10; // 0.026s

还是上面的语句,只是数据内容是一样的;为什么两者时间是一个级别?

为什么会产生这种现象呢?

  1. 因为数据都一样的devcho 索引其实是没有用的;成为链表了;
  2. 第一条语句,找出20010条语句就找到内容了,因为,都存在一起 都在一个或者几个页表中,随机IO升级为顺序IO,是有回表,但是,顺序IO的回表也是很快的。 所以,效率很高;即,第一条语句和第二条语句花的时间是差不多的;

4.2 写sql时,把”77”写成了77;

现象再现:

1
2
sql复制代码select id from amj_devinfo where devcho = 77 limit 20000, 10; // 查询时间2.064s
select * from amj_devinfo where devcho = 77 limit 20000, 10; // 查询时间3.716s

这里 第一条语句因为字段比第二条语句中少;所以,放入sort_buffer中的数据是不同的;

问题回顾:我之前就在想,为什么我基于索引列查询id会这么慢?我当时没想到索引失效问题;后来,我是怎么发现这个问题的呢?因为,基于索引列查询的时候,Mysql要扫描的字段也就是20010条数据即可;而我查看Handler_read_next(此选项表明在进行索引扫描时,按照索引从数据文件里取数据的次数)时,

1
js复制代码Handler_read_next 4274160

explain分析结果:
image.png
如果,扫描这么多行,需要这么多时间是可以理解的,那么,为什么需要扫描这么多行呢?
我那时候,重新看了一下表的设计,发现原来devcho字段的类型是varchar;这个时候,就想到了索引失效这个问题;

4.2.1 为什么会索引失效?

既然,发现了类型不同导致索引失效,那么就分析一下,为什么会导致索引失效?这条sql又将如何执行?
因为,他是基于索引列找的。但是,由于77 != ‘77’所以,这就导致了索引实现;但是,最终它还是找到了数据,这个时候,结合了扫描行数,我个人感觉应该是采用了全表扫描,然后,通过,强制类型转换,cpu进行判断,查询所得;

当改成 select id from amj_devinfo where devcho = "77" limit 20000, 10;就没有这个问题了;扫描的行数为20009行; 所以,在写sql语句的过程中还是要注意啊;

字段为varchar 传入 int 会索引失效,那么,字段为bigint 传入 “String” 会失效吗?

经过测试:不会失效;

image.png

所以,在Mybatis中,可以放心使用#{}占位符了;

4.3 一个有趣的现象

大扫描行数 VS 随机IO

1
2
sql复制代码select * from amj_devinfo  where devcho  = 77 limit 20000, 10; 查询时间 3.311s
select * from amj_devinfo where devcho = "77" limit 20000, 10; 查询时间 3.188s

第一个sql扫描的行数是500多万行; 但是,由于每个行都需要读入内存中,使用的是顺序IO
第二个sql扫描的行数是20010行,但是,需要访问随机IO 20010次;其实,基本上也就把所有的页表都找了一次;

小总结:随机IO,查询次数都要避免;

总结

本文,主要是模拟了分页查询中,往后数据查询较慢的现象,以及分析了速度较慢的原因;limit导致随机回表数增多。并提供了解决方法,先找到符合条件的id;然后,根据id做内联查询,减少随机IO的次数;并且,总结了一下自己出现的问题以及原因;如果,有一些个人见解不一定正确的话,希望大家多多指正;

本文转载自: 掘金

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

依赖倒置原则:高层代码和底层代码,到底谁该依赖谁?

发表于 2021-05-13

时间告诉我,无理取闹的年龄过了,该懂事了

前言

上一篇,我们讲了 ISP 原则,知道了在设计接口的时候,我们应该设计小接口,不应该让使用者依赖于用不到的方法。

依赖这个词,程序员们都好理解,意思就是,我这段代码用到了谁,我就依赖了谁。依赖容易有,但能不能把依赖弄对,就需要动点脑子了。如果依赖关系没有处理好,就会导致一个小改动影响一大片,而把依赖方向搞反,就是最典型的错误。

那什么叫依赖方向搞反呢?我们就来讨论关于依赖的设计原则:依赖倒置原则。

谁依赖谁

依赖倒置原则(Dependency inversion principle,简称 DIP)是这样表述的:

高层模块不应依赖于低层模块,二者应依赖于抽象。

抽象不应依赖于细节,细节应依赖于抽象。

学习这个原则,最重要的是要理解“倒置”,而要理解什么是“倒置”,就要先理解所谓的“正常依赖”是什么样的。

我们很自然地就会写出类似下面的这种代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码class CriticalFeature {
private Step1 step1;
private Step2 step2;
...

void run() {
// 执行第一步
step1.execute();
// 执行第二步
step2.execute();
...
}
}

但是,这种未经审视的结构天然就有一个问题:高层模块会依赖于低层模块。在上面这段代码里,CriticalFeature 类就是高层类,Step1 和 Step2 就是低层模块,而且 Step1 和 Step2 通常都是具体类。虽然这是一种自然而然的写法,但是这种写法确实是有问题的。

在实际的项目中,代码经常会直接耦合在具体的实现上。比如,我们用 Kafka 做消息传递,我们就在代码里直接创建了一个 KafkaProducer 去发送消息。我们就可能会写出这样的代码:

1
2
3
4
5
6
7
8
9
10
java复制代码class Handler {
private KafkaProducer producer;

void send() {
...
Message message = ...;
producer.send(new KafkaRecord<>("topic", message);
...
}
}

也许你会问,我就是用了 Kafka 发消息,创建一个 KafkaProducer,这有什么问题吗?其实,我们需要站在长期的角度去看,什么东西是变的、什么东西是不变的。Kafka 虽然很好,但它并不是系统最核心的部分,我们在未来是可能把它换掉的。

你可能会想,这可是我实现的一个关键组件,我怎么可能会换掉它呢?软件设计需要关注长期、放眼长期,所有那些不在自己掌控之内的东西,都是有可能被替换的。其实,替换一个中间件是经常发生的。所以,依赖于一个可能会变的东西,从设计的角度看,并不是一个好的做法。那我们应该怎么做呢?这就轮到倒置登场了。

所谓倒置,就是把这种习惯性的做法倒过来,让高层模块不再依赖于低层模块。那要是这样的话,我们的功能又该如何完成呢?计算机行业中一句名言告诉了我们答案:

计算机科学中的所有问题都可以通过引入一个间接层得到解决。

是的,引入一个间接层。这个间接层指的就是 DIP 里所说的抽象。也就是说,这段代码里面缺少了一个模型,而这个模型就是这个低层模块在这个过程中所承担的角色。

既然这个模块扮演的就是消息发送者的角色,那我们就可以引入一个消息发送者(MessageSender)的模型:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码interface MessageSender {
void send(Message message);
}

class Handler {

void send(MessageSender sender) {
...
sender.send(message);
...
}
}

有了消息发送者这个模型,那我们又该如何把 Kafka 和这个模型结合起来呢?那就要实现一个 Kafka 的消息发送者:

1
2
3
4
5
6
7
8
java复制代码class KafkaMessageSender implements MessageSender {

private KafkaProducer producer;

public void send(final Message message) {
this.producer.send(new KafkaRecord<>("topic", message));
}
}

消费者可以这样消费消息:

1
2
java复制代码Handler handler = new Handler();
handler.send(new KafkaMessageSender());

这样一来,高层模块就不像原来一样直接依赖低层模块,而是将依赖关系“倒置”过来,让低层模块去依赖由高层定义好的接口。这样做的好处就在于,将高层模块与低层实现解耦开来。

img

如果未来我们要用RabbitMQ替换掉 Kafka,只要重写一个 MessageSender 就好了,其他部分并不需要改变。这样一来,我们就可以让高层模块保持相对稳定,不会随着低层代码的改变而改变。

1
2
3
4
5
6
7
8
9
10
11
java复制代码class RabbitmqMessageSend implements MessageSender {

private RabbitTemplate rabbitTemplate;

public void send(final Message message) {
rabbitTemplate.setExchange(exchangeKey);
rabbitTemplate.setRoutingKey(routingKey);
CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
this.rabbitTemplate.convertAndSend(exchangeKey,routingKey,message,correlationId);
}
}

消费者可以这样消费消息:

1
2
java复制代码Handler handler = new Handler();
handler.send(new RabbitmqMessageSend());

依赖于抽象

抽象不应依赖于细节,细节应依赖于抽象。

其实,这个可以更简单地理解为一点:依赖于抽象,从这点出发,我们可以推导出一些更具体的指导编码的规则:

  • 任何变量都不应该指向一个具体类;
  • 任何类都不应继承自具体类;
  • 任何方法都不应该改写父类中已经实现的方法。

举个List 声明的例子,其实背后遵循的就是这里的第一条规则:

1
java复制代码List<String> list = new ArrayList<>();

在实际的项目中,这些编码规则有时候也并不是绝对的。如果一个类特别稳定,我们也是可以直接用的,比如字符串类。但是,请注意,这种情况非常少。因为大多数人写的代码稳定度并没有那么高。所以,上面几条编码规则可以成为覆盖大部分情况的规则,出现例外时,我们就需要特别关注一下。

总结

  1. 如果说实现开闭原则的关键事抽象化,是面向对象设计的目标的话,依赖倒置原则就是这个面向对象设计的主要机制。
  2. 依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:
    • 每个类尽量提供接口或抽象类,或者两者都具备。
    • 变量的声明类型尽量是接口或者是抽象类。
    • 任何类都不应该从具体类派生。
    • 使用继承时尽量遵循里氏替换原则。

本文转载自: 掘金

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

Spring Security动态权限控制居然如此简单 |

发表于 2021-05-13

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接

之前在动态权限控制的教程中,我们通过自定义FilterInvocationSecurityMetadataSource和AccessDecisionManager 两个接口实现了动态权限控制。这里需要我们做的事情比较多,有一定的学习成本。今天来介绍一种更加简单和容易理解的方法实现动态权限控制。

基于表达式的访问控制

1
2
3
java复制代码httpSecurity.authorizeRequests()
.anyRequest()
.access("hasRole('admin')")

这种方式不用多说了吧,我们配置了表达式hasRole('admin')后,Spring Security会调用SecurityExpressionRoot的hasRole(String role)方法来判断当前用户是否持有角色admin,进而作出是否放行的决策。这种方式除了可以静态的权限控制之外还能够动态的权限控制。

基于Bean的访问控制表达式

Spring Security扩展了对表达式进行了扩展,支持引用任何公开的Spring Bean,假如我们有一个实现下列接口的Spring Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 角色检查器接口.
*
* @author n1
* @since 2021 /4/6 16:28
*/
public interface RoleChecker extends InitializingBean {

/**
* Check boolean.
*
* @param authentication the authentication
* @param request the request
* @return the boolean
*/
boolean check(Authentication authentication, HttpServletRequest request);
}

基于JDBC的角色检查,最好这里做个缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码/**
* 基于jdbc的角色检查 最好这里做个缓存
* @author n1
* @since 2021/4/6 16:43
*/
public class JdbcRoleChecker implements RoleChecker {
// 系统集合的抽象实现,这里你可以采用更加合理更加效率的方式
private Supplier<Set<AntPathRequestMatcher>> supplier;


@Override
public boolean check(Authentication authentication, HttpServletRequest request) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

// 当前用户的角色集合
System.out.println("authorities = " + authorities);
//todo 这里自行实现比对逻辑
// supplier.get().stream().filter(matcher -> matcher.matches(request));
// true false 为是否放行
return true;
}

@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(supplier.get(), "function must not be null");
}
}

我们就可以这样配置HttpSecurity:

1
2
3
java复制代码httpSecurity.authorizeRequests()
.anyRequest()
.access("@roleChecker.check(authentication,request)")

通过RoleChecker中的Authentication我们可以获得当前用户的信息,尤其是权限集。通过HttpServletRequest我们可以获得当前请求的URI。该URI在系统中的权限集和用户的权限集进行交集判断就能作出正确的访问决策。

路径参数

有些时候我们的访问URI中还包含了路径参数,例如/foo/{id}。我们也可以通过基于Bean的访问控制表达式结合具体的id值来控制。这时应该这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 角色检查器接口.
*
* @author n1
* @since 2021 /4/6 16:28
*/
public interface RoleChecker extends InitializingBean {

/**
* Check boolean.
*
* @param authentication the authentication
* @param request the request
* @return the boolean
*/
boolean check(Authentication authentication, String id);
}

对应的配置为:

1
2
3
java复制代码httpSecurity.authorizeRequests()
.antMatchers("/foo/{id}/**")
.access("@roleChecker.check(authentication,#id)")

这样当/foo/123请求被拦截后,123就会赋值给check方法中的id处理。

总结

这种表达式的动态权限控制比之前的方式更加容易掌握和理解。但是它也有它的局限性,比如表达式中的方法中的参数类型比较单一。而通过FilterInvocationSecurityMetadataSource的方式则更加强大可以自定义一些访问决策,适合更加复杂的场景。我是:码农小胖哥,多多关注,分享更多原创编程干货。

本文转载自: 掘金

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

1…670671672…956

开发者博客

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