索引库数据管理
秒杀商品数量庞大,我们要想实现快速检索,不建议直接使用关系型数据库查找。不建议使用Redis缓存所有数据,因为秒杀商品量大,会影响Redis的性能,并且Redis的条件检索能力偏弱。我们可以使用Elasticsearch,它在海量数据存储与检索上,能力卓越,市场使用面广。
查询MySQL数据
我们需要将秒杀商品数据导入到ES索引库中,但秒杀商品数量庞大,所以我们应该分页查询并导入,流程如下:
1)service总数量查询
我们先在seckill-goods中编写相关方法实现数据查询,因为要用到分页,所以先查询总数量,然后再实现分页集合查询。
在seckill-goods的com.seckill.goods.service.SkuService中添加count方法,用于查询秒杀商品总数量:
java
/**
* 总数量加载
* @return
*/
Integer count();
在seckill-goods的com.seckill.goods.service.impl.SkuServiceImpl中添加count方法,用于实现查询秒杀商品总数量:
java
/**
* 总数量加载
* @return
*/
@Override
public Integer count() {
Example example = new Example(Sku.class);
Example.Criteria criteria = example.createCriteria();
//秒杀剩余商品数量>0
criteria.andGreaterThan("seckillNum",0);
//状态为参与秒杀,1:普通商品,2:参与秒杀
criteria.andEqualTo("status","2");
//秒杀结束时间>=当前时间
criteria.andGreaterThanOrEqualTo("seckillEnd",new Date());
return skuMapper.selectCountByExample(example);
}
在seckill-goods的com.seckill.goods.controller.SkuController中添加count方法,用于实现查询秒杀商品总数量:
java
/***
* Sku数量加载
* @return
*/
@PostMapping(value = "/count" )
public Integer count(){
return skuService.count();
}
2)service分页集合数据查询
在seckill-goods的com.seckill.goods.service.SkuService中添加list方法,用于查询秒杀商品:
java
/***
* 分页加载
* @param page
* @param size
* @return
*/
List<Sku> list(int page, int size);
在seckill-goods的com.seckill.goods.service.impl.SkuServiceImpl中添加list方法,用于实现查询秒杀商品:
java
/***
* 分页加载
* @param page
* @param size
* @return
*/
@Override
public List<Sku> list(int page, int size) {
//分页
PageHelper.startPage(page,size);
//条件构建
Example example = new Example(Sku.class);
Example.Criteria criteria = example.createCriteria();
//秒杀剩余商品数量>0
criteria.andGreaterThan("seckillNum",0);
//状态为参与秒杀,1:普通商品,2:参与秒杀
criteria.andEqualTo("status","2");
//秒杀结束时间>=当前时间
criteria.andGreaterThanOrEqualTo("seckillEnd",new Date());
return skuMapper.selectByExample(example);
}
在seckill-goods的com.seckill.goods.controller.SkuController中添加list方法,用于实现查询秒杀商品:
java
/***
* Sku分页条件加载
* @param page
* @param size
* @return
*/
@GetMapping(value = "/list/{page}/{size}" )
public List<Sku> list(@PathVariable int page, @PathVariable int size){
//调用SkuService实现分页条件查询Sku
List<Sku> skus = skuService.list(page, size);
return skus;
}
3)Feign接口编写
在seckill-goods-api的com.seckill.goods.feign.SkuFeign中编写feign方法,分别调用刚才的count、list方法,代码如下:
java
/***
* Sku数量加载
* @return
*/
@PostMapping(value = "/sku/count" )
Integer count();
/***
* Sku分页条件加载
* @param page
* @param size
* @return
*/
@GetMapping(value = "/sku/list/{page}/{size}" )
List list(@PathVariable(value ="page") Integer page, @PathVariable(value = "size")Integer size);
集成SpringData Elasticsearch实现索引导入流程:
java
1.配置Elasticsearch地址信息
2.编写Dao代码,继承ElasticsearchRepository<T,ID>
3.在Service中分页调用查询秒杀商品集合
4.分页导入秒杀商品集合数据到Elasticsearch中
添加依赖:
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
当前项目已经集成好了SpringDataElasticsearch,我们只需要实现相关的操作过程即可。
bootstrap.yml添加es配置:
java
spring:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: es-server:9300
将数据添加到索引库,需要先创建一个能体现索引库的JavaBean映射对象,将要保存到索引库的数据赋值给JavaBean,利用API将JavaBean保存到索引库。
我们首先编写一个和索引库中一一对应的实体Bean,代码如下:
java
@Document(indexName = "goodsindex",type = "skuinfo")
public class SkuInfo implements Serializable {
//Sku相关的数据
//商品id,同时也是商品编号
@Id //唯一标识符,ES中对应的_id
private String id;
/***
* SKU名称
* type =FieldType.Text:指定当前name属性所对应的域的类型为Text类型,该类型支持分词支持创建索引
* FiledType.Keyword:不分词
* searchAnalyzer="ik_smart":搜索所使用的分词器
* analyzer = "ik_smart":添加索引所使用的分词器
*/
@Field(type =FieldType.Text ,searchAnalyzer = "ik_smart",analyzer = "ik_smart",store =false)
private String name;
//商品价格,单位为:元
private Long price;
//秒杀价
private Long seckillPrice;
//商品图片
private String image;
//更新时间
private Date updateTime;
//类目ID
private String category1Id;
//类目ID
private String category2Id;
//类目ID
private String category3Id;
//类目名称
@Field(type = FieldType.Keyword)
private String category1Name;
//类目名称
@Field(type = FieldType.Keyword)
private String category2Name;
//类目名称
@Field(type = FieldType.Keyword)
private String category3Name;
//品牌名称
@Field(type = FieldType.Keyword)
private String brandName;
//开始时间,用于做搜索
@Field(type = FieldType.Keyword)
private String bgtime;
//品牌ID
private String brandId;
private Date seckillBegin;//秒杀开始时间
private Date seckillEnd;//秒杀结束时间
private Integer status; //秒杀状态,1普通商品,2秒杀
//规格
private String spec;
//..get..set略
}
创建dao
java
public interface SkuInfoMapper extends ElasticsearchRepository<SkuInfo,String> {
}
在seckill-search的com.seckill.search.service.SkuInfoService中编写导入索引,代码如下:
java
/***
* 导入所有
*/
void addAll();
在seckill-search的com.seckill.search.service.impl.SkuInfoServiceImpl中编写导入索引,代码如下:
java
@Autowired
private SkuFeign skuFeign;
/***
* 使用Feign导入所有
*/
@Override
public void addAll() {
//从第1页开始,每页处理500条
int pageNum=1;
int size=500;
//查询所有数量
Integer count = skuFeign.count();
//总页数
int totalPage = count%size==0? count/size : (count/size)+1;
//分页查询,并将所有数据导入到索引库中
for (int i = 0; i <totalPage ; i++) {
List list = skuFeign.list(pageNum, size);
//将数据转换成List<SkuInfo>
List<SkuInfo> skuInfos =JSON.parseArray( JSON.toJSONString(list) ,SkuInfo.class);
//规格处理
for (SkuInfo skuInfo : skuInfos) {
//获取秒杀时间
Date seckillBegin = skuInfo.getSeckillBegin();
if(seckillBegin!=null){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyymmddhhmmss");
String bgtime = simpleDateFormat.format(seckillBegin);
skuInfo.setBgtime(bgtime);
}
}
skuInfoMapper.saveAll(skuInfos);
//页数递增
pageNum++;
}
}
注意:我们可以发现下面这段代码以后在其他地方有可能也会用得着,我们可以把它单独抽取出一个方法来:
抽取后:
在seckill-search的com.seckill.search.controller.SearchController中编写导入索引,代码如下:
java
/***
* 所有记录导入到搜索引擎中
*/
@GetMapping(value = "/all/add")
public Result addAll(){
//添加数据到索引库中
skuInfoService.addAll();
return new Result(true, StatusCode.OK,"导入所有数据到索引库成功!");
}
4)测试
启动seckill-goods、seckill-search、seckill-gateway,访问刚才编写的批量导入的方法,访问地址:http://localhost:8001/api/search/all/add
增量导入
增量导入,也就是某个商品设置成秒杀商品的时候,或者发生变更的时候,能实现增量备份(只将修改的数据同步修改索引库),所以我们还需要实现单个商品导入索引库,我们可以在变更方法(增删改)中调用这边同步方法,但随着系统的增加,这种方法容易有漏网之鱼,我们可以采用canal实现数据库增量监听,然后调用seckill-search的单个操作方法。
1)索引操作方法编写
在seckill-search的com.seckill.search.service.SkuInfoService中添加modify方法,代码如下:
java
/****
* 单条索引操作
* @param type: 1:增加,2:修改,3:删除
* @param skuInfo
*/
void modify(Integer type, SkuInfo skuInfo);
在seckill-search的com.seckill.search.service.impl.SkuInfoServiceImpl中添加modify实现方法,代码如下:
java
/***
* 单条索引操作
* @param type
* @param skuInfo
*/
@Override
public void modify(Integer type, SkuInfo skuInfo) {
if(type==1 || type==2){
//时间转换
skuInfoConverter(skuInfo);
//增加数据到索引库
skuInfoMapper.save(skuInfo);
}else{
skuInfoMapper.deleteById(skuInfo.getId());
}
}
在seckill-search的com.seckill.search.controller.SkuInfoController中添加modify方法,代码如下:
java
/***
* 将一条记录导入到搜索引擎中
*/
@PostMapping(value = "/modify/{type}")
public Result add(@PathVariable(value = "type")Integer type, @RequestBody SkuInfo skuInfo){
//添加数据到索引库中
skuInfoService.modify(type,skuInfo);
return new Result(true, StatusCode.OK,"操作一条数据到索引库成功!");
}
2)Feign接口编写
在seckill-search-api中编写Feign接口,实现调用modify方法,代码如下:
java
@FeignClient(value = "seckill-search")
public interface SkuInfoFeign {
/***
* 将一条记录导入到搜索引擎中
*/
@PostMapping(value = "/search/modify/{type}")
Result modify(@PathVariable(value = "type")Integer type, @RequestBody SkuInfo skuInfo);
}
商品搜索
根据秒杀页面的需求,多数是查询指定秒杀时段下的秒杀商品,同时还会有分页,当然,如果有复杂的查询,我们Elasticsearch也都满足。我们可以根据多数秒杀需求,实现按照秒杀时段分页查询数据。
编写Dao
java
public interface SkuInfoMapper extends ElasticsearchRepository<SkuInfo,String> {
/***
* 根据bgtime分页查询
* @param time
* @param pageable
* @return
*/
Page<SkuInfo> findByBgtime(String time, Pageable pageable);
}
编写Servicecom.seckill.search.service.SkuInfoService创建搜索方法,代码如下:
java
/***
* 搜索
*/
Page<SkuInfo> searchPage(Map<String,String> searchMap);
编写Servicecom.seckill.search.service.impl.SkuInfoServiceImpl创建搜索实现方法,代码如下:
java
/***
* 搜索实现
* @param searchMap
* @return
*/
@Override
public Page<SkuInfo> searchPage(Map<String, String> searchMap) {
//根据开始时间查询 findByBgtime(Dao)
return skuInfoMapper.findByBgtime(searchMap.get("starttime"), PageRequest.of(pageNumber(searchMap)-1,20));
}
/***
* 获取当期页
* @param searchMap
* @return
*/
public int pageNumber(Map<String, String> searchMap){
try {
return Integer.parseInt( searchMap.get("pageNum") );
} catch (NumberFormatException e) {
return 1;
}
}
编写Servicecom.seckill.search.controller.SearchController创建搜索方法调用,代码如下:
java
/***
* 秒杀分页查询
* pageNum:当前页
* starttime:秒杀活动开始时间
*/
@GetMapping
public Page search(@RequestParam(required = false) Map<String,String> searchMap){
if(searchMap==null){
return null;
}
return skuInfoService.searchPage(searchMap);
}
Canal增量数据同步利器
Canal介绍
canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,应用场景十分丰富。
github地址:https://github.com/alibaba/canal
版本下载地址:https://github.com/alibaba/canal/releases
文档地址:https://github.com/alibaba/canal/wiki/Docker-QuickStart
Canal应用场景
1.电商场景下商品、用户实时更新同步到至Elasticsearch、solr等搜索引擎;
2.价格、库存发生变更实时同步到redis;
3.数据库异地备份、数据同步;
4.代替使用轮询数据库方式来监控数据库变更,有效改善轮询耗费数据库资源。
MySQL主从复制原理
1.MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
2.MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
3.MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
Canal工作原理
1.canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
2.MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
3.canal 解析 binary log 对象(原始为 byte 流)
Canal安装
参考文档:https://github.com/alibaba/canal/wiki/QuickStart
MySQL Bin-log开启
1)MySQL开启bin-log
a.进入mysql容器
bash
docker exec -it -u root mysql /bin/bash
b.开启mysql的binlog
bash
cd /etc/mysql/mysql.conf.d
在mysqld.cnf最下面添加如下配置
# 开启 binlog
log-bin=/var/lib/mysql/mysql-bin
# 选择 ROW 模式
binlog-format=ROW
# 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
server-id=12345
c.创建账号并授权
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant:
bash
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
d.重启mysql
bash
docker restart mysql
开启bin-log后,我们可以用sql语句查看下:
bash
show variables like '%log_bin%'
效果如下:
Canal安装
1)拉取镜像
bash
docker pull canal/canal-server:v1.1.1
2)安装容器
a.安装canal-server容器
bash
docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server
b.配置canal-server
修改/home/admin/canal-server/conf/canal.properties,将它的id属性修改成和mysql数据库中server-id不同的值,如下图:
c.修改/home/admin/canal-server/conf/example/instance.properties,配置要监听的数据库服务地址和监听数据变化的数据库以及表,修改如下:
指定监听数据库表的配置如下canal.instance.filter.regex:
bash
mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\)
常见例子:
1. 所有表:.* or .*\\..*
2. canal schema下所有表: canal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)
重启canal:
bash
docker restart canal
Canal微服务
我们搭建一个微服务,用于读取canal监听到的变更日志,微服务名字叫seckill-canal。该项目我们需要引入canal-spring-boot-autoconfigure包,并且需要实现EntryHandler接口,该接口中有3个方法,分别为insert、update、delete,这三个方法用于监听数据增删改变化。
参考地址:https://github.com/NormanGyllenhaal/canal-client
1)pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seckill-service</artifactId>
<groupId>com.seckill</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seckill-canal</artifactId>
<dependencies>
<!--web-->
<dependency>
<groupId>com.seckill</groupId>
<artifactId>seckill-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--esAPI-->
<dependency>
<groupId>com.seckill</groupId>
<artifactId>seckill-search-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--goodsAPI-->
<dependency>
<groupId>com.seckill</groupId>
<artifactId>seckill-goods-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--canal-->
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-autoconfigure</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 指定该Main Class为全局的唯一入口 -->
<mainClass>com.seckill.CanalApplication</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal><!--可以把依赖的包都打包到生成的Jar包中-->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
bootstrap.yml配置
yaml
server:
port: 18088
spring:
application:
name: seckill-canal
cloud:
nacos:
config:
file-extension: yaml
server-addr: nacos-server:8848
discovery:
#Nacos的注册地址
server-addr: nacos-server:8848
#超时配置
ribbon:
ReadTimeout: 3000000
#Canal配置
canal:
server: canal-server:11111
destination: example
#日志
logging:
level:
root: error
2)创建com.seckill.handler.SkuHandler实现EntryHandler接口,代码如下:
java
@Component
@CanalTable(value = "tb_sku")
public class SkuHandler implements EntryHandler<Sku> {
/***
* 增加数据
* @param sku
*/
@Override
public void insert(Sku sku) {
System.out.println("===========insert:"+sku);
}
/***
* 修改数据
* @param before
* @param after
*/
@Override
public void update(Sku before, Sku after) {
System.out.println("===========update-before:"+before);
System.out.println("===========update-after:"+after);
}
/***
* 删除数据
* @param sku
*/
@Override
public void delete(Sku sku) {
System.out.println("===========delete:"+sku);
}
}
3)创建启动类
java
@SpringBootApplication
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
程序启动后,修改tb_sku数据,可以看到控制会打印修改前后的数据:
索引库同步
当tb_sku秒杀商品发生变化时,我们应该同时变更索引库中的索引数据,比如秒杀商品增加,则需要同步增加秒杀商品的索引,如果有秒杀商品删除,则需要同步移除秒杀商品。
修改seckill-canal中的com.seckill.handler.SkuHandler的增删改方法,代码如下:
java
@Component
@CanalTable(value = "tb_sku")
public class SkuHandler implements EntryHandler<Sku> {
@Autowired
private SkuInfoFeign skuInfoFeign;
/***
* 增加数据
* @param sku
*/
@Override
public void insert(Sku sku) {
//将Sku转换成SkuInfo
SkuInfo skuInfo = JSON.parseObject( JSON.toJSONString(sku) ,SkuInfo.class);
//同步索引
skuInfoFeign.modify(1,skuInfo);
}
/***
* 修改数据
* @param before
* @param after
*/
@Override
public void update(Sku before, Sku after) {
int type=2;
//将Sku转换成SkuInfo
SkuInfo skuInfo = JSON.parseObject( JSON.toJSONString(after) ,SkuInfo.class);
if(skuInfo.getStatus()==1 || after.getSeckillNum()<=0){
//商品变成了普通商品,或者商品库存为0,则需要删除索引数据
type=3;
}
//同步索引
skuInfoFeign.modify(type,skuInfo);
}
/***
* 删除数据
* @param sku
*/
@Override
public void delete(Sku sku) {
//将Sku转换成SkuInfo
SkuInfo skuInfo = JSON.parseObject( JSON.toJSONString(sku) ,SkuInfo.class);
//同步索引
skuInfoFeign.modify(3,skuInfo);
}
}
开启Feign功能:@EnableFeignClients(basePackages = {"com.seckill.search.feign"})
此时对数据库中tb_sku表进行增删改的时候,会同步到索引库中。