主键生成策略
默认ASSIGN_ID全局唯一ID
分布式系统唯一ID生成方案汇总
系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会有多个ID生成的策略。下面就介绍一些常见的ID生成策略。
1.数据库自增长序列或字段
最常见的方式,利用数据库,全数据库唯一
优点:
1.简单,代码方便,性能可以接受
2.数字ID天然排序,对分页或者需要排序的结果很有帮助
缺点:
1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
3)在性能达不到要求的情况下,比较难于扩展。(不适用于海量高并发)
4)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。
5)分表分库的时候会有麻烦。
6)并非一定连续,类似MySQL,当生成新ID的事务回滚,那么后续的事务也不会再用这个ID了。这个在性能和连续性的折中。如果为了保证连续,必须要在事务结束后才能生成ID,那性能就会出现问题。
7)在分布式数据库中,如果采用了自增主键的话,有可能会带来尾部热点。分布式数据库常常使用range的分区方式,在大量新增记录的时候,IO会集中在一个分区上,造成热点数据。
优化方案:
针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。
2.UUID
常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一。UUID是由32个的16进制数字组成,所以每个UUID的长度是128位(16^32 = 2^128)。UUID作为一种广泛使用标准,有多个实现版本,影响它的因素包括时间、网卡MAC地址、自定义Namesapce等等。
优点:
1.简单,代码方便
2.生成ID性能非常好,基本上不会有性能问题
3.全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更的情况下,可以从容应对
缺点:
1.没有排序,无法保证趋势递增
2.UUID往往使用是字符串存储,查询的效率比较低
3.存储空间比较大,如果是海量数据库,就需要考虑存储量的问题
4.传输数据量大
5.不可读
3.Redis生成的ID
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21、B:2,7,12,17,22、C:3,8,13,18,23、D:4,9,14,19,24、E:5,10,15,20,25
优点:
1.不依赖数据库,灵活方便,且性能优于数据库
2.数字ID天然排序,对分页或者需要排序的结果很有帮助
缺点:
1.如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
2.需要编码和配置的工作量比较大
主键递增
- 实体类字段上
@TableId(type = IdType.AUTO)
- 数据库字段一定要是自增的!
自动填充
阿里巴巴开发手册:所有的数据库表:gmt_create、gmt_modified几乎所有的表都要配置上!而且需要自动化!
- 实体类字段属性上需要增加注解
//字段添加填充内容
//插入的时候填充字段
@TableField(fill = FieldFill.INSERT)
private Date createTime;
//插入和更新的时候填充字段
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
- 编写处理器来处理这个注解即可!
@Slf4j//对日志依赖的引用
@Component//一定不要忘记把处理器增加到IOC容器中!
public class MyMetaObjectHandler implements MetaObjectHandler {
//插入时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill......");
this.setFieldValByName("createTime",new Date(),metaObject);
//MetaObject[反射对象类]是Mybatis的工具类,通过MetaObject获取和设置对象的属性值。
this.setFieldValByName("updateTime",new Date(),metaObject);
}
//更新时的填充策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill.....");
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
乐观锁和悲观锁
乐观锁:顾名思义十分乐观,它总是认为不会出现问题,无论干什么不去上锁!如果出现了问题,就上锁,更新失败悲观锁:顾名思义十分悲观,它总是认为会出现问题,无论干什么都会去上锁!再去操作
乐观锁实现方式
取出记录时,获取当前version更新时,带上这个version执行更新时,set version = newVersion where version = oldVersion如果version不对,就更新失败【简而言之,就是持续更新,当发现版本不对时,也就是发现问题时,就上锁,更新失败】
//乐观锁:1.先查询,获得版本号 version = 1
--A
update user set name = "kuangshen" ,version = version + 1
where id = 2 and version = 1
--B 线程抢先完成,这个时候 version = 2,会导致 A 修改失败!
update user set name = "kuangshen" ,version = version + 1
where id = 2 and version = 1
1.给数据库中增加version字段!
2.我们实体类要加对应的字段
@Version//乐观锁Version注解
private Integer version;
3.注册组件
//扫描我们的mapper文件夹
@MapperScan("com.kuang.mapper")
@EnableTransactionManagement//自动管理事务开启,默认也是开启的
@Configuration//这是一个配置类
public class MybatisPlusConfig {
//注册乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
4.测试
//测试乐观锁成功
@Test
public void testOptimisticLocker(){
//1.查询
User user = userMapper.selectById(1L);
//2.修改,它很乐观它觉得这个用户没有问题,,进行修改
user.setName("呀嘿");
user.setEmail("5555555@qq.com");
user.setAge(2);
//3.执行更新操作
userMapper.updateById(user);
}
//测试乐观锁失败
@Test
public void testOptimisticLocker2(){
//线程一
User user = userMapper.selectById(1L);
user.setName("yiyi嘿11");
user.setEmail("111111@qq.com");
//模拟另外一个线程执行了插队操作
User user2 = userMapper.selectById(1L);
user2.setName("yi嘿哈22");
user2.setEmail("222222@qq.com");
userMapper.updateById(user2);
//自旋锁来多次尝试提交!
userMapper.updateById(user);//如果没有乐观锁就会覆盖插队线程的值!
}
分页查询
1.原始的limit进行分页
2.pageHelper第三方插件
3.MP内置了分页插件
分页插件
1.配置拦截器组件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
//乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
2.直接使用page对象
//测试分页查询
@Test
public void testPage(){
//参数一:当前页
//参数二:页面大小
//使用了分页插件后,所有的分页操作也变得简单了
IPage page = new Page<>(2, 5);
BaseMapper.selectPage(page,null);
page.getRecords().forEach(System.out::println);
System.out.println(page.getTotal());
}
逻辑删除
删除操作
//测试删除
//通过id
@Test
public void testDeleteById(){
userMapper.deleteById(1518117539816615937L);
}
//通过id批量删除
@Test
public void testDeleteBatchIds(){
userMapper.deleteBatchIds(Arrays.asList(1518117539816615938L,8L));
}
//通过map删除
@Test
public void testDeleteMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("name","yi嘿哈22");
userMapper.deleteByMap(map);
}
物理删除:从数据库中直接移除-逻辑删除:在数据库中没有被移除,而是通过一个变量来让他失效!delete = 0>>delete=1
管理员可以查看被删除的记录!防止数据的丢失,类似于回收站!
1.在数据库中增加一个delete字段,设置默认值
2.实体类中增加属性
@TableLogic//逻辑删除
private Integer deleted;
3.配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不进行@TableLogic注解)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
更新操作
==> Preparing: UPDATE book SET deleted=1 WHERE id=? AND deleted=0
==> Parameters: 2(Long)
<== Updates: 1
性能分析插件
相关的依赖架包
<!--执行 SQL 分析打印插件-->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.8.1</version>
</dependency>
yml配置
#数据库连接配置
datasource:
username: root
password:
url: jdbc:p6spy:mysql://localhost:3306/book?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8&tinyInt1isBit=true
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
在项目的resources下添加"spy.properties" p6spy的位置文件
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
条件构造器
复杂的sql语句可以用它来代替
@Test
void contextLoads(){
//查询name不为空,邮箱不为空的用户,年龄大于等于24
QueryWrapper<User> wrapper = new QueryWrapper<>();//和mapper很像,但是wrapper是一个对象,不用put,而是去用方法
wrapper
.isNotNull("name")
.isNotNull("email")
.ge("age",24);
userMapper.selectList(wrapper).forEach(System.out::println);
}