1、数据库分库分表
1.1、业务分库
按照业务模块将数据分散到不同的数据库服务器,比如用户数据、商品数据、订单数据存放在三个不同的数据库服务器中,分散存储和访问压力。但也会带来一些问题:
- join 操作问题:业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。
- 事务问题:原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。
- 成本问题:业务分库同时也带来了成本的代价,本来 1 台服务器,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台。
1.2、主从复制和读写分离:
将数据库读写操作分散到不同的节点上。
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机
1.3、数据库分表
单表数据拆分有两种方式:垂直分表和水平分表,用于分散存储压力和提升性能
- 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。
- 水平分表适合表行数特别大的表。
但也会引入复杂性:
水平分表可能存在分布不均匀的问题,一个表中数据非常多,而另一个表中数据非常少。
2、雪花算法:
是分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性
- 长度共64bit(一个long型)。
- 首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。
- 41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。
- 10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。
- 12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。
3、MP(MyBatis-Plus)
3.1、主键策略:
MyBatis-Plus默认的主键策略是:ASSIGN_ID (使用了雪花算法)
@TableId(type = IdType.ASSIGN_ID)
AUTO主键自增:
@TableId(type = IdType.AUTO)
3.2、自动填充
如插入时间、更新时间的自动填充。添加@TableField
@Data
public class User {
@TableField(fill = FieldFill.INSERT)
private Date createTime;
//@TableField(fill = FieldFill.UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
同时也要实现元对象处理器接口:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
3.3、乐观锁和悲观锁
悲观锁:假设多个线程会同时访问一个共享资源,并且这些线程会试图修改它。悲观锁的策略是假设最坏情况,即认为在任何时候都有可能有另一个线程来修改这个共享资源,因此在访问前会先锁定这个资源,以防止其他线程修改。悲观锁的典型实现是数据库中的行锁或表锁。
乐观锁:相反,乐观锁的策略是认为多个线程同时访问共享资源的概率很小,因此不需要在访问前锁定资源。相反,每次访问共享资源时,先获取一个版本号或时间戳,并在更新数据时检查这个版本号或时间戳是否被其他线程修改过。如果检查到冲突,则放弃当前操作。乐观锁的典型实现是在Java中使用的CAS(Compare And Swap)操作。
在并发控制的选择上,悲观锁一般会降低并发性,因为它会频繁地加锁和解锁资源,而乐观锁则可以更好地保持并发性,因为它只在冲突发生时才进行回滚操作。但是,乐观锁在并发更新高的情况下容易发生冲突,因为每次更新都需要检查版本号或时间戳,这会增加系统的开销。因此,在实际应用中,应该根据具体情况选择适合的并发控制策略。
Mybatis-Plus项目中,使用乐观锁
- 数据库中添加version字段
- 添加 @Version 注解
@Version
private Integer version;
创建配置文件并注册乐观锁插件:
@EnableTransactionManagement
@Configuration
@MapperScan("com.atguigu.mybatis_plus.mapper")
public class MybatisPlusConfig {
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
其中:
@EnableTransactionManagement是Spring Framework中的一个注解,用于启用Spring的事务管理功能。
当多个业务操作可能需要同时对多个数据源进行读写操作。如果这些操作不是原子性的,可能会导致数据不一致或错误的结果。为了解决这个问题,应用程序需要使用事务管理来确保一组业务操作被视为单个操作,即要么全部成功,要么全部回滚。
@EnableTransactionManagement注解会启用Spring框架中的事务管理功能,它会基于AOP机制拦截业务层方法调用,自动管理这些方法中的事务,以确保它们被作为一个单独的操作执行。当使用@EnableTransactionManagement注解时,我们需要在Spring配置文件中配置事务管理器、事务拦截器、数据源等相关内容。
3.4分页插件
添加分页插件配置:
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
使用分页
@Test
public void testSelectPage() {
Page<User> page = new Page<>(1,5);//第一个参数为当前页,第二个为每页的记录个数
Page<User> pageParam = userMapper.selectPage(page, null);
pageParam.getRecords().forEach(System.out::println); //获取当前页记录
System.out.println(pageParam.getCurrent()); //获取当前页
System.out.println(pageParam.getPages()); //总页数
System.out.println(pageParam.getSize()); //记录个数
System.out.println(pageParam.getTotal()); //总记录
System.out.println(pageParam.hasNext()); //是否有下一页
System.out.println(pageParam.hasPrevious()); //是否有上一页
}
返回特定的列:使用结果集Map,例如下学生有姓名、性别、学号、电话、住址等,但是只返回姓名、性别、学号。
@Test
public void testSelectMapsPage() {
Page<Map<String, Object>> page = new Page<>(1, 5);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();//条件构造器
queryWrapper.select("name", "age");//选择要返回的列
Page<Map<String, Object>> pageParam = userMapper.selectMapsPage(page, queryWrapper);
List<Map<String, Object>> records = pageParam.getRecords();
records.forEach(System.out::println);
System.out.println(pageParam.getCurrent());
System.out.println(pageParam.getPages());
System.out.println(pageParam.getSize());
System.out.println(pageParam.getTotal());
System.out.println(pageParam.hasNext());
System.out.println(pageParam.hasPrevious());
}
3.5、逻辑删除
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据
- 逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录
@TableLogic
private Integer deleted;
4、统一返回结果
创建一个返回码定义枚举类,然后创建结果类
5、统一异常处理,特定异常处理,自定义异常
@ControllerAdvice是Spring Framework中的一个注解,用于定义一个全局的异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //用于处理不同类型的异常
@ResponseBody
public R error(Exception e){
e.printStackTrace();
return R.error();
}
}
添加特定异常处理,比如http异常
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public R error(HttpMessageNotReadableException e){
e.printStackTrace();
return R.setResult(ResultCodeEnum.JSON_PARSE_ERROR);
}
自定义异常:创建自定义异常类,继承RuntimeException
@Data
public class GuliException extends RuntimeException {
//状态码
private Integer code;
/**
* 接受状态码和消息
* @param code
* @param message
*/
public GuliException(Integer code, String message) {
super(message);
this.code=code;
}
/**
* 接收枚举类型
* @param resultCodeEnum
*/
public GuliException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
}
@Override
public String toString() {
return "GuliException{" +
"code=" + code +
", message=" + this.getMessage() +
'}';
}
}
添加异常处理方法:
@ExceptionHandler(GuliException.class)
@ResponseBody
public R error(GuliException e){
log.error(ExceptionUtils.getMessage(e));
return R.error().message(e.getMessage()).code(e.getCode());
}
在业务中需要的地方抛出这个异常
public R upload(...) {
try {
......
} catch (Exception e){
log.error(ExceptionUtils.getMessage(e));
throw new GuliException(ResultCodeEnum.FILE_UPLOAD_ERROR);
}
}
6、阿里云存储OSS服务
开通这个服务,会获得相应的keyid,keysecret字段,然后根据官方的技术文档写代码,实现讲师头像的上传和删除。
7、Nacos
- 引入依赖,
- 添加配置,将服务注册进Nacos
-
spring: application: name: service-edu # 服务名 cloud: nacos: discovery: server-addr: localhost:8848 # nacos服务地址
- 客户端启动类添加注解@EnableDiscoveryClient
8、OpenFeign
引入依赖,
启动类添加注解@EnableFeignClients
在OSS Service中实现业务接口,然后再edu服务中通过OpenFeign远程调用,在创建远程调用接口的类上加@FeignClient("service-oss"),并在请求方法中添加对应的请求地址