Redis优化Spring项目的数据库读写

使用Redis做缓存,简单优化软件工程实践课的MySql读写的性能

前言

醋来了!前面对Redis的学习就是为了优化一下一个完整的SpringBoot+MySql项目的数据库搜索的性能,使用Redis作为MySql数据库的缓存使用,优化高频读取操作的性能,以及涉及多表查询的结果的缓存(减小服务器负担)。

优化思路

本质上就是非常经典的缓存模型,客户端对一些高频接口或高搜索复杂度接口进行请求时,在后端将请求先引导到对Redis的访问,若请求命中(Redis存有对应的数据),则直接返回给客户端该数据,若未命中(Redis返回结果为空),则按照正常流程访问MySql等主数据库,并在返回结果前向Redis写入该数据(并设置过期时间),以便下次访问。

img1

具体案例

案例代码来自软件工程专业课《软件工程综合实践》的Vue3+SpringBoot+MySql的项目。

这里选择一个涉及多表查询的查询性能较差的搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public Orders getOrdersById(Integer orderId) {
//从Redis查询
String key = "orders:id:" + orderId; //统一设置key为 "类名:请求参数名"+请求参数名
Orders redis_order = (Orders) redisTemplate.opsForValue().get(key); //查询key
if(redis_order != null)
return redis_order;

Orders orders =ordersMapper.getOrdersById(orderId);

//存入Redis
redisTemplate.opsForValue().set(key,orders,3600, TimeUnit.SECONDS);
return orders;
}

具体实现还是非常简单的,接下来简单测试一下性能。

性能测试

因为是课程设计的小项目,数据量没有多大,只是相对的对比一下采用Redis做缓存和不用时在ApiFox上的响应速度。

在ApiFox上对接口对应的方法getOrderById进行100次测试,结果如下:

使用Redis作为缓存的100次访问测试:

img2

未使用Redis作为缓存的100次访问测试:

img3

测试结论

可见二者有着较大的性能差异,并且随着查询接口对数据库搜索数据规模的增大,不使用缓存机制的搜索时间还将上升,而使用Redis作为缓存本质上就是对一个键值对进行简单的读取,除了第一次的未命中外,此后的缓存访问都将以较快的速度进行。

此外针对高热度的访问接口,缓存机制也将持续保留并刷新其过期时间,本质上是对高负载接口的读写优化。

那么代价是什么呢?

数据库缓存不一致

缓存是写入Redis了,如果在其过期时间内又有在MySql对其相关数据进行了操作,导致MySql上的对应数据和Redis上的不一致,此时Redis上存的不就是错误数据了吗?

此时的解决方法就是,在后端的涉及Redis存储的数据的类的操作,也是不仅仅要更新MySql,也要”更新“Redis(如果有)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public Orders handleOrders(Integer orderId, String orderState) {
String key = "orders:id:" + orderId;
User user =userMapper.getUserByName(getCurrentUserName());
Orders order =ordersMapper.getOrdersById(orderId);
int businessId = order.getBusinessId();
User boss = userMapper.getUserByName(getCurrentUserName());
List<Business> businessList = businessMapper.getBusinessListByUserId(boss.getId());
boolean isAssociated = businessList.stream()
.anyMatch(biz -> biz.getId().equals(businessMapper.getBusinessById(businessId).getId()));
if ("BUSINESS".equals(getCurrentUserType()) && !isAssociated) {
throw new RuntimeException("错误:老板未关联此商家");
}
ordersMapper.handleOrders(orderId, orderState);
redisTemplate.delete(key); //删除!!!
return ordersMapper.getOrdersById(orderId);
}

删除缓存还是更新缓存

在对缓存数据进行更新时有两个选择:
更新缓存:找到对应的key,更新其value
删除缓存:直接删除对应key的键值对数据,等待下一次访问时再将数据写入缓存

通常会先更新数据库再删除缓存,而不是直接更新缓存,原因如下:
1.简单直接:代码逻辑清晰,不易出错。
2.避免并发写问题:在多个线程/服务同时更新同一个数据时,无论操作顺序如何,最终都会通过删除缓存来保证下次读取到最新数据。它避免了“更新顺序”竞态条件(后面会详述)。
3.处理复杂数据:如果缓存的数据是经过复杂计算或聚合(如多个表的JOIN结果),更新缓存时需要重复这个复杂计算。而删除缓存则把计算延迟到下次读取时,逻辑更清晰。
4.写操作轻量:写操作只需要处理数据库,无需关心缓存数据的构建,性能更好。

先操作缓存还是先操作数据库

推荐先操作数据库,再操作缓存,出现不一致的概率远低于先操作缓存再更新数据库。

具体原因涉及多线程相关,可以自行了解。