《从零开始的毕业设计》-ELS中台快递物流调度系统(三)基本物流地址功能

835 阅读9分钟

往期文章

仓库地址: ashlee618/ELS快递物流调度系统 (github.com)

前倾回顾

在上一篇文章中,我们搭建好了基本的物流订单能力,整合了Mybatis-plus,完成了基本的CRUD功能。在本文中,我们将完成物流地址的基本功能。完成构建四级地址能力以及基本的CRUD,使用本地缓存Caffeine。

物流地址能力

确保地址是有效性

以前做项目的时候设计到地址,都是直接填一个用户自己写好的地址,其实这也是非常不负责任地。

万一用户填一个,太平洋比基尼海滩海螺街124号之类的无效地址,那么就会引起不必要纠纷

image.png


商家:我只是负责发货的,如何送到顾客的手上,可不关我事嗷

快递公司:我只是负责打包和分发包裹,怎么送是快递员的事情,这可不关我事嗷

快递员:???

image.png


所以我们在上游的时候,就要对收货地址校验,确保他的有效性。

简单做法

对于省、市、区、街道,我们可以直接用常量来固定好,用户只能通过下拉框来选择。然后再多一个字段给用户填详细的地址,比如小区名字、第几栋、多少号等等。

但是这种做法有一个问题:

不同省下面的区,可能会有相同的名字

比如: 北京朝阳区和长春朝阳区。

如果要解决这种问题,那么地址必须要携带省、市、区、街道用来缩小问题。但是这也会引来另外的问题

占用带宽

  • 传输层面上:直接传输汉字的效率,肯定是比传输数字、字符等要低,并且有的地方的地址很长很长,不太合适。

  • 并发层面上:物流地址是一个并发频繁的模块,订单、物流信息、物理服务模块都需读取地址。

四级地址

四级地址,就是指省、市、县、街道。构成四级。

image.png

我们可以看到,每一级地址,都有其对应的code,并且下一级地址的code,是由上一级转移过来的。因此我 们只需要知道某一级的地址,就可以推出他的所属上一级地址。

然后我就去网上区找全国的四级地址,发现这个东西还真不太好找,基本都是在某dn上,要积分下载的

image.png

好不容易找到一个,发现光是文本格式的就占7.62MB,打开的时候都会卡

image.png

最最最无语的是,他的格式,还不是原作者的那种,name,code,level的,只有name和code

image.png

image.png

就在我感觉快要寄了的时候,发现了宝藏!原来的路行不通,那么我们就用别的东西来替代。

刚好找到别的做好的数据库,虽然不是跟原作者的数据表重合,但是也能来用,

image.png

![image.png](p6-juejin.byteimg.com/tos-cn-i-k3… fbf33030c6~tplv-k3u1fbpfcp-watermark.image?)

image.png

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的,大致就是这么简单。

image.png

我们可以看到,他第一次获取的时候,缓存时不存在,所以他去DB找到了数据之后,就把他加入到缓存中。 第二获取的时候,发现缓存中存在,就直接返回缓存了。

我们通过Debug的方式查看,就会更加明显。

image.png

可以看到我们的结果缓存再Cache里面。这里多提一嘴,缓存有两种方式,一种是缓存sql语句,就是本文这种,还有一种是指定的Key,一般是那种特定的资源名字,PV数据,UV数据等。

结尾

后续待完成部分

  • 根据用户的经纬度,直接定位地址。Redis中有Geo数据结构,能够很好的处理经纬度的数据,感觉可以用一用。

至此,我们已经基本完成了物流地址的服务能力,下一篇就要去完成物流服务和物流详情部分。请大家持续关注我!球球各位🧓给我点个赞吧,你们的赞真的非常重要!

image.png