Loading

短链微服务

ShardingJDBC章节

ShardingJDBC官网

ShardingJDBC学习笔记

分库分表暴露的问题-ID冲突和分布式id生成

  • 单库下一般使用Mysql自增ID, 但是分库分表后,会造成不同分片上的数据表主键会重复。

  • 需求

    • 性能强劲
    • 全局唯一
    • 防止恶意用户根据id的规则来获取数据
  • 业界常用ID解决方案

    • 数据库自增ID

      • 利用自增id, 设置不同的自增步长,auto_increment_offset、auto-increment-increment
      DB1: 单数
      //从1开始、每次加2
      
      DB2: 偶数
      //从2开始,每次加2
      
      
      • 缺点
        • 依靠数据库系统的功能实现,但是未来扩容麻烦
        • 主从切换时的不一致可能会导致重复发号
        • 性能瓶颈存在单台sql上
    • UUID

      • 性能非常高,没有网络消耗
      • 缺点
        • 无序的字符串,不具备趋势自增特性
        • UUID太长,不易于存储,浪费存储空间,很多场景不适用
    • Redis发号器

      • 利用Redis的INCR和INCRBY来实现,原子操作,线程安全,性能比Mysql强劲
      • 缺点
        • 需要占用网络资源,增加系统复杂度
    • Snowflake雪花算法

      • twitter 开源的分布式 ID 生成算法,代码实现简单、不占用宽带、数据迁移不受影响
      • 生成的 id 中包含有时间戳,所以生成的 id 按照时间递增
      • 部署了多台服务器,需要保证系统时间一样,机器编号不一样
      • 缺点
        • 依赖系统时钟(多台服务器时间一定要一样)

雪花算介绍

  • 什么是雪花算法Snowflake

    • twitter用scala语言编写的高效生成唯一ID的算法

    • 优点

      • 生成的ID不重复
      • 算法性能高
      • 基于时间戳,基本保证有序递增
  • 计算机的基础知识回顾

    • bit与byte

      • bit(位):电脑中存储的最小单位,可以存储二进制中的0或1
      • byte(字节):一个byte由8个bit组成
    • 常规64位系统里面java数据类型存储字节大小

      • int:4 个字节
      • short:2 个字节
      • long:8 个字节
      • byte:1 个字节
      • float:4 个字节
      • double:8 个字节
      • char:2 个字节
    • 科普:数据类型在不同位数机器的平台下长度不同(怼面试官的严谨性)

      • 16位平台 int 2个字节16位
      • 32位平台 int 4个字节32位
      • 64位平台 int 4个字节32位
  • 雪花算法生成的数字,long类,所以就是8个byte,64bit

    • 表示的值 -9223372036854775808(-2的63次方) ~ 9223372036854775807(2的63次方-1)
    • 生成的唯一值用于数据库主键,不能是负数,所以值为0~9223372036854775807(2的63次方-1)

image-20230213204141282

分布式ID生成器Snowflake避坑

  • 分布式ID生成器需求

    • 性能强劲
    • 全局唯一不能重复
    • 防止恶意用户根据id的规则来获取数据
  • 全局唯一不能重复-坑

    • 坑一

      • 分布式部署就需要分配不同的workId, 如果workId相同,可能会导致生成的id相同
    • 坑二:

      • 分布式情况下,需要保证各个系统时间一致,如果服务器的时钟回拨,就会导致生成的 id 重复
        • 啥时候会有系统回拨????
          • 人工去生产环境做了系统时间调整
          • 业务需求,代码里面做了系统时间同步

--------表ID和workerID生成方案--------

分布式ID生成方案

  • 方式一

    • 订单id使用MybatisPlus的配置,TrafficDO类配置

      @TableId(value = "id", type = IdType.ASSIGN_ID)
      默认实现类为DefaultIdentifierGenerator雪花算法
      
  • 方式二

    • 使用Sharding-Jdbc配置文件,注释DO类里面的id分配策略
    #id生成策略
              key-generator:
                column: id
                props:
                  worker:
                    id: 0
                #id生成策略
                type: SNOWFLAKE
    

分布式workerID生成方案

进阶:动态指定sharding jdbc 的雪花算法中的属性work.id属性

  • 使用sharding-jdbc中的使用IP后几位来做workId, 但在某些情况下会出现生成重复ID的情况
    • 解决办法时
      • 在启动时给每个服务分配不同的workId, 引入redis/zk都行,缺点就是多了依赖
      • 启动程序的时候,通过JVM参数去控制,覆盖变量
public class SnowFlakeWordIdConfig {

    /**
     * 动态指定sharding jdbc 的雪花算法中的属性work.id属性
     * 通过调用System.setProperty()的方式实现,可用容器的 id 或者机器标识位
     * workId最大值 1L << 100,就是1024,即 0<= workId < 1024
     * {@link SnowflakeShardingKeyGenerator#getWorkerId()}
     *
     */
    static {

        try {
            InetAddress inetAddress = Inet4Address.getLocalHost();

            String hostAddressIp = inetAddress.getHostAddress();

            String workId = Math.abs(hostAddressIp.hashCode()) % 1024+"";

            System.setProperty("workId",workId);

            log.info("workId:{}",workId);

        } catch (UnknownHostException e) {
            e.printStackTrace();
        }

    }


}
#id生成策略
spring.shardingsphere.sharding.tables.traffic.key-generator.props.worker.id=${workId}

短链应用场景

  • 业务背景:为啥需要短链
    • 公司电商产品推广、业务活动页、广告落地页 缺少实时【数据反馈和渠道效果分析】
    • 老项目业务推广【没人维护,无法做埋点】需要统计效果
    • APP和营销活动发送营销短信链接过长,【浪费短信发送费用】
    • 国内【反垄断后】微信、抖音、淘宝 流量互通,很多知识付费公司需要做 私域流量、社群运营
    • 可以对外做产品输出,实现商业化能力增加公司营收
    • 积累终端数据和人群数据,为公司未来产品人群做策略助力
  • 短链组成

    • 协议://短链域名/短链码
  • 最简单的方式

    • 一个短链编码,去数据库select * from table where code =XXX,返回给用户就行

微服务功能

生命周期

image-20230115133810171

接口

创建者(UI图大体类似哈)

  • 流量包管理
    • 免费流量包
    • 付费流量包

image-20230115133844487

分组管理

  • 新增分组
  • 删除分组
  • 修改分组
  • 查看分组下的短链

分组作用,例如:同一个链接需要生成多端的短链,抖音、快手、B站三个平台作为分组,后期埋点数据可以用来分析引流收益

image-20230115133856480

短链管理

  • 创建短链

    • 目标地址
    • 短链标题
    • 短链域名
    • 所属分组
    • 有效期

image-20230115133917672

  • 删除短链

  • 修改短链

  • 查看短链

    • 访问PV、UV
    • 地域分布
    • 时间分布
    • 来源分布

访问者

  • 访问短链
  • 跳转目标站点

短链服务问题解决方案

长链的关系和短链的关系是一对一还是一对多?

  • 一个长链,在不同情况下,生成的短网址应该不一样,才不会造成冲突
  • 多渠道推广下,也可以区分统计不同渠道的效果质量
  • 所以是 一个短链接只能对应一个长链接,当然一个长链接可以对应多个短链接

前端访问短链是如何跳转到对应的页面的?

  • 服务端转发

    • 由服务器端进行的页面跳转,刚学Servlet时, 从OneServlet中转发到TwoServlet
    • 地址栏不发生变化,显示的是上一个页面的地址
    • 请求次数:只有1次请求
    • 转发只能在同一个应用的组件之间进行,不可以转发给其他应用的地址
     request.getRequestDispatcher("/two").forward(request, response); 
    

    image-20211203113753524

  • 页面的跳转-重定向

    • 由浏览器端进行的页面跳转

    image-20211203114328117

    • 重定向涉及到3xx状态码,访问跳转是301还是302,301和302代表啥意思?
      • 301 是永久重定向
        • 会被浏览器硬缓存,第一次会经过短链服务,后续再访问直接从浏览器缓存中获取目标地址
      • 302 是临时重定向
        • 不会被浏览器硬缓存,每次都是会访问短链服务
      • 短地址一经生成就不会变化,所以用 301 是同时对服务器压力也会有一定减少
      • 但是如果使用了 301,无法统计到短地址被点击的次数
      • 所以选择302虽然会增加服务器压力,但是有很多数据可以获取进行分析
      • 选择使用302,这个也可以对违规推广的链接进行实时封禁

短链码如何是如何生成的

  • 短链码特点
    • 生成性能强劲
    • 碰撞概率低
      • 避免重复
      • 恶意猜测
    • 业务规则安全

image-20211203145018603

  • 方式

    • 自增ID

      • 利用插入数据库,利用数据库自增id
      • 把自增id转成62进制作为短链码
      • 短链码的长度不固定,随着 id 变大,短链码长度也增长
      • 可以指定从某个长度开始增长,到百亿、千亿数量
      • 转换工具:https://tool.lu/hexconvert/
      • 是否存在重复: 不重复
      • 但短链码是有序的递增,存在【业务数据安全】问题
    • MD5内容压缩

      • 长链接做md5加密
      43E08496,9E5CF455,E6D2D2B3,3407A6D2
      
      • 加密串查询是否已经生成过短链接
        • 如果已经存在,则拼接时间戳再MD5加密,插入数据库
        • 如果不存在则把长链接、长链接加密串插入数据库
      • 取MD5后 最后1 个 8 位字符串作为短链码
      • 是否存在重复: 存在碰撞(重复)可能
      • 是有损压缩算法,数据量超大情况碰撞概念越大
        • 比如 【小滴课堂-老王的女友】有300多个,每再多1个,再同一天生日的概率越大,就更加复杂

数据脱敏解决方案

  • 什么是数据脱敏
    • 也叫数据的去隐私化,在给定脱敏规则和策略的情况下,对敏感数据比如 手机号身份证 等信息,进行转换或者修改的一种技术手段,防止敏感数据直接在不可靠的环境下使用和泄露、撞库等

    • 技术分两类

      • 静态数据脱敏
        • 将生产数据导出,进行对外发送或者给开发、测试人员等
      • 动态数据脱敏
        • 程序直接连接生产数据的场景,如运维人员在运维的工作中直接连接生产数据库进行运维
        • 客服人员在生产中通过后台查询的个人信息

哈希算法:将一个元素映射成另一个元素

  • 加密哈希,如SHA256、MD5(上集讲了)

  • 非加密哈希,如MurMurHash,CRC32

MurMurHash

Murmur哈希是一种非加密散列函数,适用于一般的基于散列的查找。
它在2008年由Austin Appleby创建,在Github上托管,名为“SMHasher” 的测试套件。 
它也存在许多变种,所有这些变种都已经被公开。 
该名称来自两个基本操作,乘法(MU)和旋转(R)--来自百科
  • 是一种【非加密型】哈希函数且【随机分布】特征表现更良好

  • 由于是非加密的哈希函数,性能会比MD5强

  • 再很多地方都用到比如Guava、Jedis、HBase、Lucence等

  • 存在两个版本

    • MurmurHash2(产生32位或64位值)
    • MurmurHash3(产生32位或128位值)
  • 数据量

    • MurmurHash的 32 bit 能表示的最大值近 43 亿的10进制

      • 满足多数业务,如果接近43亿则冲突概率大
    • 产品目标【超理想情况】

    首年日活用户: 10万
    
    首年日新增短链数据:10万*50 = 500万
    
    年新增短链数:500万 * 365天 = 18.2亿 
    
    年新增用户数:50万/1年
    
    年营收目标: 10万付费用户 * 客单价200元 = 2千万
    
    新增短链:50条/用户每日
    
    • MurMurHash得到的数值是10进制,一般会转化为62进制进行缩短

    • 常规短链码是6~8位数字+大小写字母组合

    0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
    
    6 位 62 进制数可表示 568 亿个短链(62的6次方,每位都有62个可能,如果扩大位数到7位,则可以支持3万5200亿)
    
    • MurmurHash的 32 bit 满足多数业务 43亿
      • 拼接上库-表位则可以表示更多数据(后续会讲分库分表的,库表位)
      • 7位则可以到到 43亿 * 62 = 2666亿
      • 8位则可以到到 2666亿 * 62 = 1.65万亿条数据
      • 结合短链过期数据归档,理论上满足未来全部需求了
    • 数据库存储
      • 单表1千万 * 62个库 * 62表 = 384亿数据

MurMurHash生成短链代码

    /**
     * 62个字符
     */
    private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";


    /**
     * 生成短链码
     *
     * @param param
     * @return
     */
    public String createShortLinkCode(String param) {

        long murmurhash = CommonUtil.murmurHash32(param);
        //进制转换
        String code = encodeToBase62(murmurhash);

        return code;
    }

    /**
     * 10进制转62进制
     *
     * @param num
     * @return
     */
    private String encodeToBase62(long num) {

        // StringBuffer线程安全,StringBuilder线程不安全
        StringBuffer sb = new StringBuffer();
        do {
            int i = (int) (num % 62);
            sb.append(CHARS.charAt(i));
            num = num / 62;
        } while (num > 0);

        String value = sb.reverse().toString();
        return value;

    }

CommonUtil工具类

  /**
     * murmur hash算法
     * @param param
     * @return
     */
    public static long murmurHash32(String param){
        long murmur32 = Hashing.murmur3_32().hashUnencodedChars(param).padToLong();
        return murmur32;
    }

--------短链码设计思路--------

短链码

murmurHash32得出来的数 N 是62的6次方,N % 62 6次结果皆为1-62的数字,index取值得到6个值依次进行拼接,到这里是短链码的生成

 private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

分库分表

后续在短链码前后随机添加库码和表码,以便路由

    public String createShortLinkCode(String param) {

        long murmurhash = CommonUtil.murmurHash32(param);
        //进制转换
        String code = encodeToBase62(murmurhash);

        String shortLinkCode = ShardingDBConfig.getRandomDBPrefix() + code + ShardingTableConfig.getRandomTableSuffix();

        return shortLinkCode;
    }
public class ShardingDBConfig {

    /**
     * 存储数据库位置编号
     */
    private static final List<String> dbPrefixList = new ArrayList<>();

    private static Random random = new Random();

    //配置启用那些库的前缀
    static {
        dbPrefixList.add("0");
        dbPrefixList.add("1");
        dbPrefixList.add("a");
    }


    /**
     * 获取随机的前缀
     *
     * @return
     */
    public static String getRandomDBPrefix() {
        int index = random.nextInt(dbPrefixList.size());
        return dbPrefixList.get(index);
    }

}
public class ShardingTableConfig {

    /**
     * 存储数据表位置编号
     */
    private static final List<String> tableSuffixList = new ArrayList<>();

    private static Random random = new Random();

    //配置启用那些表的后缀
    static {
        tableSuffixList.add("0");
        tableSuffixList.add("a");
    }


    /**
     * 获取随机的后缀
     *
     * @return
     */
    public static String getRandomTableSuffix() {
        int index = random.nextInt(tableSuffixList.size());
        return tableSuffixList.get(index);
    }
}

分库分表配置

分组管理-水平分库分表配置

  • 需求

    • 未来2年,短链平台累计5百万用户
      • 短链组:一个用户30个组,就是1.5亿个组
      • 单表不超过1千万数据,需要分15张表
      • 进一步延伸,进行水平分库分表,比如 2库、4库、8库、16库
      • 一个库一张表
    • 需要降低单表数据量,进行水平分库分表
    • 分库数量:线上分16库,本地分2库即可
    • 分片key: account_no,查询维度都是根据account_no进行查询
    • 分片策略:行表达式分片策略 InlineShardingStrategy
  • 配置

#----------短链组,策略:水平分库,不水平分表--------------
# 先进行水平分库, 水平分库策略,行表达式分片
spring.shardingsphere.sharding.tables.link_group.database-strategy.inline.sharding-column=account_no
spring.shardingsphere.sharding.tables.link_group.database-strategy.inline.algorithm-expression=ds$->{account_no % 2}

分库分表免迁移扩容方案-库码编码

  • 需要解决的问题

    • 数据量增加,扩容避免迁移数据或者免迁移
    • 前期数据量不多,不浪费库表系统资源
      • 分库分表:16个库, 每个库64个表,总量就是 1024个表
  • 短链码

    • 比如 g1.fit/92AEva 的短链码 92AEva
  • 如何做?

    • 从短链码入手-增加库表位
    • 类似案例-阿里这边商品订单号-里面也包括了库表信息(规则不能说)

image-20211210155218497

image-20211211101342722
  • 为啥能做到免迁移扩容?
    • A92AEva1
    • 由于短链码的前缀和后缀是是固定的,所以扩容也不影响旧的数据
    • 类似的免迁移扩容策略还有哪些?
      • 时间范围分库分表
      • id范围分库分表

分片算法

精准算法分库

public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

    /**
     * @param availableTargetNames 数据源集合
     *                             在分库时值为所有分片库的集合 databaseNames
     *                             分表时为对应分片库中所有分片表的集合 tablesNames
     * @param shardingValue        分片属性,包括
     *                             logicTableName 为逻辑表,
     *                             columnName 分片健(字段),
     *                             value 为从 SQL 中解析出的分片健的值
     * @return
     */

    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {

        //获取短链码第一位,即库位
        String codePrefix = shardingValue.getValue().substring(0, 1);

        for (String targetName : availableTargetNames) {
            //获取库名的最后一位,真实配置的ds
            String targetNameSuffix = targetName.substring(targetName.length() - 1);

            //如果一致则返回
            if (codePrefix.equals(targetNameSuffix)) {
                return targetName;
            }
        }

        //抛异常
        throw new BizException(BizCodeEnum.DB_ROUTE_NOT_FOUND);

    }
}

精准算法分表

public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

    /**
     * @param availableTargetNames 数据源集合
     *                             在分库时值为所有分片库的集合 databaseNames
     *                             分表时为对应分片库中所有分片表的集合 tablesNames
     * @param shardingValue        分片属性,包括
     *                             logicTableName 为逻辑表,
     *                             columnName 分片健(字段),
     *                             value 为从 SQL 中解析出的分片健的值
     * @return
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {


        //获取逻辑表名
        String targetName = availableTargetNames.iterator().next();

        String value = shardingValue.getValue();
        //短链码最后一位
        String codePrefix = value.substring(value.length()-1);
        //拼装actual table
        return targetName + "_" + codePrefix;

    }
}

配置规则

#----------短链,策略:分库+分表--------------
# 先进行水平分库,然后再水平分表
spring.shardingsphere.sharding.tables.short_link.database-strategy.standard.sharding-column=code
spring.shardingsphere.sharding.tables.short_link.database-strategy.standard.precise-algorithm-class-name=net.xdclass.strategy.CustomDBPreciseShardingAlgorithm

# 水平分表策略,自定义策略。   真实库.逻辑表
spring.shardingsphere.sharding.tables.short_link.actual-data-nodes=ds0.short_link,ds1.short_link,dsa.short_link
spring.shardingsphere.sharding.tables.short_link.table-strategy.standard.sharding-column=code
spring.shardingsphere.sharding.tables.short_link.table-strategy.standard.precise-algorithm-class-name=net.xdclass.strategy.CustomTablePreciseShardingAlgorithm

冗余双写架构

问题:商家怎么看自己的全部短链

* 普通用户根据短链码可以路由到对应的库表
* 但是商家创建的短链码都是没规律,分布再不同的库表上
  • 不同维度查看数据,场景是不一样的,

    • 主要是分:有PartitionKey,没PartitionKey两个场景

      • 电商订单案例一:
        • 订单表 的partionKey是user_id,用户查看自己的订 单列表方便
        • 但商家查看自己店铺的订单列表就麻烦,分布在不同数据节点

      image-20211212152029514

      • 短链访问案例
        • 普通用户访问短链,根据短链码code可以解析到对应的库表
        • 但短链商家,查看自己全部的短链就麻烦了,分布再不同的库下面
image-20211212170650207
  • 这个是【 通用的业务场景痛点 】
    • 除了上面的电商业务、短链业务,还有更多
    • 招聘网站业务
      • 企业查看自己某个岗位的面试记录
      • 应聘者查看自己的全部面试记录
      • 痛点:
        • 根据user_id进行hash分库分表
        • 但是企业的岗位存在不同user_id进行面试

分库分表多维度查询解决方案

方案一【字段解析配置】

  • 分库分表后的查询问题

    • 有PartitionKey,没PartitionKey两个场景
    • 不同维度查询是不一样的,怎么解决?
    • 解决方案

      • 字段解析配置

      • NOSQL冗余

      • 本身库表冗余双写方案

        • 部分字段冗余
        • 全量内容冗余
  • 解决方式一(字段解析配置):

    • 建一个表,存储account_no对应的库表位,商家生成的【短链码】固定前缀或者后缀
    • 即【短链码】里面包括了商家的信息

image-20211212174022539

方案二【NOSQL方案】

  • 分库分表后的查询问题

    • 有PartitionKey,没PartitionKey两个场景
    • 不同维度查询是不一样的,怎么解决?
    • 解决方案
      • 字段解析配置
      • NOSQL冗余
      • 本身库表冗余双写方案
        • 部分字段冗余
        • 全量内容冗余
  • 解决方式二:

    • 电商订单案例
      • 订单表 的partionKey是user_id,用户查看自己的订单列表方便
      • 但商家查看自己店铺的订单列表就麻烦,分布在不同数据节点
        • 订单冗余存储在es上一份
        • 业务架构流程

    image-20211212175450861

    • 短链平台案例
      • 短链表的partionKey是短链码,用户访问短码方便解析
      • 但商家查看自己某个分组下全部短链列表就麻烦,分布在不同数据节点
        • 短链码冗余存储在es上一份
        • 业务架构流程

image-20211212180211424

方案三【冗余双写方案】

  • 分库分表后的查询问题

    • 有PartitionKey,没PartitionKey两个场景
    • 不同维度查询是不一样的,怎么解决?
  • 解决方式三:

    • 电商场景
      • b2b平台,比如淘宝、京东,买家和卖家都要能够看到自己的订单列表
      • 无论是按照买家id切分订单表,还是按照卖家id切分订单表都没法满足要求
      • 拆分买家库和卖家库
        • 买家库,按照用户的id来分库分表
        • 卖家库,按照卖家的id来分库分表
      • 数据冗余
        • 下订单的时候写两份数据
        • 在买家库和卖家库各写一份

    image-20211213131242530

    • 短链场景

    image-20211213130611913

冗余双写分布式事务问题解决

  • 方案一
    • 直接RPC调用+Seata分布式事务框架
    • 优点:强一致性,代码逻辑简单,业务侵入性小
    • 缺点:性能下降,seata本身存在一定的性能损耗
      • Seata支持AT、TCC、Saga 三种模式
        • AT:隔离性好和低改造成本, 但性能低
        • TCC:性能和隔离性,但改造成本大
        • Saga:性能和低改造成本,但隔离性不好

image-20211213133145677

  • 方案二(选用)
    • 使用MQ, 生产者确认消息发送成功后,不同的消费者订阅消息消费
      • 同时保证消息处理的幂等性
      • 保证Broker的高可用
    • 优点
      • 实现简单,改造成本小
      • 性能高,没有全局锁
    • 缺点
      • 弱一致性,需要强一致性的场景不适用
      • 消费者消费失败,需要额外写接口回滚生产者业务逻辑

image-20211213161903711

posted @ 2023-01-15 15:03  yonugleesin  阅读(400)  评论(0编辑  收藏  举报