Redis应用—2.在列表数据里的应用
大纲
1.基于数据库 + 缓存双写的分享贴功能
2.查询分享贴列表缓存时的延迟构建
3.分页列表惰性缓存方案如何节约内存
4.用户分享贴列表数据按页缓存实现精准过期控制
5.用户分享贴列表的分页缓存的异步更新
6.数据库与缓存的分页数据一致性方案
7.热门用户分享贴列表的分页缓存失效时消除并发线程串行等待锁的影响
8.总结
1.基于数据库 + 缓存双写的分享贴功能
@Transactional(rollbackFor = Exception.class)
@Override
//新增或修改分享贴
public SaveOrUpdateCookbookDTO saveOrUpdateCookbook(SaveOrUpdateCookbookRequest request) {//获取分布式锁,避免重复提交,保证幂等性String cookbookUpdateLockKey = RedisKeyConstants.COOKBOOK_UPDATE_LOCK_PREFIX + request.getId();Boolean lock = null;if (request.getId() != null && request.getId() > 0) {lock = redisLock.lock(cookbookUpdateLockKey);}if (lock != null && !lock) {log.info("操作分享帖获取锁失败,operator:{}", request.getOperator());throw new BaseBizException("新增/修改失败");}try {//构建分享帖数据CookbookDO cookbookDO = buildCookbookDO(request);//保存分享帖数据cookbookDAO.saveOrUpdate(cookbookDO);//构建分享帖里关联的商品数据,一个分享帖可以种草多个商品,需要保存该分享帖和多个商品的关联关系List<CookbookSkuRelationDO> cookbookSkuRelationDOS = buildCookbookSkuRelationDOS(cookbookDO, request);//保存分享帖关联的商品数据cookbookSkuRelationDAO.saveBatch(cookbookSkuRelationDOS);//更新分享贴数据的缓存updateCookbookCache(cookbookDO, request);//返回信息SaveOrUpdateCookbookDTO dto = SaveOrUpdateCookbookDTO.builder().success(true).build();return dto;} finally {if (lock != null) {redisLock.unlock(cookbookUpdateLockKey);}}
}//更新分享帖信息对应的缓存
private void updateCookbookCache(CookbookDO cookbookDO, SaveOrUpdateCookbookRequest request) {CookbookDTO cookbookDTO = buildCookbookDTO(cookbookDO, request.getSkuIds());String cookbookKey = RedisKeyConstants.COOKBOOK_PREFIX + cookbookDO.getId();//缓存分享贴具体内容,并设置缓存的随机过期时间为:2天加上随机几小时,避免缓存惊群 + 为筛选冷热数据做准备redisCache.set(cookbookKey, JsonUtil.object2Json(cookbookDTO), CacheSupport.generateCacheExpireSecond());//缓存某用户的分享贴数量,这个占用内存很少,可以无需设置过期时间,常驻内存String userCookbookCountKey = RedisKeyConstants.USER_COOKBOOK_COUNT_PREFIX + request.getUserId();redisCache.increment(userCookbookCountKey, 1);
}
2.查询分享贴列表缓存时的延迟构建
(1)功能需求介绍
一个用户发布完分享贴后,可能会分页查询发布出去的分享贴列表,而关注他的其他用户也可能会进入其主页分页查询其发布过的分享贴列表。所以可将用户的分享贴列表数据缓存起来,以应对可能的高并发查询。
(2)功能实现分析
如果要分页查询一个用户发布过的分享贴,就要用到Redis的List数据结构。但并不是在发布分享贴时,就把分享贴数据写入到Redis的List数据结构。
因为用户发布完分享贴后,不确定会不会频繁对其所有分享贴进行分页浏览。而且社区平台的分享贴会非常多,缓存这些列表信息在Redis里会很耗内存。根据不确定有多少用户会浏览分享贴列表 + 缓存分享贴列表信息很耗内存,所以就没必要每次发布分享贴时就立刻去构建这个分享贴列表缓存。
于是可以把构建分享贴列表缓存的时机,延迟到有用户来浏览分享贴列表时。比如某用户的分享贴列表被用户第一次浏览时,才去构建该分享贴列表缓存。
3.分页列表惰性缓存方案如何节约内存
基于Redis实现千万级用户的社区平台的缓存分页查询:发布分享贴数据入库时,是不会马上将数据也写入到Redis的一个List里的。
因为在面向千万级用户群体的社区平台中:每天都会有很多用户在发布分享贴,每个用户发布过的分享贴数据也会很多。而且有些用户的分享贴,可能根本就不会有其他用户进行关注和查询。举个例子,有个用户可能发布了1000个分享贴,每页显示20个,就有50页。该用户自己也未必一页一页去翻页查询,其他用户可能更不会看到某一页,所以也没必要在Redis里维护一个List来保存每个用户的所有分享列表数据。
因此数据需要被写入缓存的一个标准是:会经常被访问。所以,可以把经常被访问的数据驻留在Redis里,比如用户数据。
假设用户的分享贴列表在前端分页查询时,是不支持进行页码跳转的。只能点击上一页和下一页两个按钮,也就是只支持上翻和下翻,这就方便我们去构建惰性分页缓存了。
由于用户对分享贴列表进行分页查询时,只能按顺序一页一页地查,所以缓存分享贴列表数据的List也可以按顺序一页一页进行构建。
这样每个用户的分享贴列表在查询时才会构建缓存(延迟构建缓存),并且第一次查询到某一页时才会缓存某一页的数据(分页列表惰性缓存),从而可以节约大量的缓存内存。
这就是所谓的分页列表惰性缓存方案,下面是具体的实现代码初版:
//分页查询某用户的分享贴列表时才构建分享贴列表缓存,也就是延迟构建分享贴列表缓存
@Override
public PagingInfo<CookbookDTO> listCookbookInfo(CookbookQueryRequest request) {//先尝试从Redis获取分享贴分页列表String userCookbookKey = RedisKeyConstants.USER_COOKBOOK_PREFIX + request.getUserId();//这里使用了Redis的List类型数据结构//对List类型的数据进行分页查询可以使用lrange()方法,指定key、起始位置和每页数据量就可以List中的一页数据查出来List<String> cookbookDTOJsonString = redisCache.lRange(userCookbookKey, (request.getPageNo() - 1) * request.getPageSize(), request.getPageSize());List<CookbookDTO> cookbookDTOS = JsonUtil.listJson2ListObject(cookbookDTOJsonString , CookbookDTO.class);log.info("从缓存中获取分享贴列表信息, request: {}, value: {}", request, JsonUtil.object2Json(cookbookDTOS));if (!CollectionUtils.isEmpty(cookbookDTOS)) {Long size = redisCache.lsize(userCookbookKey);return PagingInfo.toResponse(cookbookDTOS, size, request.getPageNo(), request.getPageSize());}return listCookbookInfoFromDB(request);
}private PagingInfo<CookbookDTO> listCookbookInfoFromDB(CookbookQueryRequest request) {//从数据库中分页查询某用户的分享贴列表LambdaQueryWrapper<CookbookDO> queryWrapper = Wrappers.lambdaQuery();queryWrapper.eq(CookbookDO::getUserId, request.getUserId());int count = cookbookDAO.count(queryWrapper);List<Cookbook> cookbookDTOS = cookbookDAO.pageByUserId(request.getUserId(), request.getPageNo(), request.getPageSize());//这里基于Redis的List类型数据结构,写入时使用rpush()方法从右边添加,读取时使用lrange()方法从左边读取//下面会把用户发布的某一页分享贴列表数据,从右边开始按顺序全部追加到List数据结构里//假设前端限制了只能从第一页开始翻,并且不能进行跳转,只能向前和向后翻页//这就是分页列表惰性缓存的构建String userCookbookKey = RedisKeyConstants.USER_COOKBOOK_PREFIX + request.getUserId();redisCache.rPushAll(userCookbookKey, JsonUtil.listObject2ListJson(cookbookDTOS));PagingInfo<CookbookDTO> pagingInfo = PagingInfo.toResponse(cookbookDTOS, (long) count, request.getPageNo(), request.getPageSize());return pagingInfo;
}
4.用户分享贴列表数据按页缓存实现精准过期控制
由于不确定分享贴列表页的访问频率 + 缓存全部分享贴列表数据耗费内存,所以没有必要用户发布完分享贴就马上构建该用户的分享贴列表缓存,以及没有必要构建用户分享贴列表缓存时缓存其所有分享贴列表数据。
因此一般会采用延迟构建缓存 + 分页列表惰性缓存的方案:即当有用户分页浏览某用户的分享贴列表时,才会构建分享贴列表缓存,并且查询一页才添加一页的数据进分享贴列表缓存中。
但这种方案目前有两个缺点:
缺点一:前端界面没办法选页,因为List缓存里的数据只能按一页一页顺序添加。
缺点二:用户不断进行翻页,将List缓存数据构建完整后,没办法合理自动过期。如果指定List缓存的key过期时间,会影响分享贴列表前几页的频繁访问。如果不指定过期时间,那么很少访问的列表页就会常驻List缓存内存。
所以可以对一个用户的分享贴列表缓存进行拆分。按用户来缓存分享贴列表数据,变成按用户 + 每一页来缓存分享贴列表数据,这时就可以针对每一页列表数据精准设置过期时间。如果有的页列表一直没被访问,就让它自动过期即可。如果有的页列表频繁被访问,就自动去做过期时间延期。这样就解决了不能随便翻页的问题,以及实现了对页列表的缓存按照冷热数据进行精准过期控制。
下面对前面的代码进行改造,按页来进行缓存。
@Override
public PagingInfo<CookbookDTO> listCookbookInfo(CookbookQueryRequest request) {//尝试从缓存中查出某一页的数据String userCookbookPageKey = RedisKeyConstants.USER_COOKBOOK_PAGE_PREFIX + request.getUserId() + request.getPageNo();String cookbooksJSON = redisCache.get(userCookbookPageKey);if (cookbooksJSON != null && !"".equals(cookbooksJSON)) {String userCookbookCountKey = RedisKeyConstants.USER_COOKBOOK_COUNT_PREFIX + request.getUserId();Longsize = Long.valueOf(redisCache.get(userCookbookCountKey));List<CookbookDTO> cookbookDTOS = Json.parseObject(cookbooksJSON, List.class);//如果是热数据就进行缓存延期redisCache.expire(userCookbookPageKey, CacheSupport.generateCacheExpireSecond());return PagingInfo.toResponse(cookbookDTOS, size, request.getPageNo(), request.getPageSize());}return listCookbookInfoFromDB(request);
}private PagingInfo<CookbookDTO> listCookbookInfoFromDB(CookbookQueryRequest request) {LambdaQueryWrapper<CookbookDO> queryWrapper = Wrappers.lambdaQuery();queryWrapper.eq(CookbookDO::getUserId, request.getUserId());int count = cookbookDAO.count(queryWrapper);List<Cookbook> cookbookDTOS = cookbookDAO.pageByUserId(request.getUserId(), request.getPageNo(), request.getPageSize());String userCookbookPageKey = RedisKeyConstants.USER_COOKBOOK_PAGE_PREFIX + request.getUserId() + request.getPageNo();//设置随机过期时间,冷数据就会自动过期,而且避免缓存惊群redisCache.set(userCookbookPageKey, JsonUtil.object2Json(cookbookDTOS), CacheSupport.generateCacheExpireSecond());PagingInfo<CookbookDTO> pagingInfo = PagingInfo.toResponse(cookbookDTOS, (long) count, request.getPageNo(), request.getPageSize());return pagingInfo;
}
5.用户分享贴列表的分页缓存的异步更新
一.上述方案在用户只新增分享贴时能很好运行
即用户不停新增一些分享贴写入数据库后,就假设用户不更新数据了。然后进行分页查询其分享贴列表时,查第几页就构建第几页的缓存。并设置随机过期时间,让构建的分页缓存实现数据冷热分离。
二.还要考虑用户删改分享贴时对列表的影响
分享贴列表的分页缓存构建好之后,插入或者删除一些分享贴。可能会导致之前构建的那些分页缓存都失效,此时就需要重建分页缓存。重建分页缓存会比较耗时,耗时的操作就必须采取异步进行处理了。
于是进行如下改进:新增或修改分享贴时,需要发送消息到MQ,然后异步消费该MQ的消息,找出该分享贴对应的分页缓存进行重建。
@Service
public class CookbookServiceImpl implements CookbookService {...//新增或修改分享贴@Transactional(rollbackFor = Exception.class)@Overridepublic SaveOrUpdateCookbookDTO saveOrUpdateCookbook(SaveOrUpdateCookbookRequest request) {//获取分布式锁,避免重复提交,保证幂等性String cookbookUpdateLockKey = RedisKeyConstants.COOKBOOK_UPDATE_LOCK_PREFIX + request.getId();Boolean lock = null;if (request.getId() != null && request.getId() > 0) {lock = redisLock.lock(cookbookUpdateLockKey);}if (lock != null && !lock) {log.info("操作分享帖获取锁失败,operator:{}", request.getOperator());throw new BaseBizException("新增/修改失败");}try {//构建分享帖数据CookbookDO cookbookDO = buildCookbookDO(request);//保存分享帖数据cookbookDAO.saveOrUpdate(cookbookDO);//构建分享帖里关联的商品数据,一个分享帖可以种草多个商品,需要保存该分享帖和多个商品的关联关系List<CookbookSkuRelationDO> cookbookSkuRelationDOS = buildCookbookSkuRelationDOS(cookbookDO, request);//保存分享帖关联的商品数据cookbookSkuRelationDAO.saveBatch(cookbookSkuRelationDOS);//更新分享贴数据的缓存updateCookbookCache(cookbookDO, request);//发布分享帖数据已被更新的事件消息publishCookbookUpdatedEvent(cookbookDO);//返回信息SaveOrUpdateCookbookDTO dto = SaveOrUpdateCookbookDTO.builder().success(true).build();return dto;} finally {if (lock != null) {redisLock.unlock(cookbookUpdateLockKey);}}}//发布分享帖数据已被更新的事件消息private void publishCookbookUpdatedEvent(CookbookDO cookbookDO) {CookbookUpdateMessage message = CookbookUpdateMessage.builder().cookbookId(cookbookDO.getId()).userId(cookbookDO.getUserId()).build();//将更新消息发布到COOKBOOK_UPDATE_MESSAGE_TOPIC这个主题defaultProducer.sendMessage(RocketMqConstant.COOKBOOK_UPDATE_MESSAGE_TOPIC, JsonUtil.object2Json(message), "分享贴变更消息");}...
}@Configuration
public class ConsumerBeanConfig {@Autowiredprivate RocketMQProperties rocketMQProperties;@Bean("cookbookAsyncUpdateTopic")public DefaultMQPushConsumer receiveCartUpdateConsumer(CookbookUpdateListener cookbookUpdateListener) throws MQClientException {DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.COOKBOOK_DEFAULT_CONSUMER_GROUP);consumer.setNamesrvAddr(rocketMQProperties.getNameServer());consumer.subscribe(RocketMqConstant.COOKBOOK_UPDATE_MESSAGE_TOPIC, "*");consumer.registerMessageListener(cookbookUpdateListener);consumer.start();return consumer;}
}@Component
public class CookbookUpdateListener implements MessageListenerConcurrently {...//消费分享贴更新的消息@Overridepublic ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgList, ConsumeConcurrentlyContext context) {try {for (MessageExt messageExt : msgList) {log.info("执行某用户的分享贴列表缓存数据的更新逻辑,消息内容:{}", messageExt.getBody());String msg = new String(messageExt.getBody());CookbookUpdateMessage message = JsonUtil.json2Object(msg, CookbookUpdateMessage.class);Long userId = message.getUserId();//首先查询该用户的所有分享贴总数,并计算出总共多少分页String userCookbookCountKey = RedisKeyConstants.USER_COOKBOOK_COUNT_PREFIX + userId;Integer count = Integer.valueOf(redisCache.get(userCookbookCountKey));int pageNum = count / PAGE_SIZE + 1;//接下来对userId用户的分享贴列表的分页缓存进行逐一重建for (int pageNo = 1; pageNo <= pageNum; pageNo++) {String userCookbookPageKey = RedisKeyConstants.USER_COOKBOOK_PAGE_PREFIX + userId + ":" + pageNo;String cookbooksJson = redisCache.get(userCookbookPageKey);//如果不存在用户的某页的分享贴列表缓存,则无需处理,跳过即可if (cookbooksJson == null || "".equals(cookbooksJson)) {continue;}//如果存在某页数据,就需要对该页的列表缓存数据进行更新List<CookbookDTO> cookbooks = cookbookDAO.pageByUserId(userId, pageNo, PAGE_SIZE);redisCache.set(userCookbookPageKey, JsonUtil.object2Json(cookbooks), CacheSupport.generateCacheExpireSecond());}}} catch (Exception e) {//本次消费失败,下次重新消费log.error("consume error, 更新分享贴的消息消费失败", e);return ConsumeConcurrentlyStatus.RECONSUME_LATER;}log.info("更新分享贴的消息消费成功, result: {}", ConsumeConcurrentlyStatus.CONSUME_SUCCESS);return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}
}
6.数据库与缓存的分页数据一致性方案
和用户数据的情况一样,有三个线程在几乎并发执行,都处理到同一条分享贴列表分页缓存数据。线程A读取不到某分享贴列表数据的分页缓存,需要读库 + 写缓存。线程B正在执行更新相关分享贴的数据,需要写库 + 发消息。线程C正在消费更新分享贴时发出的MQ消息,需要读库 + 写缓存。
那么就可能会出现如下情况:线程A先完成读库获得旧值,正准备写缓存。接着线程B马上完成写库和发消息,紧接着线程C又很快消费到该消息并完成读库获得新值 + 写缓存。之后才轮到线程A执行写缓存,但是写的却是旧值,覆盖了新值。从而造成不一致。
所以需要对读缓存失败时要读库和消费消息重建缓存时要读库加同一把锁。
7.热门用户分享贴列表的分页缓存失效时消除并发线程串行等待锁的影响
和用户数据一样,有个用户发布的分享贴突然流量暴增成为热门数据。一开始大量的并发线程读缓存失败,需要准备读库+写缓存,出现缓存击穿。这时就需要处理将并发线程的"串行等待锁+读缓存"转换成"串行读缓存",这可以通过简单的设定尝试获取分布式时的超时时间来实现。
也就是当并发进来串行排队的线程获取分布式锁超时返回失败后,就让这些线程重新读缓存(实现"串行等待锁+读缓存"转"串行读缓存"),从而消除串行等待锁带来的性能影响。
注意:等待锁释放的并发线程在超时时间内成功获取到锁之后要进行双重检查,这样可以避免出现大量并发进来的线程又串行地重复去查库。
@Service
public class CookbookServiceImpl implements CookbookService {...private PagingInfo<CookbookDTO> listCookbookInfoFromDB(CookbookQueryRequest request) {String userCookbookPageLockKey = RedisKeyConstants.USER_COOKBOOK_PREFIX + request.getUserId() + request.getPageNo();boolean lock = false;try {//尝试加锁并且设置锁的超时时间//第一个拿到锁的线程在超时时间内处理完事情会释放锁,其他线程会继续竞争锁//而在这个超时时间里没有获得锁的线程会被挂起并进入队列进行串行等待//如果在这个超时时间外还获取不到锁,排队的线程就会被唤醒并返回falselock = redisLock.tryLock(userCookbookPageLockKey, USER_COOKBOOK_LOCK_TIMEOUT);} catch(InterruptedException e) {PagingInfo<CookbookDTO> page = listCookbookInfoFromCache(request);if (page != null) {return page;}log.error(e.getMessage(), e);}if (!lock) {//并发进来串行排队的线程获取分布式锁超时返回失败后,就重新读缓存(实现"串行等待锁+读缓存"转"串行读缓存")PagingInfo<CookbookDTO> page = listCookbookInfoFromCache(request);if (page != null) {return page;}log.info("缓存数据为空,从数据库查询用户分享贴列表时获取锁失败,userId:{}, pageNo:{}", request.getUserId(), request.getPageNo());throw new BaseBizException("查询失败");}try {//双重检查Double Check,避免超时时间内获取到锁的串行排队的并发线程,重复读数据库PagingInfo<CookbookDTO> page = listCookbookInfoFromCache(request);if (page != null) {return page;}LambdaQueryWrapper<CookbookDO> queryWrapper = Wrappers.lambdaQuery();queryWrapper.eq(CookbookDO::getUserId, request.getUserId());int count = cookbookDAO.count(queryWrapper);List<CookbookDTO> cookbookDTOS = cookbookDAO.pageByUserId(request.getUserId(), request.getPageNo(), request.getPageSize());//设置随机过期时间,冷数据就会自动过期,而且避免缓存惊群String userCookbookPageKey = RedisKeyConstants.USER_COOKBOOK_PAGE_PREFIX + request.getUserId() + ":" + request.getPageNo();redisCache.set(userCookbookPageKey, JsonUtil.object2Json(cookbookDTOS), CacheSupport.generateCacheExpireSecond());PagingInfo<CookbookDTO> pagingInfo = PagingInfo.toResponse(cookbookDTOS, (long) count, request.getPageNo(), request.getPageSize());return pagingInfo;} finally {redisLock.unlock(userCookbookPageLockKey);}}
}@Component
public class CookbookUpdateListener implements MessageListenerConcurrently {...@Overridepublic ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgList, ConsumeConcurrentlyContext context) {try {for (MessageExt messageExt : msgList) {log.info("执行某用户的分享贴列表缓存数据的更新逻辑,消息内容:{}", messageExt.getBody());String msg = new String(messageExt.getBody());CookbookUpdateMessage message = JsonUtil.json2Object(msg, CookbookUpdateMessage.class);Long userId = message.getUserId();//首先查询该用户的所有分享贴总数,并计算出总共多少分页String userCookbookCountKey = RedisKeyConstants.USER_COOKBOOK_COUNT_PREFIX + userId;Integer count = Integer.valueOf(redisCache.get(userCookbookCountKey));int pageNum = count / PAGE_SIZE + 1;//接下来对userId用户的分享贴列表的分页缓存进行逐一重建for (int pageNo = 1; pageNo <= pageNum; pageNo++) {String userCookbookPageKey = RedisKeyConstants.USER_COOKBOOK_PAGE_PREFIX + userId + ":" + pageNo;String cookbooksJson = redisCache.get(userCookbookPageKey);//如果不存在用户的某页的分享贴列表缓存,则无需处理,跳过即可if (cookbooksJson == null || "".equals(cookbooksJson)) {continue;}//阻塞式加分布式锁,避免数据库和缓存不一致String userCookbookPageLockKey = RedisKeyConstants.USER_COOKBOOK_PREFIX + userId + pageNo;redisLock.blockedLock(userCookbookPageLockKey);try {//如果存在某页数据,就需要对该页的列表缓存数据进行更新List<CookbookDTO> cookbooks = cookbookDAO.pageByUserId(userId, pageNo, PAGE_SIZE);redisCache.set(userCookbookPageKey, JsonUtil.object2Json(cookbooks), CacheSupport.generateCacheExpireSecond());} finally {redisLock.unlock(userCookbookPageLockKey);}}}} catch (Exception e) {//本次消费失败,下次重新消费log.error("consume error, 更新分享贴的消息消费失败", e);return ConsumeConcurrentlyStatus.RECONSUME_LATER;}log.info("更新分享贴的消息消费成功, result: {}", ConsumeConcurrentlyStatus.CONSUME_SUCCESS);return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}
}@Data
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
public class RedisConfig {...@Bean@ConditionalOnClass(RedissonClient.class)public RedisLock redisLock(RedissonClient redissonClient) {return new RedisLock(redissonClient);}
}public class RedisLock {...//阻塞式加锁,获取不到锁就阻塞直到获得锁才返回public boolean blockedLock(String key) {RLock rLock = redissonClient.getLock(key);rLock.lock();return true;}//tryLock()没timeout参数就是非阻塞式加锁//tryLock()有timeout参数就是最多阻塞timeout时间//即在timeout时间内,能获取到就返回true,不能获取到就阻塞等待,如果超出timeout还获取不到就返回falsepublic boolean tryLock(String key, long timeout) throws InterruptedException {RLock rLock = redissonClient.getLock(key);return rLock.tryLock(timeout, TimeUnit.MILLISECONDS);}
}
8.总结
相关文章:
Redis应用—2.在列表数据里的应用
大纲 1.基于数据库 缓存双写的分享贴功能 2.查询分享贴列表缓存时的延迟构建 3.分页列表惰性缓存方案如何节约内存 4.用户分享贴列表数据按页缓存实现精准过期控制 5.用户分享贴列表的分页缓存的异步更新 6.数据库与缓存的分页数据一致性方案 7.热门用户分享贴列表的分…...
【Linux基础】基本开发工具的使用
目录 一、编译器——gcc/g的使用 gcc/g的安装 gcc的安装: g的安装: gcc/g的基本使用 gcc的使用 g的使用 动态链接与静态链接 程序的翻译过程 1. 一个C/C程序的构建过程,程序从源代码到可执行文件必须经历四个阶段 2. 理解选项的含…...
C++ 中面向对象编程如何实现动态绑定?
在 C 中,面向对象编程的一个重要特性就是动态绑定。动态绑定允许在程序运行时根据对象的实际类型来决定调用哪个函数,这为程序的灵活性和可扩展性提供了强大的支持。本文将详细介绍 C 中面向对象编程如何实现动态绑定。 一、静态绑定与动态绑定的概念 静…...
电源芯片的SYNC引脚
-----本文简介----- 主要内容包括: ① 电源芯片的SYNC引脚 ----- 正文 ----- 先赞↓后看,养成习惯! 1. SYNC引脚是什么? 电源芯片里面的SYNC引脚是 Synchronization clock in,意思是同步时钟输入。 2. SYNC引脚的作用…...
安卓报错Switch Maven repository ‘maven‘....解决办法
例如:Switch Maven repository ‘maven(http://developer.huawei.com/repo/)’ to redirect to a secure protocol 在库链接上方添加配置代码:allowInsecureProtocol true...
935. 骑士拨号器
935. 骑士拨号器 题目链接:935. 骑士拨号器 代码如下: class Solution { public:int knightDialer(int n) {if (n 1){return 10;}long long res 0;for (int j 0; j < 10; j){res dfs(n - 1, j);}return res % MOD;}int dfs(int i, int j){if (…...
linux下的posix信号量
目录 引言 信号量背景知识 PV操作 信号量接口 基于环形队列的PC模型 代码实现 demo模型 具体实现 引言 在多线程编程领域,同步机制是确保数据一致性和避免竞态条件的关键技术。Linux操作系统作为开源软件的杰出代表,提供了多种同步原语…...
【JavaWeb后端学习笔记】Spring框架下的Bean管理
Bean 1、Bean的获取2、Bean的作用域3、第三方Bean 1、Bean的获取 默认情况下,Spring项目启动时,会把Bean创建好交给IOC容器管理。当需要使用时,通过Autowired注解注入或者通过构造方法注入即可。 除此之外还可以通过Spring提供的Applicatio…...
如何在 ASP.NET Core 3.1 应用程序中使用 Log4Net
介绍 日志记录是应用程序的核心。它对于调试和故障排除以及应用程序的流畅性非常重要。 借助日志记录,我们可以对本地系统进行端到端的可视性,而对于基于云的系统,我们只能提供一小部分可视性。您可以将日志写入磁盘或数据库中的文件…...
Photoshop提示错误弹窗dll缺失是什么原因?要怎么解决?
Photoshop提示错误弹窗“DLL缺失”:原因分析与解决方案 在创意设计与图像处理领域,Photoshop无疑是众多专业人士和爱好者的首选工具。然而,在使用Photoshop的过程中,有时会遇到一些令人头疼的问题,比如突然弹出的错误…...
mall-admin-web开源项目搭建教程(图文)
本章教程,介绍如何在本地部署运行mall-admin-web这个开源项目。 开源地址:https://gitee.com/macrozheng/mall-admin-web mall-admin-web是一个电商后台管理系统的前端项目,基于Vue+Element实现。主要包括商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计…...
nginx做为文件服务器
docker-compose 创建nginx version: 3services:nginx-web:image: nginx:1.23.4container_name: nginx-webenvironment:# 时区上海TZ: Asia/Shanghaiports:- "88:80"- "443:443"volumes:# 证书映射- /home/dockerdata/nginx/cert:/etc/nginx/cert# 配置文件…...
加速合并,音频与字幕的探讨
因上一节。合并时速度太慢了。显卡没用上。所以想快一点。1分钟的视频用了5分钟。 在合并视频时,进度条中的 now=None 通常表示当前处理的时间点没有被正确记录或显示。这可能是由于 moviepy 的内部实现细节或配置问题。为了加快视频合并速度并利用 GPU 加速,可以采取以下措…...
(3)spring security - 认识PasswordEncoder
目录 1.简介1.1.简单了解认证流程 2.密码验证3.PasswordEncoder的内置实现4.小结 目标: 简单了解认证的流程简单认识spring security中的Password Encoder 1.简介 还是以这幅图为基础,认识Password Encoder到底是什么? 1.1.简单了解认证流程…...
React 入门:JSX语法详解
简介 React是一个用于构建用户界面的JavaScript库,它引入了JSX语法,使得你可以在JavaScript代码中编写类似HTML的结构。JSX在编译后会被转换成合法的JavaScript对象。 JSX基础 JSX是一种看起来像HTML的JavaScript语法扩展。它并不直接被浏览器执行&am…...
Pandas常见函数
Pandas 是 Python 中用于数据分析和处理的强大工具库。以下是 Pandas 中一些常见的函数和方法,按用途分类总结: 1. 数据创建 pd.Series(data, index):创建一维的序列对象。pd.DataFrame(data, index, columns):创建二维的DataFra…...
【笔试】亚马逊
亚马逊的笔试题目有两道,一共70分钟 1.给一个数组代表每轮损失的血量power,另外一个变量是盾牌armor,可以选择任意一轮使用这个盾牌,可以抵挡min(power[i],armor)的攻击,请问最小血量是多少能够…...
【力扣算法】234.回文链表
快慢指针:一个指针走两步,一个指针走一步,当快指针走到链表末尾时,慢指针走到中间位置。 逆转链表:根据指针位置分成两个表,逆转第二个表。 按序判断就可以,如果是相同就是回文,反之…...
vue3-tp8-Element:对话框实现
效果 参考框架 Dialog 对话框 | Element Plus 具体实现 一、建立view页面 /src/views/TestView.vue 二、将路径写入路由 /src/router/index.js import { createRouter, createWebHistory } from vue-router import HomeView from ../views/HomeView.vueconst router create…...
35、Firefly_rk3399 同步互斥
文章目录 1、简述问题2、原子操作(atomic_ops )指令解析: 3、锁函数说明3.1、自旋锁API例子 3.2、信号量(semaphore)API例子 3.3、互斥量/锁API例子 3.4、信号量和互斥锁的区别 4、锁的内核实现4.1、自旋锁(…...
Docker-Dockerfile、registry
Dockerfile 一、概述 1、commit的局限 很容易制作简单的镜像,但碰到复杂的情况就十分不方便,例如碰到下面的情况: 需要设置默认的启动命令需要设置环境变量需要指定镜像开放某些特定的端口 2、Dockerfile是什么 Dockerfile是一种更强大的镜…...
chattts生成的音频与字幕修改完善,每段字幕对应不同颜色的视频,准备下一步插入视频。
上一节中,实现了先生成一个固定背景的与音频长度一致的视频,然后插入字幕。再合并成一个视频的方法。 但是:这样有点单了,所以: 1.根据字幕的长度先生成视频片断 2.在片段上加上字幕。 3.合并所有片断,…...
8、笔记本品牌分类介绍:LG - 计算机硬件品牌系列文章
LG笔记本品牌以其高性能和先进技术而闻名,提供多种型号以满足不同用户的需求。 LG笔记本产品线包括多种类型,以满足不同用户的需求。其中,LG Gram Pro系列以其超薄设计和高性能配置受到关注。该系列笔记本采用16:10的OLED显示屏&…...
在 Vue 2 中隐藏页面元素的方法
目录 在 Vue 2 中隐藏页面元素的方法 引言 1. 使用 v-if 指令 2. 使用 v-show 指令 3. 使用自定义类名与 v-bind:class 4. 使用内联样式与 v-bind:style 5. 使用组件的 keep-alive 和条件渲染 在 Vue 2 中隐藏页面元素的方法 引言 在开发 Web 应用时,我们经…...
基于springboot+vue的高校校园交友交流平台设计和实现
文章目录 系统功能部分实现截图 前台模块实现管理员模块实现 项目相关文件架构设计 MVC的设计模式基于B/S的架构技术栈 具体功能模块设计系统需求分析 可行性分析 系统测试为什么我? 关于我项目开发案例我自己的网站 源码获取: 系统功能 校园交友平台…...
Redis是什么?Redis和MongoDB的区别在那里?
Redis介绍 Redis(Remote Dictionary Server)是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。以下是关于Redis的详细介绍: 一、数据结构支持 字符串(String) 这是Redis最…...
《开源时间序列数据:探索与应用》
《开源时间序列数据:探索与应用》 一、开源时间序列数据概述二、热门的开源时间序列数据库1. InfluxDB2. TimescaleDB3. Prometheus4. OpenTSDB5. Graphite6. Druid 三、开源时间序列数据的应用场景1. 物联网领域2. 金融领域3. 运维监控领域4. 能源领域 四、开源时间…...
Java后端面试场景题汇总
1.50 亿数据如何去重&排序? 如此大的数据集进行去重(例如50亿数据条目),我们需要考虑内存和存储空间的限制,同时还需要有一个高效的算法。一般来说,这样的数据量无法直接载入内存进行处理,因此需要采用磁盘存储和分布式处理的技术。主要有以下几种思路: 外部排序…...
方法引用和lambda表达式的奥妙
方法引用替代Lambda表达式 什么情况可以使用方法引用替代lambda表达式? 下面代码中两处使用了lambda表达式,一个是filter内,一个是forEach内。其中,forEach内的lambda表达式可以被方法引用替代,但是filter内的lambda…...
AI 智能名片 S2B2C 商城小程序在社群团购运营中的作用与价值
摘要:本文深入探讨了 AI 智能名片 S2B2C 商城小程序在社群团购运营中的重要作用。随着社群团购的兴起,如何有效运营成为关键问题。AI 智能名片 S2B2C 商城小程序凭借其独特功能,能够在促进消费者互动、提升产品传播效果、影响购买决策以及实现…...
设计模式の建造者适配器桥接模式
文章目录 前言一、建造者模式二、适配器模式2.1、对象适配器2.2、接口适配器 三、桥接模式 前言 本篇是关于设计模式中建造者模式、适配器模式(3种)、以及桥接模式的笔记。 一、建造者模式 建造者模式是属于创建型设计模式,通过一步步构建一个…...
.net framework手动升级到.net core注意点
因为项目原因,还使用着比较原始的 .NETFramework框架,但因为某种原因,暂时不让升级到.NET 6。为了能够解锁更多 VisualStudio2022的功能,尝试手动修改 csproj文件。 这个过程中,也会遇到不少坑,再次做个记…...
排队论、负载均衡和任务调度关系
目录 排队论、负载均衡和任务调度关系 一、排队论 二、负载均衡 三、任务调度 四、总结 排队论、负载均衡和任务调度关系 排队论为负载均衡和任务调度提供了数学理论和方法支持 排队论、负载均衡和任务调度是三个相关但不同的概念。以下是对这三个概念的详细解释和它们之…...
【C++图论】1042. 不邻接植花|1712
本文涉及知识点 C图论 LeetCode1042. 不邻接植花 有 n 个花园,按从 1 到 n 标记。另有数组 paths ,其中 paths[i] [xi, yi] 描述了花园 xi 到花园 yi 的双向路径。在每个花园中,你打算种下四种花之一。 另外,所有花园 最多 有…...
AI开源南京分享会回顾录
AI 开源南京分享会,已于2024年11月30日下午在国浩律师(南京)事务所5楼会议厅成功举办。此次活动由 KCC南京、PowerData、RISC-Verse 联合主办,国浩律师(南京)事务所协办。 活动以“开源视角的 AI 对话”为主…...
Java版-图论-最短路-Floyd算法
实现描述 网络延迟时间示例 根据上面提示,可以计算出,最大有100个点,最大耗时为100*wi,即最大的耗时为10000,任何耗时计算出来超过这个值可以理解为不可达了;从而得出实现代码里面的: int maxTime 10005…...
ChatGPT大模型 创作高质量文案的使用教程和案例
引言 随着人工智能技术的飞速发展,大语言模型如 ChatGPT 在创作文案、生成内容方面展现出了强大的能力。无论是个人用户还是企业用户,都可以利用 ChatGPT 提高工作效率、激发创意、甚至解决实际问题。本文将详细介绍 ChatGPT 如何帮助创作各类高质量文案,并通过具体案例展示…...
SQL注入及解决
SQL注入是一种常见的网络攻击方式,攻击者通过在输入字段中插入恶意的SQL代码,诱使应用程序执行攻击者构造的SQL语句,从而达到非法获取数据、篡改数据或执行恶意操作的目的。 以下是SQL注入的主要原理总结: 1. 核心原理 SQL注入…...
uni-app多环境配置动态修改
前言 这篇文章主要介绍uniapp在Hbuilderx 中,通过工程化,区分不同环境、动态修改小程序appid以及自定义条件编译,解决代码发布和运行时手动切换问题。 背景 当我们使用uniapp开发同一个项目发布不同的环境二级路径不同时,这时候…...
EasyPlayer.js播放器如何在iOS上实现低延时直播?
随着流媒体技术的迅速发展,H5流媒体播放器已成为现代网络视频播放的重要工具。其中,EasyPlayer.js播放器作为一款功能强大的H5播放器,凭借其全面的协议支持、多种解码方式以及跨平台兼容性,赢得了广泛的关注和应用。 那么要在iOS上…...
mHand Pro动捕数据手套在人形机器人领域的具体运用
mHandPro是一款高精度的动作捕捉数据手套,可应用于动作捕捉与VR交互等领域,配套”mHand Studio“引擎,可实时捕捉真人手部位姿及运动轨迹数据,将数据导出还可以用于人形机器人的训练加速高精度机器人操作技能的培训进程。 高精度动…...
【css常用动画总结01】
一、效果如下: 屏幕录制2024-11-27 17.28.30 二、css常用动画代码: .flex-box{position: relative; } .animation-all {display: flex;p{margin:0;font-size: 12px;}.animate-test1 {width: 102.4px;height: 102.4px;background: url(../assets/images/…...
从入门到精通:系统化棋牌游戏开发全流程教程
棋牌游戏开发需要丰富的技术知识和全面的规划,从开发环境搭建到实际功能实现,步骤清晰且逻辑严谨。以下是完整教程,涵盖了每个关键环节,并提供相关软件的具体下载地址,助力开发者高效完成棋牌游戏项目。 一、开发环境准…...
MyBatis 框架学习与实践
引言 MyBatis 是一个流行的 Java 持久层框架,它提供了简单的方法来处理数据库中的数据。本文将结合笔记和图片内容,详细讲解 MyBatis 的使用,包括配置、注解、优化技巧以及如何处理特殊字符和参数。 1. MyBatis 基础 1.1 引入依赖 首先&a…...
数据可视化的Python实现
一、GDELT介绍 GDELT ( www.gdeltproject.org ) 每时每刻监控着每个国家的几乎每个角落的 100 多种语言的新闻媒体 -- 印刷的、广播的和web 形式的,识别人员、位置、组织、数量、主题、数据源、情绪、报价、图片和每秒都在推动全球社会的事件,GDELT 为全…...
微信小程序实现联动删除输入验证码框
以下是json代码 {"component": true,"usingComponents": {} }以下是wxml代码 <van-popup show"{{ show }}" bind:close"onClose" custom-class"extract"><image src"../../images/extract/icon1.png"…...
C语言程序设计P6-1【应用指针进行程序设计 | 第一节】——知识要点:指针的概念、定义和运算、指针变量作函数的参数
知识要点:指针的概念、定义和运算、指针变量作函数的参数 视频: 目录 一、任务分析 二、必备知识与理论 三、任务实施 一、任务分析 输入两个整数,按大小顺序输出,要求用函数处理,而且用指针类型的数据作函数参数…...
C++编程: 基于cpp-httplib和nlohmann/json实现简单的HTTP Server
文章目录 0. 引言1. 完整实例代码2. 关键实现3. 运行与测试 0. 引言 本文基于 cpp-httplib 和 nlohmann/json 实现简单的 HTTPS Server 实例代码,这两个库均是head-only的。 1. 完整实例代码 如下实例程序修改自example/server.cc #include <httplib.h>#i…...
多模态大模型(二)——用Transformer Encoder和Decoder的方法(BLIP、CoCa、BEiTv3)
文章目录 BLIP: Bootstrapping Language-Image Pre-training for Unified Vision-Language Understanding and Generation 理解、生成我都要,一个很有效的、根据图片生成caption的工具1. BLIP的研究动机2. BLIP的模型结构3. CapFilt Model4. BLIP的训练过程 CoCa: C…...
SpringBoot快速入门
SpringBoot 文章目录 SpringBoot1. Spring Boot 概念2. Spring 使用痛点3. Spring Boot功能4. 快速搭建5. 起步依赖原理6. SpringBoot 配置6.1 配置文件6.2 YAML介绍6.3 YAML语法6.4 YAML数据6.5 YAML参数引用 7.配置数据读取7.1 Value("${}")7.2 Environment7.3 Con…...