往期文章
- 《从零开始的毕业设计》-ELS中台快递物流调度系统(一)搭建项目环境 - 掘金 (juejin.cn)
- 《从零开始的毕业设计》-ELS中台快递物流调度系统(二)基本订单功能 - 掘金 (juejin.cn)
仓库地址: ashlee618/ELS快递物流调度系统 (github.com)
前倾回顾
在上一篇文章中,我们搭建好了基本的物流订单能力,整合了Mybatis-plus,完成了基本的CRUD功能。在本文中,我们将完成物流地址的基本功能。完成构建四级地址能力以及基本的CRUD,使用本地缓存Caffeine。
物流地址能力
确保地址是有效性
以前做项目的时候设计到地址,都是直接填一个用户自己写好的地址,其实这也是非常不负责任地。
万一用户填一个,太平洋比基尼海滩海螺街124号之类的无效地址,那么就会引起不必要纠纷
商家:我只是负责发货的,如何送到顾客的手上,可不关我事嗷
快递公司:我只是负责打包和分发包裹,怎么送是快递员的事情,这可不关我事嗷
快递员:???
所以我们在上游的时候,就要对收货地址校验,确保他的有效性。
简单做法
对于省、市、区、街道,我们可以直接用常量来固定好,用户只能通过下拉框来选择。然后再多一个字段给用户填详细的地址,比如小区名字、第几栋、多少号等等。
但是这种做法有一个问题:
不同省下面的区,可能会有相同的名字
比如: 北京朝阳区和长春朝阳区。
如果要解决这种问题,那么地址必须要携带省、市、区、街道用来缩小问题。但是这也会引来另外的问题
占用带宽
-
传输层面上:直接传输汉字的效率,肯定是比传输数字、字符等要低,并且有的地方的地址很长很长,不太合适。
-
并发层面上:物流地址是一个并发频繁的模块,订单、物流信息、物理服务模块都需读取地址。
四级地址
四级地址,就是指省、市、县、街道。构成四级。
我们可以看到,每一级地址,都有其对应的code,并且下一级地址的code,是由上一级转移过来的。因此我
们只需要知道某一级的地址,就可以推出他的所属上一级地址。
然后我就去网上区找全国的四级地址,发现这个东西还真不太好找,基本都是在某dn上,要积分下载的
好不容易找到一个,发现光是文本格式的就占7.62MB,打开的时候都会卡
最最最无语的是,他的格式,还不是原作者的那种,name,code,level的,只有name和code
就在我感觉快要寄了的时候,发现了宝藏!原来的路行不通,那么我们就用别的东西来替代。
刚好找到别的做好的数据库,虽然不是跟原作者的数据表重合,但是也能来用,

ok那么四级地址数据到这里就算是弄完了
CRUD
CRUD也是整合的是Mybatis-plus的,在上一篇中已经介绍过了,以后如果没有细节的东西,就不对这个部分进行介绍了。
读多写少
全国地址基本上是不会改变的,除非是发现某片世外桃源,或者是重要历史大事件,才会进行改名,所以数据基本上不会变,因此它是一个读多写少的类型。
对于查询压力,我们可以常用的解决手段有数据库层面上的、还有缓存层面上的
数据库
索引
把相关字段建立联合索引,不让他走回表,建立完索引,还要自己用explain执行计划查看一下,是不是最后走了索引,如果没有走,可以用force index让他强行走索引。
一主多从
一个master负责写,多个slave来缓解读的压力。因为我就只有一台阿里云服务器,后面的话打算用dokcer部署Mysql集群还是挺方便的。
分表分库
可以从垂直维度和水平维度,进行切分,打算是自己mock十万级别的数据,然后根据某一种策略来进行切片,可能会用到sharding-jdbc等其他分库分表中间件吧。这一步留作后面优化,目前先把基本的能力搭建起来。
缓存
缓存我们一般想到的是Redis,但是我们要具体问题具体分析,如果跑开了业务,谈技术,是耍流氓。
四级地址在从下单到收货的业务流程中都会用到,因此设计的时候要考虑如何最大限度地提高QPS
我们知道缓存效率是,本地缓存 > 远程缓存 > 数据库。
本地缓存就是有一点不好的是,他不能存太多东西,太多的话会导致堆内存泄漏等问题。但是他的优点也是非常地明显,就是他很快!因此在一些特殊地场景,比如秒杀、抢红包等高并发并且数据量不算太大的时候,技术选型上,可以选择本地缓存 + Redis形成二级缓存。
常见优秀的本地缓存框架就有:Guava的Cache、Apache的LRUMap、Caffeine、Edcache等等
springboot2.0 的时候就放弃了Guava的Cache作为缓存,改用Caffeine,那我们也要学一下别人,就用Caffeine!
Caffein本地缓存
Caffeine是基于Cache的,操作也非常相似,很容易从Cache上转移到Caffeine。可以理解为前者是后者的扩展版,内置了更加丰富的功能,比如驱逐策略、监听器、统计等功能。
依赖
用的是2.6.2版本的
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
CaffeineService
这里我们需要声明一个Service给Mybatis-plus使用,我尝试过了用 @Bean的方式,先注入Caffeine,发现不太行,估计是Mybatis-plus里面的bean加载的时候,caffeine还没加载,导致找不到。 示例中,也是通过SpringUtil后面来手动获取的bean。我们就简单理解为给Caffeine封装了一套衣服就行。
/**
* Caffeine本地缓存
* @author 洪敏锋
*/
@Component
public class CaffeineService {
private Cache<Object, Object> cache;
public CaffeineService() {
this.cache = Caffeine.newBuilder()
//过期策略:一分钟没有读取,则过期删除
.expireAfterWrite(1, TimeUnit.MINUTES)
//允许容量100个,超过自动删除
.maximumSize(100)
.build();
}
public Object put(Object key, Object value) {
cache.put(key, value);
return key;
}
public Object getIfPresent(Object key) {
return cache.getIfPresent(key);
//还可以写如果本地缓存找不到可以去redis找
//因为是跟mybatis-plus进行整合没加redis,所以默认情况下,本地缓存找不到是去数据库找的
}
public Object get(Object key, Function<Object, Object> function) {
return cache.get(key, function);
}
public void remove(Object key) {
cache.invalidate(key);
}
public void cleanUp() {
cache.cleanUp();
}
public long estimatedSize() {
return cache.estimatedSize();
}
}
MybatisCache
Mybatis-plus的本地缓存,官网说的是建议是service层,我这边是在Dao层上进行缓存的。
/**
* 二级缓存实现
* @author 洪敏锋
*/
@Slf4j
public class MybatisCache implements Cache {
private CaffeineService caffeineService;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private String id;
public MybatisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
if (caffeineService == null) {
caffeineService = SpringUtil.getBean(CaffeineService.class);
}
log.info("加入缓存");
if (value != null) {
caffeineService.put(key, value);
}
}
@Override
public Object getObject(Object key) {
if (caffeineService == null) {
caffeineService = SpringUtil.getBean(CaffeineService.class);
}
log.info("获取缓存"+key.toString());
try {
if (key != null) {
return caffeineService.getIfPresent(key);
}
} catch (Exception e) {
log.error("缓存出错 ");
}
return null;
}
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
* @param key The key
* @return Not used
*/
@Override
public Object removeObject(Object key) {
if (caffeineService == null) {
caffeineService = SpringUtil.getBean(CaffeineService.class);
}
log.info("移除缓存");
caffeineService.remove(key);
return null;
}
/**
* Clears this cache instance.
*/
@Override
public void clear() {
if (caffeineService == null) {
caffeineService = SpringUtil.getBean(CaffeineService.class);
}
log.info("清理缓存");
caffeineService.cleanUp();
}
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
@Override
public int getSize() {
if (caffeineService == null) {
caffeineService = SpringUtil.getBean(CaffeineService.class);
}
log.info("获取缓存大小");
return (int) caffeineService.estimatedSize();
}
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
* <p>
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
@Override
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
}
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}
public static Object getBean(String name){
return applicationContext.getBean(name);
}
public static <T> T getBean(String name, Class<T> clazz){
return applicationContext.getBean(name, clazz);
}
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}
配置Mapper层的缓存
@CacheNamespace(implementation= MybatisCache.class,eviction=MybatisCache.class)
public interface BsStreetMapper extends BaseMapper<BsStreet> {
}
在mapper的xml文件生命namespace
<mapper namespace="com.example.elsaddress.mapper.BsStreetMapper">
<cache-ref namespace="com.example.elsaddress.mapper.BsStreetMapper"/>
</mapper>
效果图
我们自己编写一个示例进行测试:
@Test
void readJsonFromTxt() throws FileNotFoundException {
UserAddress byId = service.getById(930929442728595456L);
UserAddress byId1 = service.getById(930929442728595456L);
UserAddress byId2 = service.getById(930929502258352128L);
QueryWrapper query = new QueryWrapper();
query.eq("STREET_NAME","东华门街道");
BsStreet one = streetService.getOne(query);
System.out.println(one);
}
首先是获取两次相同的,然后再获取另外一个,最后试试street的,大致就是这么简单。
我们可以看到,他第一次获取的时候,缓存时不存在,所以他去DB找到了数据之后,就把他加入到缓存中。 第二获取的时候,发现缓存中存在,就直接返回缓存了。
我们通过Debug的方式查看,就会更加明显。
可以看到我们的结果缓存再Cache里面。这里多提一嘴,缓存有两种方式,一种是缓存sql语句,就是本文这种,还有一种是指定的Key,一般是那种特定的资源名字,PV数据,UV数据等。
结尾
后续待完成部分
- 根据用户的经纬度,直接定位地址。Redis中有Geo数据结构,能够很好的处理经纬度的数据,感觉可以用一用。
至此,我们已经基本完成了物流地址的服务能力,下一篇就要去完成物流服务和物流详情部分。请大家持续关注我!球球各位🧓给我点个赞吧,你们的赞真的非常重要!