1. 数据存储方案
1.1 分区
1.1.1 优点
(1)水平拆分,逻辑上还是一张表,业务中的sql语句不需要改变
(2)提高数据检索、统计的性能
(3)打破磁盘容量限制,不同的分区可以放到不同的磁盘
(4)根据分区清理数据效率高
1.1.2 缺点
(1)分区字段必须是索引的一部分或全部
(2)分区方式不够灵活
(3)只支持水平分区
1.2 分表
1.2.1 优点
(1)解决了字段多和数据多引起的各种问题
(2)分区方式更灵活
1.2.2 缺点
(1)增删改查时需要自己判断操作哪张表
(2)垂直分表需要处理事务问题、数据关联问题
(3)水平分表要处理聚合操作的数据合并问题
1.3 分库
1.3.1 优点
(1)避免了不同服务间数据耦合
(2)请求分分流,提高了整体的并发能力
1.3.2 缺点
(1)热点服务容易出现单点故障
(2)部分库依然存在瓶颈
(3)有分布式事务问题
1.4 集群
1.4.1 优点
(1)解决了海量数据存储的问题
(2)提高了读写并发能力,避免了单点故障
1.4.2 缺点
(1)分片后数据聚合统计比较复杂
(2)会有主从数据同步问题
(3)存在分布式事务问题
1.5 解决单表数据量大的问题有哪些方案?
首先是库内表分区或分表,可以解决大多数问题。如果单个库压力太大,再考虑分库。水平分库结合分表,实现数据分片。进一步提高数据存储规模。
1.6 数据库的读写压力较大,并发较高该怎么办?
首先考虑垂直分表,看看能不能将写频繁的数据与其他数据分离,避免相互影响。如果不行则考虑主从集群,实现读写分离。
2. 创建赛季表的代码实现
2.1 创建定时任务
java">package com.tianji.learning.handler;import com.tianji.learning.service.IPointsBoardSeasonService;
import com.tianji.learning.service.IPointsBoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;/*** @ClassName PointsBoardPersistentHandler* @Description 定义定时任务* @Author 孙克旭* @Date 2024/12/17 16:10*/
@Component
@RequiredArgsConstructor
public class PointsBoardPersistentHandler {private final IPointsBoardSeasonService seasonService;private final IPointsBoardService pointsBoardService;/*** 创建赛季积分表*/@Scheduled(cron = "0 0 3 1 * ?")public void createPointsBoardTableOfLasteSeason() {//1.获取上个月时间LocalDateTime time = LocalDateTime.now().minusMonths(1);//2.查询赛季idInteger season = seasonService.querySeasonByTime(time);//3.创建表pointsBoardService.createPointsBoardTableBySeason(season);}
}
2.2 Service
java">package com.tianji.learning.service.impl;import com.tianji.learning.domain.po.PointsBoardSeason;
import com.tianji.learning.mapper.PointsBoardSeasonMapper;
import com.tianji.learning.service.IPointsBoardSeasonService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.util.Optional;/*** <p>* 服务实现类* </p>** @author 孙克旭* @since 2024-12-15*/
@Service
public class PointsBoardSeasonServiceImpl extends ServiceImpl<PointsBoardSeasonMapper, PointsBoardSeason> implements IPointsBoardSeasonService {@Overridepublic Integer querySeasonByTime(LocalDateTime time) {Optional<PointsBoardSeason> optional = lambdaQuery().le(PointsBoardSeason::getBeginTime, time).gt(PointsBoardSeason::getEndTime, time).oneOpt();return optional.map(PointsBoardSeason::getId).orElse(null);}
}
java"> @Overridepublic void createPointsBoardTableBySeason(Integer season) {getBaseMapper().createPointsBoardTable("points_board_" + season);}
2.3 Mapper
java">package com.tianji.learning.mapper;import com.tianji.learning.domain.po.PointsBoard;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;/*** <p>* 学霸天梯榜 Mapper 接口* </p>** @author 孙克旭* @since 2024-12-15*/
public interface PointsBoardMapper extends BaseMapper<PointsBoard> {/*** 创建赛季表** @param s*/void createPointsBoardTable(@Param("tableName") String tableName);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tianji.learning.mapper.PointsBoardMapper"><insert id="createPointsBoardTable" parameterType="java.lang.String">CREATE TABLE `${tableName}`(`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '榜单id',`user_id` BIGINT NOT NULL COMMENT '学生id',`points` INT NOT NULL COMMENT '积分值',PRIMARY KEY (`id`) USING BTREE,INDEX `idx_user_id` (`user_id`) USING BTREE) COMMENT ='学霸天梯榜'COLLATE = 'utf8mb4_0900_ai_ci'ENGINE = InnoDBROW_FORMAT = DYNAMIC</insert>
</mapper>
最后需要将Redis中数据持久化到赛季表。
3. XXL-Job 定时任务
3.1 改进代码
java"> @XxlJob("createTableJob")public void createPointsBoardTableOfLasteSeason() {//1.获取上个月时间LocalDateTime time = LocalDateTime.now().minusMonths(1);//2.查询赛季idInteger season = seasonService.querySeasonByTime(time);//3.创建表pointsBoardService.createPointsBoardTableBySeason(season);}
3.2 新增执行器
3.3 设置任务
4. 历史榜单持久化
4.1 定义ThreadLocal
java">package com.tianji.learning.utils;/*** @ClassName TableInfoContext* @Description 表信息上下文* @Author 孙克旭* @Date 2024/12/17 17:33*/
public class TableInfoContext {private static final ThreadLocal<String> TL = new ThreadLocal<>();public static void setInfo(String info) {TL.set(info);}public static String getInfo() {return TL.get();}public static void remove() {TL.remove();}
}
4.2 定义Mybatis拦截器
java">package com.tianji.learning.config;import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.tianji.learning.utils.TableInfoContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;/*** @ClassName MybatisConfiguration* @Description mybatis配置类* @Author 孙克旭* @Date 2024/12/17 17:40*/
@Configuration
public class MybatisConfiguration {@Beanpublic DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor() {Map<String, TableNameHandler> map = new HashMap<>(1);map.put("points_board", (sql, tableName) -> TableInfoContext.getInfo());return new DynamicTableNameInnerInterceptor(map);}}
4.2.1 注入拦截器
java"> /*** mybatis拦截器* 并不是每个微服务都需要DynamicTableNameInnerInterceptor拦截器* 所以设置为按需注入该拦截器** @param innerInterceptor* @return*/@Bean@ConditionalOnMissingBeanpublic MybatisPlusInterceptor mybatisPlusInterceptor(@Autowired(required = false) DynamicTableNameInnerInterceptor innerInterceptor) {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(200L);interceptor.addInnerInterceptor(paginationInnerInterceptor);interceptor.addInnerInterceptor(new MyBatisAutoFillInterceptor());if (innerInterceptor != null) {interceptor.addInnerInterceptor(innerInterceptor);}return interceptor;}
4.3 定义持久化任务
java"> /*** 数据持久化任务*/@XxlJob("savePointsBoard2DB")public void savePointsBoard2DB() {//1.获取上个月时间LocalDateTime time = LocalDateTime.now().minusMonths(1);//2. 计算动态表名//2.1 查询赛季信息Integer season = seasonService.querySeasonByTime(time);//2.2 存入ThreadLocalTableInfoContext.setInfo("points_board_" + season);//3.查询榜单数据//3.1 拼接keyString key = RedisConstants.POINTS_BOARD_KEY_PREFIX + time.format(DateUtils.POINTS_BOARD_SUFFIX_FORMATTER);//3.2 查询数据int pageNo = 1;int pageSize = 1000;while (true) {List<PointsBoard> boardList = pointsBoardService.queryCurrentBoardList(key, pageNo, pageSize);if (CollUtils.isEmpty(boardList)) {break;}//4. 持久化到数据库//4.1 把排名信息写入idboardList.forEach(b -> {b.setId(b.getRank().longValue());b.setRank(null);});//4.2 持久化pointsBoardService.saveBatch(boardList);//5. 翻页pageNo++;}TableInfoContext.remove();}/*** 删除Redis数据*/@XxlJob("cleanPointsBoardFromRedis")public void cleanPointsBoardFromRedis() {//1.获取上个月时间LocalDateTime time = LocalDateTime.now().minusMonths(1);//2 拼接keyString key = RedisConstants.POINTS_BOARD_KEY_PREFIX + time.format(DateUtils.POINTS_BOARD_SUFFIX_FORMATTER);//3. 删除redisTemplate.unlink(key);}
4.4 设置任务链
首先设置三个任务:
任务链是 10->12->13
点击右边操作按钮设置子任务: