主键生成策略

默认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.需要编码和配置的工作量比较大

主键递增

  1. 实体类字段上@TableId(type = IdType.AUTO)
  2. 数据库字段一定要是自增的!

自动填充

阿里巴巴开发手册:所有的数据库表:gmt_create、gmt_modified几乎所有的表都要配置上!而且需要自动化!

  1. 实体类字段属性上需要增加注解
//字段添加填充内容
      //插入的时候填充字段
      @TableField(fill = FieldFill.INSERT)
      private Date createTime;
      //插入和更新的时候填充字段
      @TableField(fill = FieldFill.INSERT_UPDATE)
      private Date updateTime;
  
  1. 编写处理器来处理这个注解即可!
@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);
  }