项目前置配置

IDEA设置热部署

2022版本IDEA配置参考博客

step1: setting->Compiler

image-20231127231931248

step2:setting-Advance Settings

image-20231127232101600

实现代码关联远程git仓库

实际场景中,完成一个小功能应当及时提交远程仓库

vcs->enable Version Control:相当于git init

image-20231127233059818

IDEA右上角出现git相关的功能按钮

image-20231127233400586

左下角git可以查看对应的信息,比如日志、未提交的文件等

image-20231127233536503

Terminal下关联自己的github`执行以下操作

1
2
3
4
5
6
7
8
9
10
11
12
#关联自己的github(以前设置后无需重复设置,ssh key免登录)
git config --global user.name xxx
git config --global user.email xxx

#将其与远程仓库关联起来,其中origin是别名
git remote add origin 远程github地址(注意是ssh形式地址)
#提交至远程仓库
git push -u origin 分支名
#删除关联远程仓库
git remote remove origin
#查看关联远程仓库列表
git remote -v

新建子模块

整个project由不同的子Module组成,project下的pom.xml只做模块管理

整个项目目录如下:

image-20231128225726688

日志的相关配置

项目启动信息配置

改写启动类

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class MemberApplication {
private static final Logger LOG = LoggerFactory.getLogger(MemberApplication.class);
public static void main(String[] args) {
// SpringApplication.run(MemberApplication.class, args);
//获取项目启动环境
SpringApplication app = new SpringApplication(MemberApplication.class);
Environment env = app.run(args).getEnvironment();
LOG.info("启动成功");
LOG.info("地址:\thttp://127.0.0.1:{}",env.getProperty("server.port"));
}
}

image-20231128230602153

banner.txt在线生成工具

项目运行日志

resource目录下新建日志配置文件loggback-spring.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- log日志文件存储路径 -->
<property name="PATH" value="./log/memeber"></property>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
<Pattern>%d{hh:mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
</encoder>
</appender>

<appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/trace.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
</appender>

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<root level="ERROR">
<appender-ref ref="ERROR_FILE" />
</root>

<root level="TRACE">
<appender-ref ref="TRACE_FILE" />
</root>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

使用Http Client完成测试接口

IDEA自带Http Clinet插件,只要新建.http文件,即可发起http请求

image-20231129000739903

image-20231129000858213

image-20231129000956969

增加AOP打印请求参数和返回结果

AOPInterceptor都可以实现此功能,但是Interceptor只能处理Controller层的处理结果,

memeber模块下新建AOP打印日志类aspect.LogAspect

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
package com.bang.train.member.aspect;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

@Aspect
@Component
public class LogAspect {
public LogAspect() {
System.out.println("Common LogAspect");
}

private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

/**
* 定义一个切点:作用于package com.bang下的所有类名中带有Controller的类
*/
@Pointcut("execution(public * com.bang..*Controller.*(..))")
public void controllerPointcut() {
}

@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) {

// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();

// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());

// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));

// 排除特殊类型的参数,如文件类型
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等
String[] excludeProperties = {};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}

@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等
String[] excludeProperties = {};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}

}

image-20231129233256857

新建公共子模块common

微服务项目一般存在多个模块,每个模块对应一个服务,负责某一项功能,各个模块可能存在许多公共的代码和相同的依赖,此时为了减小代码冗余和代码管理和修改,我们在项目下新建一个子模块common

  • 将公共代码放在此模块下
    • 比如:工具类、拦截器、AOP、常量、枚举类、公共配置等
  • 将公共的依赖包放在此模块下的pom文件中
    • 根目录下的pom文件负责依赖包的版本管理
    • 公共模块下的pom文件负责管理需要导入的包

比如:上述AOP实现打印请求和返回结果的日志代码就可以移除到该模块下,但要注意,此时应该修改memeber模块下启动类的扫描范围,即@ComponentScan("com.bang.train.*")

增加公共模块后的项目目录

image-20231129235041656

新建网关模块

网关模块主要用于:路由转发、请求校验

网关模块的配置文件application.yaml

1
2
3
4
5
6
7
8
9
10
server:
port: 8000
spring:
cloud:
gateway:
routes:
- id: memeber
uri: http://127.0.0.1:8001 #目前项目中未使用注册中心Nacos,所以只能使用ip地址+端口号的形式表明需要转发的地址
predicates:
- Path=/member/**

本地数据库的构建

对于各个项目而言,最好能够做到配置专库专用,对于一个项目,新建对应数据库的同时,创建一个专门的用户,将该用户的权限局限于对本项目对应数据库的增删改查,避免影响服务器中其他数据库里的数据。

集成Mybatis持久层框架

引入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- 集成mysql连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

配置数据库连接

application.yaml配置文件中进行设置

集成Mybatis官方生成器

利用mybatis框架,需要:编写持久层接口->编写对应的mapper.xml文件(需要手动编写对应的SQL语句)

以上过程需要耗费较多经历,为简化开发可以使用以下两种替代方案

  • Mybatis-Plus第三方框架
  • Mybatis+官方生成器

这里我们采用第二种方案Mybatis+官方生成器

使用Mybatis官方生成器步骤

  1. 新建一个新的maven项目generator

  2. generator项目的pom文件中引入mybatis generator自动生成代码插件

    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
    <build>
    <plugins>
    <!-- mybatis generator 自动生成代码插件 -->
    <plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.4.0</version>
    <configuration>
    <!-- 配置文件申明,为什么数据库、什么表生成代码,都需要在该配置文件中定义-->
    <!--<configurationFile>src/main/resources/generator-config-member.xml</configurationFile>-->
    <configurationFile>src/main/resources/generator-config-business.xml</configurationFile>
    <!--<configurationFile>src/main/resources/generator-config-batch.xml</configurationFile>-->
    <overwrite>true</overwrite>
    <verbose>true</verbose>
    </configuration>
    <dependencies>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
    </dependency>
    </dependencies>
    </plugin>
    </plugins>
    </build>
  3. 编写对应的配置文件src/main/resources/generator-config-member.xml

    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
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
    PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
    "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

    <generatorConfiguration>
    <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">

    <!-- 自动检查关键字,为关键字增加反引号 -->
    <property name="autoDelimitKeywords" value="true"/>
    <property name="beginningDelimiter" value="`"/>
    <property name="endingDelimiter" value="`"/>

    <!--覆盖生成XML文件-->
    <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
    <!-- 生成的实体类添加toString()方法 -->
    <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>

    <!-- 不生成注释 -->
    <commentGenerator>
    <property name="suppressAllComments" value="true"/>
    </commentGenerator>

    <!-- 配置数据源,需要根据自己的项目修改 -->
    <jdbcConnection driverClass="com.mysql.jdbc.Driver"
    connectionURL="jdbc:mysql://localhost:3306/train_member?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false"
    userId="train_member"
    password="wu123456">
    </jdbcConnection>

    <!-- domain类的位置 targetProject是相对pom.xml的路径-->
    <javaModelGenerator targetProject="../member/src/main/java"
    targetPackage="com.bang.train.member.domain"/>

    <!-- mapper xml的位置 targetProject是相对pom.xml的路径 -->
    <sqlMapGenerator targetProject="../member/src/main/resources"
    targetPackage="mapper"/>

    <!-- mapper类的位置 targetProject是相对pom.xml的路径 -->
    <javaClientGenerator targetProject="../member/src/main/java"
    targetPackage="com.bang.train.member.mapper"
    type="XMLMAPPER"/>

    <table tableName="member" domainObjectName="Member"/>
    <!--<table tableName="passenger" domainObjectName="Passenger"/>-->
    <!-- <table tableName="ticket" domainObjectName="Ticket"/>-->
    </context>
    </generatorConfiguration>
  4. 点击mybatis generator对应maven插件,会生成对应的代码文件

    image-20231203231111219

  5. 生成的代码文件

    • com.bang.train.member.domain.Member:数据库train_membermember表对应的java实体类(PO)
    • com.bang.train.member.domain.MemberExample:组装SQL语句中where后面的条件对应的实体类;条件构建器,用于构建SQL语句中的各种条件
    • com.bang.train.member.mapper.MemberMapper:持久层对应的接口
    • src/main/resources/mapper/memberMapper.xml:对应的mapper.xml文件,里面含有各种SQL语句

    image-20231203231302379

  6. 注意:

    以上四个文件一定不要去动,每次重新店家genartor maven插件,这四个文件都会被覆盖重写;如果官方生成器对应插件无法满足项目需求,自定义的代码应编写在新的文件里,千万不要直接在这四个文件后面追加。

会员注册接口开发

业务层

com.bang.train.member.IMemberService接口下新建抽象方法register

1
2
3
4
5
6
/**
* 用户注册功能
* @param mobile 用户提供的手机号
* @return
*/
long register(String mobile);

com.bang.train.member.MemberServiceImpl类下实现对应抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public long register(String mobile) {
//1.判断当前手机号是否已被注册
MemberExample memberExample = new MemberExample();
memberExample.createCriteria().andMobileEqualTo(mobile);
List<Member> memberList = memberMapper.selectByExample(memberExample);
if(!CollUtil.isEmpty(memberList)){//非空,说明手机号已被注册
throw new RuntimeException("手机号已被注册");
}
Member member = new Member();
member.setId(System.currentTimeMillis());
member.setMobile(mobile);
memberMapper.insert(member);
return member.getId();
}

控制层

1
2
3
4
@PostMapping("/register")
public long register(String mobile){
return memberService.register(mobile);
}

编写http文件利用Http Client进行测试

1
2
3
4
POST http://localhost:8001/member/register
Content-Type: application/x-www-form-urlencoded

mobile=15823209537

封装请求参数和结果(此模块的代码个人认为没有电脑商城项目好))

封装请求参数

对于每个功能模块,将对应的请求参数封装成一个实体类,注意实体类的属性名与请求参数名要一致,这样前端请求会自动映射到实体类对应属性值

新建member模块下用户注册对应的请求参数实体类com.bang.train.member.req.MemberRegReq

1
2
3
4
5
6
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberRegReq {
String mobile;
}

封装响应结果

响应结果包含三大基本信息:响应状态、响应状态描述信息、响应数据

common模块新建公共响应实体类com.bang.train.common.resp.CommonResp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 响应公共实体类
* @param <T> 响应数据类类型,这里用泛型表示
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResp<T>{
//响应状态,true表示成功,false表示失败
public boolean success=true;
//响应状态描述信息
public String message;
//响应数据
public T content;

public CommonResp(T content){
this.content=content;
}
}

对应控制层代码修改

1
2
3
4
5
6
@PostMapping("/register")
public CommonResp<Long> register(MemberRegReq req){
CommonResp<Long> commResp = new CommonResp<>();
commResp.setContent(memberService.register(req));
return commResp;
}

统一异常处理(此模块的代码个人认为没有电脑商城项目好)

业务层根据业务逻辑和执行结果,会向上层抛出各种类型异常,控制层需要对异常进行处理,直接将异常抛给前端不友好,需要针对异常,转换成统一的响应结果数据格式,所以需要构建统一异常处理类,借助于Spring@ExceptionHandler注解来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
controller层统一的异常处理拦截
@ControllerAdvice使得该类对所有的Controller均有效
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public CommonResp<Void> handleException(Exception e){
CommonResp<Void> commResp = new CommonResp<>();
commResp.setSuccess(false);
commResp.setMessage(e.getMessage());
return commResp;
}
}

自定义异常

根据业务层的业务逻辑,自定义对应的异常类,可以考虑利用枚举类进行异常的管理

定义枚举类,统一管理自定义异常

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
package com.bang.train.common.exception;

public enum BusinessExceptionEnum {
MEMBER_EXIST_ERROR("手机号已注册");

private String desc;

BusinessExceptionEnum(String desc) {
this.desc = desc;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

@Override
public String toString() {
return "BusinessExceptionEnum{" +
"desc='" + desc + '\'' +
'}';
}
}

自定义业务异常类

1
2
3
4
5
6
7
8
9
10
11
12
package com.bang.train.common.exception;

import lombok.Data;

@Data
public class BusinessException extends RuntimeException{
public BusinessExceptionEnum E;

public BusinessException(BusinessExceptionEnum E) {
this.E = E;
}
}

修改统一的异常处理模块

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
package com.bang.train.common.controller;


import com.bang.train.common.exception.BusinessException;
import com.bang.train.common.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/*
controller层统一的异常处理拦截
@ControllerAdvice使得该类对所有的Controller均有效
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);

//未知类型异常
@ExceptionHandler(Exception.class)
@ResponseBody
public CommonResp<Void> handleException1(Exception e){
CommonResp<Void> commResp = new CommonResp<>();
commResp.setSuccess(false);
commResp.setMessage("未知类型异常,请联系管理员");
return commResp;
}
//业务类型异常
@ExceptionHandler(BusinessException.class)
@ResponseBody
public CommonResp<Void> handleException2(BusinessException e){
CommonResp<Void> commResp = new CommonResp<>();
commResp.setSuccess(false);
commResp.setMessage(e.getE().getDesc());
return commResp;
}
}

集成校验框架Validation

在实际生产环境中,大多数情况下需要对用户的输入参数进行校验,比如校验输入是否有特殊字符、手机号位数是否正确等;当然,输入的校验也可在前端进行

校验框架Validation的使用步骤

引入对应的pom依赖

1
2
3
4
5
<!-- Validation校验框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

通过注解方式进行参数校验

member模块中注册功能对应的请求参数实体类进行修改

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberRegReq {
//@NotBlank:该参数不能为空,为空返回message,不能进入对应请求
@NotBlank(message = "【手机号】不能为空")
String mobile;
}

在注册功能对应controller类的请求处理方法上加上注解@Valid让校验功能起效

1
2
3
4
5
6
@PostMapping("/register")
public CommonResp<Long> register(@Valid MemberRegReq req){
CommonResp<Long> commResp = new CommonResp<>();
commResp.setContent(memberService.register(req));
return commResp;
}

新增校验异常处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ExceptionHandler(Exception.class)
@ResponseBody
public CommonResp<Void> handleException1(Exception e){
CommonResp<Void> commResp = new CommonResp<>();
commResp.setSuccess(false);
if(e instanceof BindException){
BindException be = (BindException) e;
//获取异常提示信息
String message = be.getBindingResult().getAllErrors().get(0).getDefaultMessage();
commResp.setMessage(message);
LOG.error("校验异常:{}",message);
}else{
commResp.setMessage("未知类型异常,请联系管理员");
LOG.error("未知类型异常,请联系管理员");
}
return commResp;
}

雪花算法

member模块注册功能业务层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public long register(MemberRegReq req) {
String mobile = req.getMobile();
//1.判断当前手机号是否已被注册
MemberExample memberExample = new MemberExample();
memberExample.createCriteria().andMobileEqualTo(mobile);
List<Member> memberList = memberMapper.selectByExample(memberExample);
if(!CollUtil.isEmpty(memberList)){//非空,说明手机号已被注册
throw new BusinessException(BusinessExceptionEnum.MEMBER_EXIST_ERROR);
}
Member member = new Member();
member.setId(System.currentTimeMillis());
member.setMobile(mobile);
memberMapper.insert(member);
return member.getId();
}

目前,新注册用户的ID是用当前时间戳来表示,在高并发场景下存在非唯一性问题,因为同一时刻存在大量请求

目前的可采取的其他方法及其对应的问题

  • 采用时间戳+随机数问题:并发数大的情况下,仍然无法保证唯一性

  • ID自增:自增ID不适用于分布式数据库,分表分库场景,只适用于小型项目

  • UUID

    • UUID会影响索引效率,因为UUID是无序的,用一堆无序的ID来构建一个有序的索引目录,性能上肯定有问题
  • 雪花算法:全局唯一、有序增长、生成效率高

    • hutool工具内部集成了雪花算法

      • ```java
        //参数1为终端ID
        //参数2为数据中心ID
        //参数1和参数2保证了每台机器生成ID的唯一性
        Snowflake snowflake = IdUtil.createSnowflake(1, 1);
        long id = snowflake.nextId();
        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

        - [hutool工具参考链接](https://www.bookstack.cn/read/hutool/bfd2d43bcada297e.md)

        ### 雪花算法原理

        <img src="03 项目配置前置工作/image-20231205232621491.png" alt="image-20231205232621491" style="zoom:80%;" />

        ### 雪花算法使用过程的注意点

        - 数据中心以及机器ID怎么设置?
        - 方法1:利用redis自增序列,这样每台机器启动时,从redis获取一个ID,不重复
        - 方法2:利用数据库,为每台机器分配ID,保存ip和workID的关系
        - 时钟回拨
        - [时钟回拨解决方法参考](https://blog.csdn.net/qq_34687559/article/details/115910152)

        ### `member`模块`service`层代码变更

        将雪花算法获取ID代码进行封装

        ```java
        public class SnowUtil {
        //workerId和datacenterId两个属性值,在项目启动时,可以通过redis或者数据库读取,读取之后,该值就会固定不变
        private static int workerId = 1;
        private static int datacenterId = 1;
        public static long getSnowflakeId(){
        return IdUtil.getSnowflake(workerId,datacenterId).nextId();
        }
        public static String getSnowflakeStr(){
        return IdUtil.getSnowflake(workerId,datacenterId).nextIdStr();
        }

        }

更改service层注册功能代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public long register(MemberRegReq req) {
String mobile = req.getMobile();
//1.判断当前手机号是否已被注册
MemberExample memberExample = new MemberExample();
memberExample.createCriteria().andMobileEqualTo(mobile);
List<Member> memberList = memberMapper.selectByExample(memberExample);
if(!CollUtil.isEmpty(memberList)){//非空,说明手机号已被注册
throw new BusinessException(BusinessExceptionEnum.MEMBER_EXIST_ERROR);
}
Member member = new Member();
//采用雪花算法生成注册用户在数据库中对应的ID
member.setId(SnowUtil.getSnowflakeId());
member.setMobile(mobile);
memberMapper.insert(member);
return member.getId();
}