数据库


主键策略

1. 数据库自增长序列或字段

最常见的方式。利用数据库,全数据库唯一。

优点:

1)简单,代码方便,性能可以接受。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
3)在性能达不到要求的情况下,比较难于扩展。(不适用于海量高并发)
4)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。
5)分表分库的时候会有麻烦。
6)并非一定连续,类似MySQL,当生成新ID的事务回滚,那么后续的事务也不会再用这个ID了。这个在性能和连续性的折中。如果为了保证连续,必须要在事务结束后才能生成ID,那性能就会出现问题。
7)在分布式数据库中,如果采用了自增主键的话,有可能会带来尾部热点。分布式数据库常常使用range的分区方式,在大量新增记录的时候,IO会集中在一个分区上,造成热点数据。

优化方案:

1)针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。(可以用redis实现)

2. 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
这个,随便负载到哪个机确定好,未来很难做修改。但是3-5台服务器基本能够满足器上,都可以获得不同的ID。但是步长和初始值一定需要事先需要了。使用Redis集群也可以方式单点故障的问题。
另外,比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

优点:
1)不依赖于数据库,灵活方便,且性能优于数据库。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
2)需要编码和配置的工作量比较大。

2. UUID

常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一。UUID是由32个的16进制数字组成,所以每个UUID的长度是128位(16^32 = 2^128)。UUID作为一种广泛使用标准,有多个实现版本,影响它的因素包括时间、网卡MAC地址、自定义Namesapce等等。
优点:
1)简单,代码方便。
2)生成ID性能非常好,基本不会有性能问题。
3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

缺点:
1)没有排序,无法保证趋势递增。
2)UUID往往是使用字符串存储,查询的效率比较低。
3)存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
4)传输数据量大
5)不可读。

优化
1)为了解决UUID不可读,可以使用UUID to Int64的方法。
2)为了解决UUID无序的问题,NHibernate在其主键生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime)。

  1. Twitter的snowflake算法
    snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。具体实现的代码可以参看https://github.com/twitter/snowflake。雪花算法支持的TPS可以达到419万左右(2^22*1000)。
    雪花算法在工程实现上有单机版本和分布式版本。单机版本如下,分布式版本可以参看美团leaf算法:https://github.com/Meituan-Dianping/Leaf

参考:https://www.cnblogs.com/haoxinyue/p/5208136.html

MabatisPlus 实现乐观锁

官方文档

踩坑记录

  • 在数据库中添加字段version时,要有默认值
  • 只有先查询,再更新才会生效

多条件组合查询带分页

Mybatis分页配置

package com.izyq.admin_house.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.izyq.admin_house.dao")
public class MybatisPlusConfig {

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
  1. 把条件值传到接口
    • 封装多条件TeacherQuery
package com.izyq.edu.entity.vo;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
public class TeacherQuery {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "教师名称,模糊查询")
    private String name;
    @ApiModelProperty(value = "头衔 1高级讲师 2首席讲师")
    private Integer level;
    @ApiModelProperty(value = "查询开始时间", example = "2019-01-01 10:10:10")
    private String begin;//注意,这里使用的是String类型,前端传过来的数据无需进行类 型转换
    @ApiModelProperty(value = "查询结束时间", example = "2019-12-01 10:10:10")
    private String end;
}
  1. 多条件组合查询
    • 判断条件值是否为空,根据判断值,拼接条件
/**
    * 多条件组合查询带分页
    */
   @ApiOperation(value = "讲师多条件组合查询")
   @PostMapping("pageTheacherCondition/{current}/{limit}")
   public R pageTheacherCondition(@PathVariable long current, @PathVariable long limit,
                                 @RequestBody(required = false) TeacherQuery teacherQuery){
       //创建Page对象
       Page<EduTeacher> pageTeacher = new Page<>(current,limit);
       //创建条件
       QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();

       String name = teacherQuery.getName();
       Integer level = teacherQuery.getLevel();
       String begin = teacherQuery.getBegin();
       String end = teacherQuery.getEnd();
       //多条件组合查询
       //动态sql
       //判断条件值是否为空,根据判断值,拼接条件
       if(!StringUtils.isEmpty(name)){
           wrapper.like("name",name);
       }
       if(level != null){
           wrapper.eq("level",level);
           System.out.println("lever不为null");
       }
       if(!StringUtils.isEmpty(begin)){
           wrapper.ge("gmt_create",begin);
       }
       if(!StringUtils.isEmpty(end)){
           wrapper.le("gmt_modified",end);
       }

       Page<EduTeacher> page = eduTeacherService.page(pageTeacher, wrapper);


       return R.ok().data("total",page.getTotal()).data("item",page.getRecords());
   }
  1. 进一步封装
    • private IEduTeacherService eduTeacherService;
      • eduTeacherService是IEduTeacherService类 我们要在这里面去实现它
      • 在 IEduTeacherService 中添加方法
public interface IEduTeacherService extends IService<EduTeacher> {

    void pageQuery(Page<EduTeacher> pageTeacher, TeacherQuery teacherQuery);
}
  1. 在EduTeacherServiceImpl中实现
package com.izyq.edu.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.izyq.edu.entity.EduTeacher;
import com.izyq.edu.entity.vo.TeacherQuery;
import com.izyq.edu.mapper.EduTeacherMapper;
import com.izyq.edu.service.IEduTeacherService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 讲师 服务实现类
 * </p>
 *
 * @author zyq
 * @since 2022-05-03
 */
@Service
public class EduTeacherServiceImpl extends ServiceImpl<EduTeacherMapper, EduTeacher> implements IEduTeacherService {

    @Override
    public void pageQuery(Page<EduTeacher> pageParam, TeacherQuery teacherQuery) {
        QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();

        String name = teacherQuery.getName();
        Integer level = teacherQuery.getLevel();
        String begin = teacherQuery.getBegin();
        String end = teacherQuery.getEnd();
        //多条件组合查询
        //动态sql
        //判断条件值是否为空,根据判断值,拼接条件
        if (teacherQuery == null) {
            baseMapper.selectPage(pageParam, wrapper);
            return;
        }
        if (!StringUtils.isEmpty(name)) {
            wrapper.like("name", name);
        }
        if (level != null) {
            wrapper.eq("level", level);
            System.out.println("lever不为null");
        }
        if (!StringUtils.isEmpty(begin)) {
            wrapper.ge("gmt_create", begin);
        }
        if (!StringUtils.isEmpty(end)) {
            wrapper.le("gmt_modified", end);
        }

        baseMapper.selectPage(pageParam, wrapper);
    }
}
  1. 在Controller中使用
/**
    * 多条件组合查询带分页
    */
   @ApiOperation(value = "讲师多条件组合查询")
   @PostMapping("pageTheacherCondition/{current}/{limit}")
   public R pageTheacherCondition(@PathVariable long current, @PathVariable long limit,
                                 @RequestBody(required = false) TeacherQuery teacherQuery){
       //创建Page对象
       Page<EduTeacher> pageTeacher = new Page<>(current,limit);
       eduTeacherService.pageQuery(pageTeacher,teacherQuery);

       return R.ok().data("total",pageTeacher.getTotal()).data("item",pageTeacher.getRecords());
   }

自动填充

需求场景

对数据库中的 CreateTime updateTime 等这类参数进行自动填充

实现

使用MP自动填充插件实现 详情查看MP官方文档
https://baomidou.com/pages/4c6bcf/

遇到的问题

Json时间格式问题

#spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
#spring.jackson.time-zone=GMT+8

不起作用

解决

在时间实体类上加上这句注解
@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”,timezone = “GMT+8”)

Java

链式编程

方法 return this 即可实现


文章作者: 周master
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 周master !
  目录