此博客用于个人学习,来源于网上,对知识点进行一个整理。
1. FastDFS:
分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连。
- 传统文件系统管理的文件就存储在本机
- 分布式文件系统管理的文件存储在很多机器,这些机器通过网络连接,要被统一管理。无论是上传或者访问文件,都需要通过管理中心来访问
FastDFS 是一个轻量级、高性能的开源分布式文件系统。用纯C语言开发,功能丰富:
- 文件存储
- 文件同步
- 文件访问(上传、下载)
- 存取负载均衡
- 在线扩容
适合有大容量存储需求的应用或系统。同类的分布式文件系统有谷歌的GFS、HDFS(Hadoop)、TFS(淘宝)等。
1.1 FastDFS 的架构:
FastDFS 两个主要的角色:Tracker Server 和 Storage Server 。
- Tracker Server:跟踪服务器,主要负责调度 storage 节点与 client 通信,在访问上起负载均衡的作用,和记录 storage 节点的运行状态,是连接 client 和 storage 节点的枢纽
- Storage Server:存储服务器,保存文件和文件的 meta data(元数据),每个 storage server 会启动一个单独的线程主动向 Tracker cluster 中每个 tracker server 报告其状态信息,包括磁盘使用情况,文件同步情况及文件上传下载次数统计等信息
- Group:文件组,多台 Storage Server 的集群。上传一个文件到同组内的一台机器上后,FastDFS 会将该文件即时同步到同组内的其它所有机器上,起到备份的作用。不同组的服务器,保存的数据不同,而且相互独立,不进行通信
- Tracker Cluster:跟踪服务器的集群,有一组 Tracker Server(跟踪服务器)组成
- Storage Cluster :存储集群,有多个 Group 组成
1.2 FastDFS 上传和下载流程:
上传:
- Client 通过 Tracker server 查找可用的 Storage server
- Tracker server 向 Client 返回一台可用的 Storage server 的 IP 地址和端口号
- Client 直接通过 Tracker server 返回的 IP 地址和端口与其中一台 Storage server 建立连接并进行文件上传
- 上传完成,Storage server 返回 Client 一个文件 ID,文件上传结束
下载:
- Client 通过 Tracker server 查找要下载文件所在的 Storage server
- Tracker server 向 Client 返回包含指定文件的某个 Storage server 的 IP 地址和端口号
- Client 直接通过 Tracker server 返回的 IP 地址和端口与其中一台 Storage server 建立连接并指定要下载文件
- 下载文件成功
1.3 java 客户端:
就用 FastDFS 改造 leyou-upload 工程:
1)引入依赖:
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
</dependency>
2)引入配置类:
@Configuration
@Import(FdfsClientConfig.class)
//解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
}
3)编写 FastDFS 属性:
在 application.yml 配置文件中添加如下内容:
fdfs:
so-timeout: 1501 # 超时时间
connect-timeout: 601 # 连接超时时间
thumb-image: # 缩略图
width: 60
height: 60
tracker-list: # tracker地址:你的虚拟机服务器地址+端口(默认是22122)
- 192.168.56.101:22122
4)配置 hosts:
将来通过域名:image.leyou.com 这个域名访问 fastDFS 服务器上的图片资源。所以,需要代理到虚拟机地址:
配置 hosts 文件,使 image.leyou.com 可以访问 fastDFS 服务器
192.168.56.101 image.leyou.com
5)改造上传逻辑:
@Service
public class UploadService {
private static final List<String> content_types = Arrays.asList("image/gif","image/jpeg");
private static final Logger LOGGER = LoggerFactory.getLogger(UploadService.class);
@Autowired
private FastFileStorageClient storageClient;
public String uploadImage(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
//校验文件类型
String contentType = file.getContentType();
if (!content_types.contains(contentType)){
LOGGER.info("文件类型不合法:{}",originalFilename);
return null;
}
try {
//校验文件内容
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
if (bufferedImage == null){
LOGGER.info("文件内容不合法:{}",originalFilename);
return null;
}
//保存到文件的服务器
//file.transferTo(new File("D:\\java\\images\\"+originalFilename));
String ext = StringUtils.substringAfterLast(originalFilename, ".");
StorePath storePath = this.storageClient.uploadFile(file.getInputStream(), file.getSize(), ext, null);
//返回url,进行回写
//return "http://image.leyou.com/" + originalFilename;
return "http://image.leyou.com/" + storePath.getFullPath();
} catch (IOException e) {
LOGGER.info("服务器内部错误:"+originalFilename);
e.printStackTrace();
}
return null;
}
}
2. 商品规格数据结构:
商城是一个全品类的电商网站,因此商品的种类繁多,每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU 和 SKU。
2.1 SPU 和 SKU:
SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU 商品集因具体特性不同而细分的每个商品
- 本页的 华为 Mate10 就是一个商品集(SPU)
- 因为颜色、内存等不同,而细分出不同的 Mate10,如亮黑色128G版。(SKU)
可以看出:
- SPU 是一个抽象的商品集概念,为了方便后台的管理。
- SKU 才是具体要销售的商品,每一个 SKU 的价格、库存可能会不一样,用户购买的是 SKU 而不是 SPU
2.2 数据库设计分析:
1)初步设计,发现问题:
SPU 应该考虑的字段:
id:主键
title:标题
description:描述
specification:规格
packaging_list:包装
after_service:售后服务
comment:评价
category_id:商品分类
brand_id:品牌
难点是商品的规格字段怎么进行设计
SKU 应该考虑的字段:
id:主键
spu_id:关联的spu
price:价格
images:图片
stock:库存
不同的商品分类,可能属性是不一样的,比如手机有内存,衣服有尺码,我们是全品类的电商网站,这些不同的商品的不同属性。其实颜色、内存、硬盘属性都是规格参数中的字段。所以,要解决这个问题,首先要能清楚规格参数。
2)分析规格参数:
虽然商品规格千变万化,但是同一类商品(如手机)的规格是统一的。
3)SKU 的特有属性:
SPU 中会有一些特殊属性,用来区分不同的 SKU,我们称为 SKU 特有属性。如华为 META10 的颜色、内存属性。不同种类的商品,一个手机,一个衣服,其 SKU 属性不相同。同一种类的商品,比如都是衣服,SKU 属性基本是一样的,都是颜色、尺码等。
SKU 的特有属性是商品规格参数的一部分
没必要单独对 SKU 的特有属性进行设计,它可以看做是规格参数中的一部分,这样规格参数中的属性可以标记成两部分:
- spu 下所有 sku 共享的规格属性(称为全局属性)
- 每个 sku 不同的规格属性(称为特有属性)
4)搜索属性:
打开一个搜索页,我们来看看过滤的条件,过滤条件中的屏幕尺寸、运行内存、网路、机身内存、电池容量、CPU 核数等,在规格参数中都能找到。也就是说,规格参数中的数据,将来会有一部分作为搜索条件来使用。我们可以在设计时,将这部分属性标记出来,将来做搜索的时候,作为过滤条件。要注意的是,无论是SPU的全局属性,还是SKU的特有属性,都有可能作为搜索过滤条件的,并不冲突,而是有一个交集。
1.3 规格参数表:
1)表结构:
规格参数是分组的,每一组都有多个参数键值对。不过对于规格参数的模板而言,其值现在是不确定的,不同的商品值肯定不同,模板中只要保存组信息、组内参数信息即可。
因此我们设计了两张表:
- tb_spec_group:组,与商品分类关联
- tb_spec_param:参数名,与组关联,一对多
2)规格组:
规格参数分组表:tb_spec_group
CREATE TABLE `tb_spec_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
`name` varchar(50) NOT NULL COMMENT '规格组的名称',
PRIMARY KEY (`id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';
规格组有3个字段:
- id:主键
- cid:商品分类id,一个分类下有多个模板
- name:该规格组的名称
3)规格参数:
规格参数表:tb_spec_param
CREATE TABLE `tb_spec_param` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id',
`group_id` bigint(20) NOT NULL,
`name` varchar(255) NOT NULL COMMENT '参数名',
`numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',
`unit` varchar(255) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',
`generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',
`searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',
`segments` varchar(1000) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',
PRIMARY KEY (`id`),
KEY `key_group` (`group_id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';
其中多了不少新定义的元素,是由于规格参数中有一部分是 SKU 的通用属性,一部分是 SKU 的特有属性,而且其中会有一些将来用作搜索过滤,这些信息都需要标记出来。
-
通用属性:用一个布尔类型字段来标记是否为通用:
- generic来标记是否为通用属性:
- true:代表通用属性
- false:代表sku特有属性
- generic来标记是否为通用属性:
-
搜索过滤:与搜索相关的有两个字段:
- searching:标记是否用作过滤
- true:用于过滤搜索
- false:不用于过滤
- segments:某些数值类型的参数,在搜索时需要按区间划分,这里提前确定好划分区间
- 比如电池容量,0~2000mAh,2000mAh~3000mAh,3000mAh~4000mAh
- searching:标记是否用作过滤
-
数值类型:某些规格参数可能为数值类型,这样的数据才需要划分区间,我们有两个字段来描述:
- numberic:是否为数值类型
- true:数值类型
- false:不是数值类型
- unit:参数的单位
- numberic:是否为数值类型
3. SPU 和 SKU 数据结构:
3.1 SPU 表:
SPU 表:
CREATE TABLE `tb_spu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`sub_title` varchar(255) DEFAULT '' COMMENT '子标题',
`cid1` bigint(20) NOT NULL COMMENT '1级类目id',
`cid2` bigint(20) NOT NULL COMMENT '2级类目id',
`cid3` bigint(20) NOT NULL COMMENT '3级类目id',
`brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
`saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',
`create_time` datetime DEFAULT NULL COMMENT '添加时间',
`last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象的商品,比如 iphone8';
其中少了一些字段,比如商品描述,于是做了表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail。
CREATE TABLE `tb_spu_detail` (
`spu_id` bigint(20) NOT NULL,
`description` text COMMENT '商品描述信息',
`generic_spec` varchar(10000) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
`special_spec` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
`packing_list` varchar(3000) DEFAULT '' COMMENT '包装清单',
`after_service` varchar(3000) DEFAULT '' COMMENT '售后服务',
PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。需要注意的是这两个字段:generic_spec和special_spec。
前面讲过规格参数与商品分类绑定,一个分类下的所有 SPU 具有类似的规格参数。SPU 下的 SKU 可能会有不同的规格参数信息,因此可以这样设计:
- SPUDetail 中保存通用的规格参数信息
- SKU 中保存特有规格参数
1)generic_spec 字段:
其中保存通用规格参数信息的值,这里为了方便查询,使用了 json 格式,其中都是键值对:
- key:对应的规格参数的 spec_param 的 id
- value:对应规格参数的值
2)special_spec 字段:
颜色、内存、机身存储,作为 SKU 特有属性,key 虽然一样,但是 SPU 下的每一个 SKU,其值都不一样,所以值会有很多,形成数组。
我们在 SPU 中,会把特有属性的所有值都记录下来,形成一个数组,每一个数据都是 json 结构:
- key:规格参数 id
- value:spu 属性的数组
那么问题来:特有规格参数应该在sku中记录才对,为什么在spu中也要记录一份?
而且有时候需要把所有规格参数都查询出来,而不是只查询1个 sku 的属性。比如,商品详情页展示可选的规格参数时,所以应该在 sku 中记录的特有规格参数在 spu 中也要记录一份。
3.2 SKU 表:
CREATE TABLE `tb_sku` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
`spu_id` bigint(20) NOT NULL COMMENT 'spu id',
`title` varchar(255) NOT NULL COMMENT '商品标题',
`images` varchar(1000) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
`price` bigint(15) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
`indexes` varchar(100) COMMENT '特有规格属性在spu属性模板中的对应下标组合',
`own_spec` varchar(1000) COMMENT 'sku的特有规格参数,json格式',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
`create_time` datetime NOT NULL COMMENT '添加时间',
`last_update_time` datetime NOT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`),
KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的64GB的iphone 8';
还有一张表,代表库存:
CREATE TABLE `tb_stock` (
`sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id',
`seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存',
`seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量',
`stock` int(9) NOT NULL COMMENT '库存数量',
PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';
因为库存字段写频率较高,而 SKU 的其它字段以读为主,因此我们将两张表分离,读写不会干扰。
1)indexes 字段:
在 SPU 表中,已经对特有规格参数及可选项进行了保存:
{
"4": [
"香槟金",
"樱花粉",
"磨砂黑"
],
"12": [
"2GB",
"3GB"
],
"13": [
"16GB",
"32GB"
]
}
这些特有属性如果排列组合,会产生12个不同的 SKU,而不同的 SKU,其属性就是上面备选项中的一个,当用户点击选中一个特有属性,你就能根据 角标快速定位到 sku。
2)own_spec 字段:
保存的是特有属性的键值对。
SPU 中保存的是可选项,但不确定具体的值,而 SKU 中的保存的就是具体的值。