首页 停车计费功能实践
文章
取消

停车计费功能实践

如何设计停车计费功能

在智慧停车场管理系统中,同可用停车位一样计费规则几乎是每辆车进出停车场都会用到,而这种数据俗称称为热数据,读关系库显然不是最优解,引入缓存才是王道。

分布式缓存

缓存作为互联网分布式开发两大杀器之一(另一个是消息队列),应用场景相当广泛,遇到高并发、高性能的案例,几乎都能看到缓存的身影。

从应用与缓存的结合角度来区分可以分为本地缓存和分布式缓存。

我们经常用 Tomcat 作为应用服务,用户的 session 会话存储,其实就是缓存,只不过是本地缓存,如果需要实现跨 Tomcat 的会话应用,还需要其它组件的配合。Java 中我们应经用到的 HashMap 或者 ConcurrentHashMap 两个对象存储,也是本地缓存的一种形式。Ehcache 和 Google Guava Cache 这两个组件也都能实现本地缓存。单体应用中应用的比较多,优势很明显,访问速度极快;劣势也很明显,不能跨实例,容量有限制。

分布式场景下,本地缓存的劣势表现的更为突出,与之对应的分布式缓存则更能胜任这个角色。软件应用与缓存分离,多个应用间可以共享缓存,容量扩充相对简便。有两个开源分布式缓存产品:memcached 和 Redis。

其中 memcached 是出现比较早的缓存产品,只支持基础的 key-value 键值存储,数据结构类型比较单一,不提供持久化功能,发生故障重启后无法恢复,它本身没有成功的分布式解决方案,需要借助于其它组件来完成。Redis 的出现,直接碾压 memcached ,市场占有率节节攀升。

Redis 在高效提供缓存的同时,也支持持久化,在故障恢复时数据得已保留恢复。支持的数据类型更为丰富,如 string , list , set , sorted set , hash 等,Redis 自身提供集群方案,也可以通过第三方组件实现,比如 Twemproxy 或者 Codis 等等,在实际的产品应用中占有很大的比重。另外 Redis 的客户端资源相当丰富,支持近 50 种开发语言。

综上所述,我最终选择 Redis 应用到智慧停车场系统中。

搭建线上 Redis 环境

下载好 Redis 之后,文件目录如下所示:

下载完成之后,首先需要对 Redis 进行配置,而 Redis 的配置是放在一个叫做 redis.conf 的文件里:

  1. 修改 bind 127.0.0.1 -::1 –> bind 0.0.0.0,这一步的作用是让 Redis 能够接收来自任何网络的请求。这就保证我们的应用能够正常访问 Redis
  2. 然后是修改 protected-mode yes –> protected-mode no,这一步的作用是关闭 Redis 的自我保护,Redis 的自我保护会拒绝一些匿名的访问。
  3. 再然后是修改 daemonize no –> daemonize yes,这能让我们的 Redis 在服务器后台运行,不至于关闭控制台就被停止运行。

然后是比较关键的两步,上面的操作都是对 Redis 本身的配置进行修改,现在需要修改我的服务器配置:

首先是防火墙设置,允许对应端口号的开放,而 Redis 设置的端口号是 6379:

然后是第二步,也是最容易忽视的一步,在 shell 上开启防火墙端口:

命令:firewall-cmd –zone=public –add-port=6379/tcp –permanent,意思是永久开启防火墙 6379 端口。然后执行命令:firewall-cmd –list-ports 能够查看到下面的效果:

这表明 6379 端口已被放行,接下来我们的应用访问 Redis 线上服务就没有问题了。

在程序中连接 Redis 服务

首先引入 Redis 对应的包:

1
2
3
4
5
<!-- 集成 redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后进行 Redis 连接配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
  redis:
    database: 0
    host: 175.178.236.7
    port: 6379
    password: 123456
    timeout: 60000
    jedis:
      pool:
        max-active: 1000
        max-wait: -1
        max-idle: 10
        min-idle: 5

其中:

  1. database 配置应用模块使用 Redis 的哪个库
  2. host 是 Redis 线上服务的地址
  3. port 是 Redis 的端口号
  4. password 是 Redis 的访问密码
  5. timeout 是访问 Redis 的超时时间
  6. jedis 是 Redis 客户端,pool 是连接池的一些配置

除了配置文件,还需要在代码中读入配置,连接线上 Redis 服务,而 Redis 的默认序列化方式往往满足不了生产的需要,所以通常需要自定义 Redis 的序列化方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 自定义 Redis 序列化方式
 */
public RedisCacheAutoConfiguration() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    JavaTimeModule timeModule = new JavaTimeModule();
    timeModule.addDeserializer(LocalDate.class,
            new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    timeModule.addDeserializer(LocalDateTime.class,
            new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    timeModule.addSerializer(LocalDate.class,
            new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
    timeModule.addSerializer(LocalDateTime.class,
            new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    objectMapper.registerModule(timeModule);
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
}

然后是执行连接 Redis 服务的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 注入 RedisTemplate
 */
@Primary
@Bean
public RedisTemplate<String, Object> redisTemplate() {
    // 指定序列化方式
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setKeySerializer(RedisSerializer.string());
    redisTemplate.setHashKeySerializer(RedisSerializer.string());
    redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
    redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    return redisTemplate;
}

设计停车计费数据表

里面的核心字段就只有三个,分别是 stay_time_start、stay_time_end 和 fee,其两个是用来比较车主停车的时间从而判断停车费用,而对应的费用就是用 fee 标注好的。下面是预插入的数据:

  1. 30分钟内免费
  2. 2小时内,5元
  3. 2小时以上12小时以内,10元
  4. 12小时以上24小时以内,20元

然后是最核心的一步,将上面的数据读入 Redis 当中,而且是要在项目第一次使用时就加载到 Redis 当中去。项目启动后就加载,Spring Boot 提供了两种方式在项目启动时就加载的方式供大家使用:ApplicationRunner 与 CommandLineRunner,都是在 Spring 容器初始化完毕之后执行起 run 方法,两者最明显的区别就是入参不同。我采用的是 ApplicationRunner 方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import static com.flameking.parking.charging.constants.ChargingConstant.CHARGING_RULE;

@Component
@Order(value = 1)//order 是加载顺序,越小加载越早,若有依赖关于,建议按顺序排列即可
public class StartUpApplicationRunner implements ApplicationRunner {

  @Autowired
  private RedisClient redisClient;
  @Autowired
  IChargingRuleService ruleService;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    List<ChargingRule> rules = ruleService.list();
    if (!redisClient.exists(CHARGING_RULE)) {
      redisClient.set(CHARGING_RULE, JSONObject.toJSONString(rules));
    }
  }
}

然后就可以在需要对停车时间进行计费的时候,随时查询 Redis 缓存了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public float calculateFee(Long stayMinutes) {
    String ruleStr = redisClient.get(CHARGING_RULE);
    JSONArray array = JSONObject.parseArray(ruleStr);
    List<ChargingRule> rules = JSONObject.parseArray(array.toJSONString(), ChargingRule.class);
    float fee = 0;
    for (ChargingRule chargingRule : rules) {
        //遍历计费规则,寻找当前停车时间对应的计费区间,以及收费
        if (chargingRule.getStayTimeStart() <= stayMinutes && chargingRule.getStayTimeEnd() > stayMinutes) {
            fee = chargingRule.getFee();
            break;
        }
    }
    return fee;
}

那么每次调用计算停车费用接口,就只需要传递停车的时间,格式是分钟。然后就能返回具体收费了。下面是存在 Redis 中的计费数据:

本文由作者按照 CC BY 4.0 进行授权

支付宝支付功能实践

分布式定时任务功能实现