如何设计停车计费功能
在智慧停车场管理系统中,同可用停车位一样计费规则几乎是每辆车进出停车场都会用到,而这种数据俗称称为热数据,读关系库显然不是最优解,引入缓存才是王道。
分布式缓存
缓存作为互联网分布式开发两大杀器之一(另一个是消息队列),应用场景相当广泛,遇到高并发、高性能的案例,几乎都能看到缓存的身影。
从应用与缓存的结合角度来区分可以分为本地缓存和分布式缓存。
我们经常用 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 的文件里:
- 修改 bind 127.0.0.1 -::1 –> bind 0.0.0.0,这一步的作用是让 Redis 能够接收来自任何网络的请求。这就保证我们的应用能够正常访问 Redis
- 然后是修改 protected-mode yes –> protected-mode no,这一步的作用是关闭 Redis 的自我保护,Redis 的自我保护会拒绝一些匿名的访问。
- 再然后是修改 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
其中:
- database 配置应用模块使用 Redis 的哪个库
- host 是 Redis 线上服务的地址
- port 是 Redis 的端口号
- password 是 Redis 的访问密码
- timeout 是访问 Redis 的超时时间
- 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 标注好的。下面是预插入的数据:
- 30分钟内免费
- 2小时内,5元
- 2小时以上12小时以内,10元
- 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 中的计费数据: