目录
业务功能模块
文件功能
对于文件的操作如文件的上传、下载功能是软件系统常见的功能。例如:电商系统中需要上传商品的图片、广告视频,办公系统中上传附件,社交类系统中上传用户头像等等
对象存储
对象存储(ObjectStorage)是一种用于存储和管理非结构化数据的存储架构。与传统文件系统(如块存储或文件存储)不同,对象存储将数据存储为独立的"对象",每个对象包含数据本身、元数据以及唯一的全局标识符(如对象ID),结合元数据和唯一的标识符可以进行数据检索。对象存储通常用于存储大量非结构化数据,如图片、视频、日志文件、备份数据等。
对象存储将所有对象一起存储在一个"数据湖"(也称之为"数据池")中。因此,对象存储可以非常快速地存储大量数据,就像打包旅行时,将衣服塞进袋子里比仔细地将衣服叠好并分类放入行李箱中要快。
对象存储是一种高效、可扩展的存储方式,适合处理大规模非结构化数据,广泛应用于云存储、备份、大数据分析等领域。
本项目的文件服务,通过阿里云OSS对象存储实现(只是学习用的话,新人可白嫖三个月)
文件服务架构

这种方式提供一个独立的文件微服务,文件微服务向业务微服务提供统一的上传、下载、查看接口,不同的业务微服务调用方式相同,并且屏蔽了底层调用OSS服务(或其他厂商所提供对象存储服务的)的接口,即使以后迁移OSS服务商,业务微服务层面的系统也不需要变动。
但是所有的下载都要走我们的带宽浏览,对于带宽的压力比较大,所以下载往往有单独的地址供下载,不占用接口服务器带宽。
下载问题解决后,上传同样会占用我们的接口带宽,所以我们采用前端直传的方式进行上传,我们只是提供一个直传专用的签名地址,这样我们既保证了安全又降低了流量。

核心设计
- 创建文件服务 fw-file
- 维护文件服务相关配置(含与服务商对接配置)
- 动态可调控文件服务相关配置
- 提供文件上传接口
- 提供前端直传签名获取接口
依赖
<!-- 阿⾥云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
添加配置文件
java
package com.hyldzbg.fwfileservice.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云OSS 配置信息,从nacos读取
*/
@Slf4j
@Data
@Configuration
@RefreshScope
//会去配置文件里找以oss开头的配置文件,然后读取里面的配置,并将配置内容和属性进行一一对应
@ConfigurationProperties(prefix = "oss")
@ConditionalOnProperty(value = "storage.type", havingValue = "oss")
public class OSSProperties {
/**
* oss是否内网上传
*/
private Boolean internal;
/**
* oss的endpoint
*/
private String endpoint;
/**
* oss的endpoint的内部地址
*/
private String intEndpoint;
/**
* 地域的代码
*/
private String region;
/**
* ak
*/
private String accessKeyId;
/**
* sk
*/
private String accessKeySecret;
/**
*存储桶
*/
private String bucketName;
/**
* 路径前缀,加在 endPoint 之后
*/
private String pathPrefix;
private Integer expre;
/**
* 限制上传文件大小
*/
private Integer minLen;
private Integer maxLen;
/**
* 获取访问URL
*
* @return url信息
*/
public String getBaseUrl() {
return "https://" + bucketName + "." + endpoint + "/";
}
/**
* 获取内部访问URL
*
* @return 内部访问URL
*/
public String getInternalBaseUrl() {
return "http://" + bucketName + "." + intEndpoint + "/";
}
}
java
package com.hyldzbg.fwfileservice.config;
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.common.comm.SignVersion;
import jakarta.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云OSS auto config
*
*/
@Configuration
//当value = havingvalue时微服务启动的时候该类就会被加载
@ConditionalOnProperty(value = "storage.type", havingValue = "oss")
// 当括号中条件成立时,微服务启动的时候就会加载当前类。
public class OSSAutoConfiguration {
/**
* oss客户端
*/
public OSSClient ossClient;
/**
* 初始化客户端
* @param prop oss配置
* @return ossclient
* @throws ClientException 客户端异常
*/
@Bean
public OSSClient ossClient(OSSProperties prop) throws ClientException {
// ref: https://help.aliyun.com/document_detail/32011.html?spm=a2c4g.32010.0.0.33386a03cVRCNW
// EnvironmentVariableCredentialsProvider credentialsProvider =
// CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
//设置签名(ak,sk)
DefaultCredentialProvider credentialsProvider = CredentialsProviderFactory.newDefaultCredentialProvider(
prop.getAccessKeyId(), prop.getAccessKeySecret());
// 创建ClientBuilderConfiguration
ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
conf.setSignatureVersion(SignVersion.V4);
// 使用内网endpoint进行上传 prop.getIntEndpoint()
if (prop.getInternal()){
ossClient = (OSSClient) OSSClientBuilder.create()
.endpoint(prop.getIntEndpoint()) //获取内网Endpoint参数
.region(prop.getRegion()) //获取地区
.credentialsProvider(credentialsProvider) //设置签名
.clientConfiguration(conf) //设置ClientBuilderConfiguration
.build();
} else {
ossClient = (OSSClient) OSSClientBuilder.create()
.endpoint(prop.getEndpoint()) //获取外网Endpoint参数
.region(prop.getRegion())
.credentialsProvider(credentialsProvider)
.clientConfiguration(conf)
.build();
}
return ossClient;
}
/**
* 关闭客户端
*/
//容器销毁前会执行该部分代码
@PreDestroy
public void closeOSSClient() {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
nacos配置
XML
server:
port: 18082
storage:
type: oss
oss:
internal: false
endpoint: 你的外⽹endpoint
intEndpoint: 你的内⽹endpoint
region: 你的region
accessKeyId: 你的accessKeyId
accessKeySecret: 你的accessKeySecret
bucketName: frameworkjava
pathPrefix: folder/
expre: 600
minLen: 0
maxLen: 1073741824
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
核心接口
上传
如何使用JavaSDK简单上传文件_对象存储(OSS)-阿里云帮助中心
controller
java
package com.hyldzbg.fwfileservice.controller;
@RestController
@Slf4j
public class FileController {
@Autowired
private IFileService fileService;
/**
* 上传文件
* @param file
* @return
*/
@PostMapping("/upload")
public R<FileVO> upload(MultipartFile file){
FileDTO fileDTO = fileService.upload(file);
FileVO fileVO = new FileVO();
//dto拷贝给vo
BeanCopyUtil.copyProperties(fileDTO,fileVO);
return R.ok(fileVO);
}
}
service
java
package com.hyldzbg.fwfileservice.service.impl;
@Service
@Slf4j
@ConditionalOnProperty(value = "storage.type", havingValue = "oss")
public class OSSFileService implements IFileService {
@Autowired
private OSSClient ossClient;
@Autowired
private OSSProperties ossProperties;
@Override
public FileDTO upload(MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
//获取原始的文件名
String originalFilename = file.getOriginalFilename();
//保留后缀名
String extName = originalFilename.substring(originalFilename.lastIndexOf(".")+1);
//在oss中存储名字就是UUID + 文件的后缀名(构建文件名/路径)
String objectName = ossProperties.getPathPrefix() + UUID.randomUUID()+"."+extName;
ObjectMetadata objectMetadata = new ObjectMetadata();
// set public read(设置权限)
objectMetadata.setObjectAcl(CannedAccessControlList.PublicRead);
// 创建PutObjectRequest对象。(构建上传文件的对象)
PutObjectRequest putObjectRequest = new PutObjectRequest(ossProperties.getBucketName(), objectName, inputStream, objectMetadata);
// 创建PutObject请求。(上传文件)
PutObjectResult putObjectResult = ossClient.putObject(putObjectRequest);
if (putObjectResult == null || StringUtils.isBlank(putObjectResult.getRequestId())) {
log.error("上传oss异常putObjectResult未正常返回: {}", putObjectRequest);
throw new ServiceException(ResultCode.OSS_UPLOAD_FAILED);
}
FileDTO sysFileDTO = new FileDTO();
sysFileDTO.setUrl(ossProperties.getBaseUrl() + objectName);
sysFileDTO.setKey(objectName);
sysFileDTO.setName(new File(objectName).getName());
return sysFileDTO;
} catch (Exception e) {
log.error("上传oss异常", e);
throw new ServiceException(ResultCode.OSS_UPLOAD_FAILED);
}
}
}
VO(DTO和VO属性一样)
java
package com.hyldzbg.fwfileservice.domain.vo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FileVO {
private String url;
//路径信息
private String key;
private String name;
}
前端直传
controller
java
/**
* 获取前端签名
*/
@GetMapping("/sign")
public R<SignVO> getSign(){
SignDTO signDTO = fileService.getSign();
SignVO signVO = new SignVO();
//dto拷贝给vo
BeanCopyUtil.copyProperties(signDTO,signVO);
return R.ok(signVO);
}
service
java
@Override
public SignDTO getSign() {
try {
//获取ak sk
String accesskeyid = ossProperties.getAccessKeyId();
String accesskeysecret = ossProperties.getAccessKeySecret();
// 获取当前时间
Instant now = Instant.now();
//构建返回数据
SignDTO signDTO = new SignDTO();
signDTO.setHost(ossProperties.getBaseUrl());
signDTO.setPathPrefix(ossProperties.getPathPrefix());//路径前缀
// 步骤1:创建policy。
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> policy = new HashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(OSSCustomConstants.SIGN_EXPIRE_TIME_FORMAT)
.withZone(java.time.ZoneOffset.UTC);
String expiration = formatter.format(now.plusSeconds(ossProperties.getExpre()));
policy.put("expiration", expiration);//设置延时时间
List<Object> conditions = new ArrayList<>();
//设置bucket
Map<String, String> bucketCondition = new HashMap<>();
bucketCondition.put("bucket", ossProperties.getBucketName());
conditions.add(bucketCondition);
//设置签名算法和格式
Map<String, String> signatureVersionCondition = new HashMap<>();
signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256");
conditions.add(signatureVersionCondition);
//
Map<String, String> credentialCondition = new HashMap<>();
formatter = DateTimeFormatter.ofPattern(OSSCustomConstants.SIGN_DATE_FORMAT)
.withZone(java.time.ZoneOffset.UTC);
String dateStr = formatter.format(now);//生成请求时间
String xOSSCredential = accesskeyid + "/" + dateStr + "/" + ossProperties.getRegion() + "/oss/aliyun_v4_request";
signDTO.setXOSSCredential(xOSSCredential);
credentialCondition.put("x-oss-credential", xOSSCredential); // 替换为实际的 access key id
conditions.add(credentialCondition);
Map<String, String> dateCondition = new HashMap<>();
// 定义日期时间格式化器
formatter = DateTimeFormatter.ofPattern(OSSCustomConstants.SIGN_REQUEST_TIME_FORMAT)
.withZone(java.time.ZoneOffset.UTC);
// 格式化时间
String xOSSDate = formatter.format(now);
signDTO.setXOSSDate(xOSSDate);
dateCondition.put("x-oss-date", xOSSDate);//请求日期
conditions.add(dateCondition);
//限制文件上传的大小
conditions.add(Arrays.asList("content-length-range", ossProperties.getMinLen(), ossProperties.getMaxLen()));
conditions.add(Arrays.asList("eq", "$success_action_status", "200"));//返回响应码
policy.put("conditions", conditions);
String jsonPolicy = mapper.writeValueAsString(policy);
// 步骤2:构造待签名字符串(StringToSign)。
String policyBase64 = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
signDTO.setPolicy(policyBase64);
// 步骤3:计算SigningKey。
byte[] dateKey = hmacsha256(("aliyun_v4" + accesskeysecret).getBytes(), dateStr);
byte[] dateRegionKey = hmacsha256(dateKey, ossProperties.getRegion());
byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
// 步骤4:计算Signature。
byte[] result = hmacsha256(signingKey, policyBase64);
String signature = BinaryUtil.toHex(result);
signDTO.setSignature(signature);
return signDTO;
} catch (Exception e) {
log.error("生成直传签名失败", e);
throw new ServiceException(ResultCode.PRE_SIGN_URL_FAILED);
}
}
public static byte[] hmacsha256(byte[] key, String data) {
try {
// 初始化HMAC密钥规格,指定算法为HMAC-SHA256并使用提供的密钥。
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
// 获取Mac实例,并通过getInstance方法指定使用HMAC-SHA256算法。
Mac mac = Mac.getInstance("HmacSHA256");
// 使用密钥初始化Mac对象。
mac.init(secretKeySpec);
// 执行HMAC计算,通过doFinal方法接收需要计算的数据并返回计算结果的数组。
byte[] hmacBytes = mac.doFinal(data.getBytes());
return hmacBytes;
} catch (Exception e) {
log.error("生成直传签名失败", e);
throw new ServiceException(ResultCode.PRE_SIGN_URL_FAILED);
}
}
VO
java
package com.hyldzbg.fwfileservice.domain.vo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SignVO {
/**
* 签名
*/
private String signature;
/**
* 请求地址
*/
private String host;
/**
* 文件前缀(路径)
*/
private String pathPrefix;
/**
*
*/
private String xOSSCredential;
/**
* 请求时间
*/
private String xOSSDate;
/**
* policy
*/
private String policy;
}
constants
java
package com.hyldzbg.fwfileservice.Contants;
public class OSSCustomConstants {
/**
* 签名过期时间格式
*/
public final static String SIGN_EXPIRE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
/**
* 请求时间格式
*/
public final static String SIGN_REQUEST_TIME_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
/**
* 请求的日期格式
*/
public final static String SIGN_DATE_FORMAT = "yyyyMMdd";
}
地图功能
地图服务现在成为各大app的强需求,如东京,淘宝,抖音等,所以本项目搭建一套统一的地图服务接口。(借助腾讯地图相关接口进行封装)
常见功能分析
**获取城市列表:**作为目前主流的城市选择服务,如美团的城市选择

**获取按 A-Z 分开的城市列表信息:**该业务主要用于实现 A-Z 的城市划分,如滴滴的城市选择组件

**获取下级行政区划:**目前所有应用都有根据城市或省份获取下一级子节点列表的基本诉求,如京东的地址选择或美团的地址选择

**获取热门城市:**主要根据业务配置对应的热门城市,像12306买火车票

**地名地图搜索:**比如平时在腾讯地图上搜索一个餐厅,或者具体的地点
**根据经纬度定位所在城市:**像美团外卖首页默认当前所在地址
地图服务放在admin包下

接口实现
先接入腾讯地图服务
- 首先,登录创建AcessKey
登录腾讯地图控制台,https://lbs.qq.com/dev/console/home
- 之后申请成为个人开发者
注册地址:https://lbs.qq.com/dev/console/register,按照以下页面提示步骤操作即可
查询地图列表
实体类
java
package com.hyldzbg.fwadminservice.map.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
/**
* sys_region表对应的实体类
*/
@Getter
@Setter
@TableName("sys_region")//告诉spring这个对象和哪个表对应
public class SysRegion{ // TODO 继承do基类 BaseDo
/**
* 区域id
*/
private Long id;
/**
* 区域名称
*/
private String name;
/**
* 区域全名
*/
private String fullName;
/**
* 父级区域id
*/
private Long parentId;
/**
* 拼音
*/
private String pinyin;
/**
* 级别
*/
private Integer level;
/**
* 经度
*/
private Double longitude;
/**
* 维度
*/
private Double latitude;
/**
* 区域编码
*/
private String code;
/**
* 父级区域编码
*/
private String parentCode;
}
controller
java
/**
* 城市列表查询V3 引入二级缓存
* @return 三级城市列表信息
*/
@GetMapping("/city_hot_list")
@Override
public R<List<RegionVO>> getClityList(){
List<SysRegionDTO> sysRegionDTOList = mapService.getClityList();
List<RegionVO> RegionVOList = BeanCopyUtil.copyListProperties(sysRegionDTOList, RegionVO::new);
return R.ok(RegionVOList);
}
service
java
@Service
@Slf4j
public class MapService implements IMapService {
@Autowired
private RegionMapper regionMapper;
@Autowired
private RedisService redisService;
@Autowired
private RedissonLockService redissonLockService;
/**
* 本地内存服务对象
*/
@Autowired
private Cache<String,Object> caffeineCache;
/**
* 腾讯地图服务的服务类
*/
@Autowired
private IMapProvider mapProvider;
/**
* 城市列表查询 mysql版
* @return 三级城市列表信息
*/
public List<SysRegionDTO> getClityListV1() {
//1.声明一个空列表
List<SysRegionDTO> retlist = new ArrayList<>();
//2.查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
//3.提取城市数据列表
//4.对象转换
for(SysRegion sysRegion: sysRegionList){
if(MapConstants.CITY_LEVEL.equals(sysRegion.getLevel())){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
//sysRegion里的属性拷贝到sysRegionDTO
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
retlist.add(sysRegionDTO);
}
}
return retlist;
}
/**
* 城市列表查询 redis版
* @return 三级城市列表信息
*/
public List<SysRegionDTO> getClityListV2() {
//1.声明一个空列表
List<SysRegionDTO> retlist = new ArrayList<>();
//2.查询缓存
List<SysRegionDTO> cache = redisService.getCacheObject(MapConstants.CACHE_MAP_CITY_KEY
, new TypeReference<List<SysRegionDTO>>() {});
if(cache != null){
return cache;
}
//3.缓存没有查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
//提取城市数据列表
//对象转换
for(SysRegion sysRegion: sysRegionList){
if(MapConstants.CITY_LEVEL.equals(sysRegion.getLevel())){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
//sysRegion里的属性拷贝到sysRegionDTO
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
retlist.add(sysRegionDTO);
}
}
//4.存入缓存
boolean ret = redisService.setCacheObject(MapConstants.CACHE_MAP_CITY_KEY,retlist);
return retlist;
}
BigKey问题:每次查询Redis报文大于10K属于bigkey,或者查询的记录数较多
解决方案:bigkey通常使用拆分法,但我们这个没有办法再度拆分,借用我们脚手架项目中已经引入的本地缓存解决,同时因为本地缓存我们性能可以进一步提高,这个也是典型的二级缓存解决方案。
/**
* 城市列表查询 二级缓存版
* @return 三级城市列表信息
*/
public List<SysRegionDTO> getClityListV3() {
//1.声明一个空列表
List<SysRegionDTO> retlist = new ArrayList<>();
//2.查询一二级缓存
List<SysRegionDTO> cache = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_CITY_KEY,
caffeineCache, new TypeReference<List<SysRegionDTO>>() {});
if(cache != null){
return cache;
}
//3.缓存没有查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
//提取城市数据列表
//对象转换
for(SysRegion sysRegion: sysRegionList){
if(MapConstants.CITY_LEVEL.equals(sysRegion.getLevel())){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
//sysRegion里的属性拷贝到sysRegionDTO
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
retlist.add(sysRegionDTO);
}
}
//4.存入一二级缓存
CacheUtil.setL2Cache(redisService,MapConstants.CACHE_MAP_CITY_KEY,retlist,
caffeineCache,120l, TimeUnit.MINUTES);
return retlist;
}
存在问题
问题:服务启动时 Redis 和本地缓存尚无数据,请求全部穿透到数据库,高并发场景下极易压垮数据库
解决方案:采用启动时缓存预热机制处理
/**
* 缓存预热,城市信息
*/
@PostConstruct //可以在服务启动之前执行一些方法,返回值是void
public void initMapCache(){
//查询数据库
List<SysRegion> sysRegionList = regionMapper.selectAllRegion();
//1.在服务启动期间,就把数据缓存好
loadCityInfo(sysRegionList);
log.info("城市列表服务缓存成功");
//2.在服务启动期间,就把数据缓存城市归类列表
loadCityPyInfo(sysRegionList);
log.info("城市分类列表服务缓存成功");
}
/**
* 初始化城市列表 TODO 优化为只执行一次
*/
private void loadCityInfo(List<SysRegion> sysRegionList){
//1.查询区域信息
//2,对象转换
List<SysRegionDTO> retlist = new ArrayList<>();
for(SysRegion sysRegion: sysRegionList){
if(MapConstants.CITY_LEVEL.equals(sysRegion.getLevel())){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
//sysRegion里的属性拷贝到sysRegionDTO
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
retlist.add(sysRegionDTO);
}
}
//3.设置缓存
//4.存入一二级缓存
CacheUtil.setL2Cache(redisService,MapConstants.CACHE_MAP_CITY_KEY,retlist,
caffeineCache,120l, TimeUnit.MINUTES);
}
/**
* 初始化A-Z归类城市列表
*/
private void loadCityPyInfo(List<SysRegion> sysRegionList){
//1.获取城市信息,封装结果
Map<String,List<SysRegionDTO>> map = new TreeMap<>();
for(SysRegion sysRegion: sysRegionList){
if(MapConstants.CITY_LEVEL.equals(sysRegion.getLevel())){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
//sysRegion里的属性拷贝到sysRegionDTO
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
//2.拿取首字母大写
String firstChar = sysRegionDTO.getPinyin().toUpperCase().substring(0,1);
//3.首字母出现过
if(map.containsKey(firstChar)){
map.get(firstChar).add(sysRegionDTO); //在该字母下追加
}else {
//第一次出现该字母
List<SysRegionDTO> regionDTOS = new ArrayList<>();
regionDTOS.add(sysRegionDTO);
map.put(firstChar,regionDTOS);
}
}
//4.构建缓存
CacheUtil.setL2Cache(redisService,MapConstants.CACHE_MAP_PINYIN_KEY,map,
caffeineCache,120l, TimeUnit.MINUTES);
}
}
/**
* 城市列表查询 缓存预热版
* @return 三级城市列表信息
*/
@Override
public List<SysRegionDTO> getClityList() {
//2.查询一二级缓存
List<SysRegionDTO> cache = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_CITY_KEY,
caffeineCache, new TypeReference<List<SysRegionDTO>>() {});
if(cache != null){
return cache;
}//这里就算没查询到也不能查询数据库,直接返回空
return null;
}
}
dao
java
<select id="selectAllRegion" resultType="com.hyldzbg.fwadminservice.map.domain.entity.SysRegion">
select * from sys_region order by pinyin
</select>
<select id="selectRegionByParentId" resultType="com.hyldzbg.fwadminservice.map.domain.entity.SysRegion">
select * from sys_region where parent_id = #{parentId}
</select>
获取按照A-Z归类的城市列表信息
controller
java
/**
* 获取按照首字母排序的城市列表
* @return
*/
@GetMapping("/city_pinyin_list")
public R<Map<String,List<RegionVO>>> getCityPyList(){
//String是字母A~Z,List<RegionVO>是以该字母开头的城市
Map<String, List<RegionVO>> result = new LinkedHashMap<>();
Map<String, List<SysRegionDTO>> pinyinList = mapService.getCityPyList();
for (Map.Entry<String, List<SysRegionDTO>> region : pinyinList.entrySet()) {
result.put(region.getKey(), BeanCopyUtil.copyListProperties(region.getValue(), RegionVO::new));
}
return R.ok(result);
}
service
java
/**
* 获取按照首字母排序的城市列表
* @return
*/
@Override
public Map<String, List<SysRegionDTO>> getCityPyList() {
//2.查询一二级缓存
Map<String,List<SysRegionDTO>> map = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_PINYIN_KEY,
caffeineCache, new TypeReference<Map<String,List<SysRegionDTO>>>() {});
return map;
//这里就算没查询到也不能查询数据库,直接返回空,正常来讲数据在项目启动时就会添加到缓存中
}
获取下级行政区划信息
controller
java
/**
* 根据父级区域ID获取子集地区
* parentId 父级区域ID
* @return子集区域列表
*/
@GetMapping("/region_children_list")
@Override
public R<List<RegionVO>> getRegionChildren(@NonNull Long parentId) {
List<SysRegionDTO> sysRegionDTOList = mapService.regionChildren(parentId);
List<RegionVO> regionVOList = BeanCopyUtil.copyListProperties(sysRegionDTOList, RegionVO::new);
return R.ok(regionVOList);
}
service
java
@Override
public List<SysRegionDTO> regionChildren(Long parentId) {
//1.查缓存
String key = MapConstants.CACHE_MAP_CITY_CHILDREN_KEY + parentId;
List<SysRegionDTO> result = CacheUtil.getL2Cache(redisService, key, caffeineCache,
new TypeReference<List<SysRegionDTO>>() {});
if(result != null){
return result;
}
//2.缓存没查到,查询数据库
List<SysRegionDTO> sysRegionDTOList = new ArrayList<>();
List<SysRegion> sysRegionList = regionMapper.selectRegionByParentId(parentId);
//3.类型转化
for(SysRegion sysRegion : sysRegionList){
SysRegionDTO sysRegionDTO = new SysRegionDTO();
BeanCopyUtil.copyProperties(sysRegion,sysRegionDTO);
sysRegionDTOList.add(sysRegionDTO);
}
//4.设置缓存
CacheUtil.setL2Cache(redisService,key,sysRegionDTOList,caffeineCache,120L,TimeUnit.MINUTES);
return sysRegionDTOList;
}
获取热门城市列表
这里之后可以扩展业务功能,比如做一个热门旅游城市推荐等。
首先通过之前封装的二级缓存进行查询,如果在缓存中查到了的话,就直接返回缓存,这样可以大大减轻数据库的压力
之后调用接口(这里的城市是静态的),查询热点城市id,如果没有查到,也要缓存一个空值,防止缓存穿透
因为一般情况下缓存里都是会有值的,但是可能在服务刚开始时,或者缓存击穿时有大量请求打向数据库,这里通过使用之前的分布式锁。
- 如果线程锁竞争失败,会先阻塞10ms,如果没有等到锁的话就放弃竞争,防止线程忙等,之后会再睡眠10ms(尽量确保请求可以返回有效值),后再次查询缓存,因为可能此时已经有线程进行完存缓存的操作了。
- 如果锁竞争成功,则会先在进行一次查询缓存操作,因为这个线程可能是等别的线程释放锁后,又竞争到的,此时该线程其实没有必要查库,既然已经有线程释放锁了,那么就一定执行了缓存操作,所以此时缓存里是有值得。如果该线程是第一个抢到锁得,那么第二次查缓存依然会返回空。之后就是进行查库,和存缓存操作,最后释放锁,如果线程卡住了,过一段时间也会自动释放(这里最好使用看门狗),而且要保证这个线程的锁,只能由这个线程释放,也就是不能去释放别的线程的锁。
controller
java
/**
*获取热门城市列表
* @return
*/
@GetMapping("/hot_city_list")
@Override
public R<List<RegionVO>> getHotCityList() {
List<SysRegionDTO> sysRegionDTOList = mapService.getHotCityList();
List<RegionVO> result = BeanCopyUtil.copyListProperties(sysRegionDTOList, RegionVO::new);
return R.ok(result);
}
service
java
/**
*获取热门城市列表
* @return
*/
@Override
public List<SysRegionDTO> getHotCityList() {
// // 双检 + Redisson 防击穿
//1.查缓存
List<SysRegionDTO> result = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_HOT_CITY, caffeineCache
, new TypeReference<List<SysRegionDTO>>() {});
if(result != null){
return result;
}
//调用参数服务
String ids = argumentService.getByConfigKey(MapConstants.CONFIG_KEY).getValue();
//缓存空值,防止缓存穿透
if (ids == null || ids.isBlank()) {
/* 空值也缓存 60 秒,防止穿透 */
List<SysRegionDTO> empty = Collections.emptyList();
CacheUtil.setL2Cache(redisService, MapConstants.CACHE_MAP_HOT_CITY,
empty, caffeineCache, 60L, TimeUnit.SECONDS);
return empty;
}
//将ids转换为List
List<Long> idList = new ArrayList<>();
for(String num : ids.split(",")){
idList.add(Long.parseLong(num));
}
// //缓存为空获取分布式锁
String lockKey =RedisConstants.LOCK_HOTCITY;
long lockExpire = RedisConstants.LOCKEXPIRE;
long waitTime = RedisConstants.WAITTIME;
// // 拿锁
RLock lock = redissonLockService.acquire(lockKey, lockExpire,waitTime);
if (lock == null) {
// 未抢到锁,稍后10ms重读缓存
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
List<SysRegionDTO> result1 = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_HOT_CITY, caffeineCache,
new TypeReference<List<SysRegionDTO>>() {});
System.out.println("没抢到锁:" + result1.size());
return result1;
}
try {
// 双检
result = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_HOT_CITY, caffeineCache,
new TypeReference<List<SysRegionDTO>>() {});
if (result != null)
return result;
//2.查数据库
//查询热门城市结果
List<SysRegion> regionList = regionMapper.selectBatchIds(idList);
List<SysRegionDTO> sysRegionDTOList = BeanCopyUtil.copyListProperties(regionList,SysRegionDTO::new);
//3.设置缓存
CacheUtil.setL2Cache(redisService,MapConstants.CACHE_MAP_HOT_CITY,sysRegionDTOList,caffeineCache,120L,TimeUnit.MINUTES);
return sysRegionDTOList;
}finally {
// int i =0;
redissonLockService.releaseLock(lock); // 必须同线程释放
}
}
根据关键词和经纬度搜索地点
获取腾讯地图数据
java
package com.hyldzbg.fwadminservice.map.service.impl;
/**
* 地图服务实现类
*/
@RefreshScope
@Service
@Slf4j
@Data
@ConditionalOnProperty(value = "map.type",havingValue = "qqmap") //如果map.type等于qqmap就加载这个bean
public class QQMapServiceImpl implements IMapProvider {
/**
* 腾讯位置服务域名
*/
@Value("${qqmap.apiServer}")
private String apiServer;
/**
* 调用腾讯位置服务的密钥
*/
@Value(("${qqmap.key}"))
private String key;
@Autowired
private RestTemplate restTemplate;//相当于是一个客户端,可以发送get,post等请求
/**
* 根据关键词搜索地点
* @param suggestSearchDTO 搜素条件
* @return
*/
@Override
public PoiListDTO serchQQMapPlaceByRegion(SuggestSearchDTO suggestSearchDTO) {
// 1 构建请求url
String url = String.format(
apiServer + MapConstants.QQMAP_API_PLACE_SUGGESTION +
"?key=%s®ion=%s®ion_fix=1&page_index=%s&page_size=%s&keyword=%s",
key, suggestSearchDTO.getId(), suggestSearchDTO.getPageIndex(), suggestSearchDTO.getPageSize(),suggestSearchDTO.getKeyword()
);
//2 直接发送请求,并获得响应结果 ,对象转换
ResponseEntity<PoiListDTO> response = restTemplate.getForEntity(url,PoiListDTO.class);//url和结果集
if(!response.getStatusCode().is2xxSuccessful()){ //如果返回的code不是以2打头的
log.error("获取关键词查询结果异常",response);
throw new ServiceException(ResultCode.QQMAP_QUERY_FAILED);
}
return response.getBody();
}
@Override
public GeoResultDTO getQQMapDistrictByLonLat(LocationDTO locationDTO) {
//1 构建url
String url = String.format(apiServer + MapConstants.QQMAP_GEOCODER +
"?key=%s&location=%s",
key, locationDTO.formatInfo()
);
//2 直接发送请求,并获得响应结果 ,对象转换
ResponseEntity<GeoResultDTO> response = restTemplate.getForEntity(url,GeoResultDTO.class);
if(!response.getStatusCode().is2xxSuccessful()){ //如果返回的code不是以2打头的
log.error("获取经纬度查询结果异常",response);
throw new ServiceException(ResultCode.QQMAP_QUERY_FAILED);
}
return response.getBody();
}
}
service
java
/**
* 根据地点搜索
* @param placeSearchReqDTO 搜索条件
* @return 搜索结果
*/
@Override
public BasePageDTO<SearchPoiDTO> searchSuggestOnMap(PlaceSearchReqDTO placeSearchReqDTO) {
//1.构建查询腾讯位置服务的入参SuggestSearchDTO
SuggestSearchDTO suggestSearchDTO = new SuggestSearchDTO();
BeanCopyUtil.copyProperties(placeSearchReqDTO,suggestSearchDTO);
suggestSearchDTO.setPageIndex(placeSearchReqDTO.getPageNo()); // 这俩类的页码名字不一样要单独转
suggestSearchDTO.setId(String.valueOf(placeSearchReqDTO.getId()));// 这俩类的id类型不一样要单独转
//2.调用地图位置查询接口
PoiListDTO poiListDTO = mapProvider.serchQQMapPlaceByRegion(suggestSearchDTO);
List<PoiDTO> poiDTOList = poiListDTO.getData();
//3.做结果对象转换
//页码信息转换
BasePageDTO<SearchPoiDTO> result = new BasePageDTO<>();
//设置查询结果总数
result.setTotals(poiListDTO.getCount());
//设置总页数(总数量/页大小)
result.setTotalPages(BasePageDTO.calculateTotalPages(poiListDTO.getCount(),placeSearchReqDTO.getPageSize()));
//结果数据转换
List<SearchPoiDTO> list = new ArrayList<>();
for(PoiDTO poiDTO : poiDTOList){
SearchPoiDTO searchPoiDTO = new SearchPoiDTO();
BeanCopyUtil.copyProperties(poiDTO,searchPoiDTO);
//设置经纬度
searchPoiDTO.setLatitude(poiDTO.getLocation().getLat());
searchPoiDTO.setLongitude(poiDTO.getLocation().getLng());
list.add(searchPoiDTO);
}
result.setList(list);
return result;
}
/**
* 根据经纬度来定位城市
* @param locationReqDTO 经纬度信息
* @return 城市信息
*/
@Override
public RegionCityDTO locateCityByLocation(LocationReqDTO locationReqDTO) {
//1.构建查询腾讯位置服务的入参SuggestSearchDTO
LocationDTO locationDTO = new LocationDTO();
BeanCopyUtil.copyProperties(locationReqDTO,locationDTO);
//2.调用地图位置查询接口
GeoResultDTO geoResultDTO = mapProvider.getQQMapDistrictByLonLat(locationDTO);
//3.做结果对象转换
RegionCityDTO result = new RegionCityDTO();
if(geoResultDTO != null && geoResultDTO.getResult() != null && geoResultDTO.getResult().getAd_info() != null){
//获取城市名称
String cityName = geoResultDTO.getResult().getAd_info().getCity();
// 查缓存
List<SysRegionDTO> cache = CacheUtil.getL2Cache(redisService, MapConstants.CACHE_MAP_CITY_KEY,caffeineCache,
new TypeReference<List<SysRegionDTO>>() {});
//遍历缓存查找对应数据(名称一致)
for(SysRegionDTO sysRegionDTO : cache){
if(cityName.equals(sysRegionDTO.getFullName())){
BeanCopyUtil.copyProperties(sysRegionDTO,result);
return result;
}
}
}
return result;
}
/**
* 根据总数和页面计算分页总数
*
* @param totals 总数量
* @param pageSize 分页大小
* @return 分页数量
*/
public int getTotalPages(int totals,int pageSize){
return totals % pageSize == 0 ? totals/pageSize :(totals/pageSize +1);
}