"小明, 多系统的 session 共享, 怎么处理?""Redis 缓存啊!"" 小明, 我想实现一个简单的消息队列?""Redis 缓存啊!"" 小明, 分布式锁这玩意有什么方案?""Redis 缓存啊!"" 小明, 公司系统响应如蜗牛, 咋整?""Redis 缓存啊!"
本着研究的精神, 我们来分析下小明的第四个问题.
准备:
- Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/
- Dubbo2.7.5/Druid1.2.21/Zookeeper3.5.5/Mysql8.0.11/vue2.5/Redis3.2
难度: 新手 -- 战士 -- 老兵 -- 大师
目标:
Spring 优雅整合 Redis 做数据库缓存
步骤:
为了遇见各种问题, 同时保持时效性, 我尽量使用最新的软件版本. 源码地址: https://github.com/xiexiaobiao/vehicle-shop-admin
1 先说结论
Redis 缓存不是金弹, 若系统 DB 毫无压力, 系统性能瓶颈不在 DB 上, 不建议强加缓存层!
增加业务复杂度: 同一缓存必须被全部相关方法所覆盖, 如订单缓存, 只要涉及到订单数据更新的方法都要进行缓存逻辑处理.
同时, KV 存储时, 因各方法返回的类型不同, 这样就需要多个缓存池, 但各方法后台的数据又存在关联, 往往导致一个方法需
要处理关联的多个缓存, 从而形成网状处理逻辑.
2. 存在并发问题: 缓存没有锁机制, B 线程进行 DB 更新, 同时 A 线程请求数据, 缓存中存在即返回, 但 B 线程还未更新到缓存, 导
致缓存与 DB 不一致; 或者 A 线程 B 线程都进行 DB 更新, 但写入缓存的顺序发生颠倒, 也会导致缓存与 DB 不一致, 请看官君想想如何解决;
3. 内存消耗: 小数据量可直接全部进内存, 但海量数据不可能全部直接进入 Redis, 机器吃不消! 可考虑只缓存 DB 数据索引, 然后配合
"布隆过滤器" 拦截无效请求, 有效请求再去 DB 查询;
4. 缓存位置: 缓存注解的方法, 执行时序上应尽量靠近 DB, 远离前端, 如放 dao 层, 请看官君思考下为啥.
适用场景: 1. 确认 DB 为系统性能瓶颈, 2. 数据内容稳定, 低频更新, 高频查询, 如历史订单数据; 3. 热点数据, 如新上市商品;
2 步骤
2.1 原理
这里我说的是注解模式, 有四个注解, SpringCache 缓存原理即注解 + 拦截器 org.springframework.cache.interceptor.CacheInterceptor 对方法进行拦截处理:
@Cacheable: 可标记在类或方法上. 标记在类上则缓存该类所有方法的返回值. 请求方法时, 先在缓存进行 key 匹配, 存在则直接取缓存数据并返回. 主要参数表:
@CacheEvict: 从缓存中移除相应数据. 主要参数表:
@CachePut: 方法支持缓存功能. 与 @Cacheable 不同的是使用 @CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,
而是每次都会执行该方法, 并将执行结果以键值对的形式存入指定的缓存中. 主要参数表:
@Caching: 多个 Cache 注解组合使用, 比如新增用户时, 同时要删除其他缓存, 并更新用户信息缓存, 即以上三个注解的集合.
2.2 编码
项目有五个微服务, 我仅改造了 customer 服务模块:
引入依赖, build.gradle 文件:
Redis 配置项, resources/config/application-dev.YAML 文件:
文件: com.biao.shop.customer.conf.RedisConf
- @Configuration
- @EnableCaching
- public class RedisConf {
- @Bean
- public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
- return RedisCacheManager.create(redisConnectionFactory);
- }
- @Bean
- public CacheManager cacheManager() {
- // configure and return an implementation of Spring's CacheManager SPI
- SimpleCacheManager cacheManager = new SimpleCacheManager();
- cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));
- return cacheManager;
- }
- @Bean
- public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
- RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
- redisTemplate.setConnectionFactory(factory);
- // 设置 key 的序列化器
- redisTemplate.setKeySerializer(new StringRedisSerializer());
- // 设置 value 的序列化器, 使用 Jackson 2, 将对象序列化为 JSON
- Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
- new Jackson2JsonRedisSerializer(Object.class);
- // JSON 转对象类, 不设置, 默认的会将 JSON 转成 hashmap
- ObjectMapper mapper = new ObjectMapper();
- mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
- mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
- jackson2JsonRedisSerializer.setObjectMapper(mapper);
- return redisTemplate;
- }
- }
以上代码解析: 1. 声明缓存管理器 CacheManager, 会创建一个切面 (aspect) 并触发 Spring 缓存注解的切点, 根据类或者方法所使用的注解以及缓存的状态,
这个切面会从缓存中获取数据, 将数据添加到缓存之中或者从缓存中移除某个值 2. RedisTemplate 即为 Redis 连接器, 实际上即为 jedis 客户端.
文件: com.biao.shop.customer.impl.ShopClientServiceImpl
- @org.springframework.stereotype.Service
- @Slf4j
- public class ShopClientServiceImpl extends ServiceImpl<ShopClientDao, ShopClientEntity> implements ShopClientService {
- private final Logger logger = LoggerFactory.getLogger(ShopClientServiceImpl.class);
- private ShopClientDao shopClientDao;
- @Autowired
- public ShopClientServiceImpl(ShopClientDao shopClientDao){
- this.shopClientDao = shopClientDao;
- }
- @Override
- public String getMaxClientUuId() {
- return shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>()
- .isNotNull(ShopClientEntity::getClientUuid).orderByDesc(ShopClientEntity::getClientUuid))
- .stream().limit(1).collect(Collectors.toList())
- .get(0).getClientUuid();
- }
- @Override
- @Caching(put = @CachePut(cacheNames = {"shopClient"},key = "#root.args[0].clientUuid"),
- evict = @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true))
- public int createClient(ShopClientEntity clientEntity) {
- clientEntity.setGenerateDate(LocalDateTime.now());
- return shopClientDao.insert(clientEntity);
- }
- /** */
- @Override
- @CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
- public int deleteBatchById(Collection<Integer> ids) {
- logger.info("deleteBatchById 删除 Redis 缓存");
- return shopClientDao.deleteBatchIds(ids);
- }
- @Override
- @CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
- public int deleteById(int id) {
- logger.info("deleteById 删除 Redis 缓存");
- return shopClientDao.deleteById(id);
- }
- @Override
- @Caching(evict = {@CacheEvict(cacheNames = "shopClient",key = "#root.args[0]"),
- @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)})
- public int deleteByUUid(String uuid) {
- logger.info("deleteByUUid 删除 Redis 缓存");
- QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
- qw.eq(true,"uuid",uuid);
- return shopClientDao.delete(qw);
- }
- @Override
- @Caching(put = @CachePut(cacheNames = "shopClient",key = "#root.args[0].clientUuid"),
- evict = @CacheEvict(cacheNames = {"shopClientPage","shopClientPlateList","shopClientList"},allEntries = true))
- public int updateClient(ShopClientEntity clientEntity) {
- logger.info("updateClient 更新 Redis 缓存");
- clientEntity.setModifyDate(LocalDateTime.now());
- return shopClientDao.updateById(clientEntity);
- }
- @Override
- @CacheEvict(cacheNames = {"shopClient","shopClientPage","shopClientPlateList","shopClientList"},allEntries = true)
- public int addPoint(String uuid,int pointToAdd) {
- ShopClientEntity clientEntity = this.queryByUuId(uuid);
- log.debug(clientEntity.toString());
- clientEntity.setPoint(Objects.isNull(clientEntity.getPoint()) ? 0 : clientEntity.getPoint() + pointToAdd);
- return shopClientDao.updateById(clientEntity);
- }
- @Override
- @Cacheable(cacheNames = "shopClient",key = "#root.args[0]")
- public ShopClientEntity queryByUuId(String uuid) {
- logger.info("queryByUuId 未使用 Redis 缓存");
- QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
- qw.eq(true,"client_uuid",uuid);
- return shopClientDao.selectOne(qw);
- }
- @Override
- @Cacheable(cacheNames = "shopClientById",key = "#root.args[0]")
- public ShopClientEntity queryById(int id) {
- logger.info("queryById 未使用 Redis 缓存");
- return shopClientDao.selectById(id);
- }
- @Override
- @Cacheable(cacheNames = "shopClientPage")
- public PageInfo<ShopClientEntity> listClient(Integer current, Integer size, String clientUuid, String name,
- String vehiclePlate, String phone) {
- logger.info("listClient 未使用 Redis 缓存");
- QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
- Map<String,Object> map = new HashMap<>(4);
- map.put("client_uuid",clientUuid);
- map.put("vehicle_plate",vehiclePlate);
- map.put("phone",phone);
- // "name" 模糊匹配
- boolean valid = Objects.isNull(name);
- qw.allEq(true,map,false).like(!valid,"client_name",name);
- PageHelper.startPage(current,size);
- List<ShopClientEntity> clientEntities = shopClientDao.selectList(qw);
- return PageInfo.of(clientEntities);
- }
- // java Stream
- @Override
- @Cacheable(cacheNames = "shopClientPlateList")
- public List<String> listPlate() {
- logger.info("listPlate 未使用 Redis 缓存");
- List<ShopClientEntity> clientEntities =
- shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>().isNotNull(ShopClientEntity::getVehiclePlate));
- return clientEntities.stream().map(ShopClientEntity::getVehiclePlate).collect(Collectors.toList());
- }
- @Override
- @Cacheable(cacheNames = "shopClientList",key = "#root.args[0].toString()")
- public List<ShopClientEntity> listByClientDto(ClientQueryDTO clientQueryDTO) {
- logger.info("listByClientDto 未使用 Redis 缓存");
- QueryWrapper<ShopClientEntity> qw = new QueryWrapper<>();
- boolean phoneFlag = Objects.isNull(clientQueryDTO.getPhone());
- boolean clientNameFlag = Objects.isNull(clientQueryDTO.getClientName());
- boolean vehicleSeriesFlag = Objects.isNull(clientQueryDTO.getVehicleSeries());
- boolean vehiclePlateFlag = Objects.isNull(clientQueryDTO.getVehiclePlate());
- // 如有 null 的条件直接不参与查询
- qw.eq(!phoneFlag,"phone",clientQueryDTO.getPhone())
- .like(!clientNameFlag,"client_name",clientQueryDTO.getClientName())
- .like(!vehicleSeriesFlag,"vehicle_plate",clientQueryDTO.getVehiclePlate())
- .like(!vehiclePlateFlag,"vehicle_series",clientQueryDTO.getVehicleSeries());
- return shopClientDao.selectList(qw);
- }
- }
以上代码解析:
1. 因方法返回类型不同, 故建立了 5 个缓存 2. 使用 SpEL 表达式 #root.args[0]取得方法第一个参数, 使用 #result 取得返回对象,
用于构造 key 3. 对于 @Cacheable 不能使用 #result 返回对象做 key 值, 如 queryById(int id)方法, 会导致 NPE,, 因为此注解将在方法执行前先
进入缓存匹配, 而 #result 则是在方法执行后计算 4. @Caching 注解可一次集合多个注解, 如 deleteByUUid(String uuid)方法, 删除一个用户记录,
需同时进行更新 shopClient, 并清空其他几个缓存.
2.3 测试
运行起来整个项目, 启动顺序: souladmin -> soulbootstrap -> zookeeper -> authority -> customer -> stock -> order -> business -> vue 前端 ,
进入后端管理页: 按页浏览客户信息, 分别点击页签:
可以看到缓存 shopClientPage 缓存了 4 项数据, key 值即为方法的参数组合, 再去点击页签, 则系统后台无 DB 请求记录输出, 说明直接使用了缓存:
编辑客户信息, 我随意打开了两个:
可以看到缓存 shopClientById 增加了两个对象, 再去点击编辑, 则系统后台无 DB 查询记录输出, 说明直接使用了缓存:
按条件查询客户:
可以看到缓存 shopClientPage 增加一项, 因为 key 值不一样, 故独立为一项缓存数据, 多次点查询, 则系统后台无 DB 查询 SQL 输出, 说明直接使用了缓存:
新增客户:
可以看到 shopClientPage 缓存将会被清空, 同时增加一个 shopClient 缓存的对象, 即同时进行了多个缓存池操作:
问题解答:
前面说到的两个问题:
1. 多线程问题, 可配合 DB 事务机制, 进行缓存延时双删, 每次 DB 更新前, 先删除缓存中对象, 更新后, 再去删除一次缓存中对象,
2. 缓存方法位置问题, 按照前端到后端的 "倒金字塔模型", 越靠近前端, 缓存数据对象被其他业务逻辑更新的可能性越大, 靠近 DB, 能尽量保证每次 DB 的更新都能被缓存逻辑感知.
全文完!
我的其他文章:
1SOFARPC 模式下的 Consul 注册中心
2 八种控制线程顺序的方法
3 移动应用 App 购物车(店铺系列二)
4 H5 开发移动应用 App(店铺系列一)
5 阿里云平台 OSS 对象存储
只写原创, 敬请关注
来源: https://www.cnblogs.com/xxbiao/p/12593525.html