通过redis实现高性能计费处理逻辑

计费服务一般都是跟资金相关,所以它在系统中是非常核心的模块,要保证服务的高可用、事务一致性、高性能。服务高可用需要集群部署,要保证事务一致性可以通过数据库来实现,但是只通过数据库却很难实现高性能的系统。
这篇文章通过使用 redis+消息队列+数据库 实现一套高性能的计费服务,接口请求扣费分为两步: 首先 使用redis扣减用户余额,扣费成功后将数据发送到消息队列,整个扣费的第一步就完成了,这时就可以返回成功给调用端; 第二步 数据持久化,由另外一个服务订阅消息队列,批量消费队列中的消息实现数据库持久化。
上面两个步骤完成后,整个扣费过程就结束了,这里最主要的就是扣费的第一步,业务流程图如下:
扣费流程图
通过上面的流程图可以知道,整个扣费的第一步主要分为两部分:redis扣费+kafka消费;单独使用redis扣费可以通过lua脚本实现事务,kafka消息队列也可以实现事务,但是如何将这两步操作实现事务就要通过编程来保障。
实现上面的处理流程打算使用一个demo项目来演示。项目使用 SpringBoot+redis+kafka+MySQL 的架构。

首先在MySQL数据库创建用到的几个表:

-- 用户信息
CREATE TABLE `t_user`  (`id` int NOT NULL AUTO_INCREMENT,`user_name` varchar(128) DEFAULT NULL,`create_time` datetime NULL DEFAULT NULL,`modify_time` datetime NULL DEFAULT NULL,`api_open` tinyint NULL DEFAULT 0,`deduct_type` varchar(16) DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `user_name`(`user_name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- 账户信息
CREATE TABLE `t_account`  (`user_id` int NOT NULL,`balance` decimal(20, 2) NULL DEFAULT NULL,`modify_time` datetime NULL DEFAULT NULL,`create_time` datetime NULL DEFAULT NULL,`version` bigint NULL DEFAULT 1,`threshold` decimal(20, 2) NULL DEFAULT NULL,PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;-- 订单信息
CREATE TABLE `t_order`  (`id` bigint NOT NULL,`user_id` int NULL DEFAULT NULL,`amount` decimal(20, 2) NULL DEFAULT NULL,`balance` decimal(20, 2) NULL DEFAULT NULL,`modify_time` datetime NULL DEFAULT NULL,`create_time` datetime NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;-- 支付信息
CREATE TABLE `t_pay`  (`id` bigint NOT NULL,`user_id` int NULL DEFAULT NULL,`pay` decimal(20, 2) NULL DEFAULT NULL,`create_time` datetime NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

用户信息表 t_user 有两个字段需要说明:
api_open 表示用户接口是否开启,如果用户账号余额不足可以把该字段设置为false拦截扣费请求;
deduct_type 这个字段表示扣费方式,ASYNC表示异步扣费,先通过redis扣减余额再异步扣减数据库,SYNC表示同步扣费,直接扣减数据库。

账户余额表 t_account 中也要说明两个字段:
version 表示余额扣减的版本号,这个版本号只能递增,每更新一次数据库版本号就增加1,在异步扣费时,可以比较redis中数据和db中数据的差异,正常来说redis中的版本号要大于等于db中的版本号;
threshold 是余额的一个阈值,由于redis中数据和db数据的差异,当异步扣费时redis中的余额小于该值时,避免有可能存在的超扣情况,要对用户的请求限流。

第一步在redis中扣减客户的余额时,需要处理三个数据:
(1)判断客户余额信息是否存在,如果存在并且余额充足,则扣减客户余额;
(2)生成订单信息缓存,由于数据库的订单和redis扣费订单存在时间差,这里的订单信息可以用于查询订单数据;redis如果发生主从切换,订单信息也可以用于判断该订单是否要重新加载历史数据;
(3)添加订单一致性集合数据,当kafka消息被消费后会把订单ID在这个集合中删除,它主要有两个用途:订单已经在redis中扣费但是由于某些原因没能在kafka中消费到,可以通过补偿逻辑将该订单入库;也可以通过这个集合中的订单条数判断redis处理数据与db处理数据的差异;

正常流程这三步的lua脚本:

local rs 
if redis.call('exists', KEYS[1]) > 0 then local vals = redis.call('hmget', KEYS[1], 'balance', 'threshold', 'v') local balance = tonumber(vals[1]) local threshold = tonumber(vals[2]) local v0 = tonumber(vals[3]) if balance >= tonumber(ARGV[1]) and (balance - ARGV[1]) >= threshold then local b = redis.call('hincrby', KEYS[1], 'balance', 0 - ARGV[1]) redis.call('hset', KEYS[1], 'ts', ARGV[2]) redis.call('set', KEYS[2], ARGV[1] .. ';' .. ARGV[2] .. ';' .. b, 'EX', 604800) redis.call('zadd', KEYS[3], ARGV[2], ARGV[3]) local v = redis.call('hincrby', KEYS[1], 'v', 1) rs = 'ok;' .. b .. ';' .. v else rs = 'fail;' .. balance .. ';' .. v0 end 
else rs = 'null' 
end 
return rs

最初redis中是不存在数据的,需要将db数据加载到redis中:

if redis.call('exists', KEYS[1]) > 0 then redis.call('hget', KEYS[1], 'balance') 
else redis.call('hmset', KEYS[1], 'balance', ARGV[1], 'v', ARGV[2], 'threshold', ARGV[3]) 
end 
return 'ok'

当redis扣费成功而后面操作出现异常时需要回滚redis的扣费:

local rs 
if redis.call('exists', KEYS[1]) > 0 then local v = tonumber(redis.call('hget', KEYS[1], 'v')) if v >= tonumber(ARGV[2]) then rs = redis.call('hincrby', KEYS[1], 'balance', ARGV[1]) redis.call('hincrby', KEYS[1], 'v', 1) redis.call('del', KEYS[2]) redis.call('zrem', KEYS[3], ARGV[3]) else rs = 'fail' end 
else rs = 'null' 
end 
return rs

上面的lua脚本涉及到多个键操作,要保证在集群模式下命令正确执行,要让所有的键落在一个hash槽,所以在键的设计时要保证计算hash的部分相同,使用用户ID包裹在{}内就能达到目的,所有涉及到的键封装成一个工具类:

/*** @Author xingo* @Date 2024/9/10*/
public class RedisKeyUtils {/*** 用户余额键* @param userId    用户ID* @return*/public static String userBalanceKey(int userId) {return ("user:balance:{" + userId + "}").intern();}/*** 用户订单键* @param userId    用户ID* @param orderId   订单ID* @return*/public static String userOrderKey(int userId, long orderId) {return ("user:order:{" + userId + "}:" + orderId).intern();}/*** 保存用户订单一致性的订单集合* 保存可能已经在redis中但不在数据库中的订单ID集合* @param userId    用户ID* @return*/public static String userOrderZsetKey(int userId) {return ("user:order:consistency:{" + userId + "}").intern();}/*** 用户分布式锁键* @param userId* @return*/public static String userLockKey(int userId) {return ("user:lock:" + userId).intern();}
}

使用springboot开发还需要将lua脚本封装成工具类:

import org.springframework.data.redis.core.script.DefaultRedisScript;/*** lua脚本工具类** @Author xingo* @Date 2024/9/10*/
public class LuaScriptUtils {/*** redis数据分隔符*/public static final String SEPARATOR = ";";/*** 异步扣费lua脚本* 键1:用户余额键,hash结构* 键2:用户订单键,string结构* 参数1:扣减的金额,单位是分;* 参数2:扣减余额的时间戳*/public static final String ASYNC_DEDUCT_BALANCE_STR ="local rs " +"if redis.call('exists', KEYS[1]) > 0 then " +"  local vals = redis.call('hmget', KEYS[1], 'balance', 'threshold', 'v') " +"  local balance = tonumber(vals[1]) " +"  local threshold = tonumber(vals[2]) " +"  local v0 = tonumber(vals[3]) " +"  if balance >= tonumber(ARGV[1]) and (balance - ARGV[1]) >= threshold then " +"    local b = redis.call('hincrby', KEYS[1], 'balance', 0 - ARGV[1]) " +"    redis.call('hset', KEYS[1], 'ts', ARGV[2]) " +"    redis.call('set', KEYS[2], ARGV[1] .. '" + SEPARATOR + "' .. ARGV[2] .. '" + SEPARATOR + "' .. b, 'EX', 604800) " +"    redis.call('zadd', KEYS[3], ARGV[2], ARGV[3]) " +"    local v = redis.call('hincrby', KEYS[1], 'v', 1) " +"    rs = 'ok" + SEPARATOR + "' .. b .. '" + SEPARATOR + "' .. v " +"  else " +"    rs = 'fail" + SEPARATOR + "' .. balance .. '" + SEPARATOR + "' .. v0 " +"  end " +"else " +"  rs = 'null' " +"end " +"return rs";/*** 同步余额数据lua脚本* 键:用户余额键,hash结构* 参数1:扣减的金额,单位是分* 参数2:扣减余额的时间戳* 参数3:余额的阈值,单位是分*/public static final String SYNC_BALANCE_STR ="if redis.call('exists', KEYS[1]) > 0 then " +"  redis.call('hget', KEYS[1], 'balance') " +"else " +"  redis.call('hmset', KEYS[1], 'balance', ARGV[1], 'v', ARGV[2], 'threshold', ARGV[3]) " +"end " +"return 'ok'";/*** 回滚扣款lua脚本* 键1:用户余额键,hash结构* 键2:用户订单键,string结构* 参数1:回滚的金额,单位是分* 参数2:扣款时对应的版本号*/public static final String ROLLBACK_DEDUCT_STR ="local rs " +"if redis.call('exists', KEYS[1]) > 0 then " +"  local v = tonumber(redis.call('hget', KEYS[1], 'v')) " +"  if v >= tonumber(ARGV[2]) then " +"    rs = redis.call('hincrby', KEYS[1], 'balance', ARGV[1]) " +"    redis.call('hincrby', KEYS[1], 'v', 1) " +"    redis.call('del', KEYS[2]) " +"    redis.call('zrem', KEYS[3], ARGV[3]) " +"  else " +"    rs = 'fail' " +"  end " +"else " +"  rs = 'null' " +"end " +"return rs";public static final DefaultRedisScript<String> ASYNC_DEDUCT_SCRIPT = new DefaultRedisScript<>(LuaScriptUtils.ASYNC_DEDUCT_BALANCE_STR, String.class);public static final DefaultRedisScript<String> SYNC_BALANCE_SCRIPT = new DefaultRedisScript<>(LuaScriptUtils.SYNC_BALANCE_STR, String.class);public static final DefaultRedisScript<String> ROLLBACK_DEDUCT_SCRIPT = new DefaultRedisScript<>(LuaScriptUtils.ROLLBACK_DEDUCT_STR, String.class);
}

所有基础组件都已经准备好了,接下来就是编写义务代码:

处理请求接口:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xingo.domain.ApiResult;
import org.xingo.front.service.DeductService;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/9*/
@Slf4j
@RestController
public class DeductController {@Autowiredprivate DeductService deductService;/*** 异步扣减余额* @param userId    用户ID* @param amount    扣减金额* @return*/@GetMapping("/async/dudect")public ApiResult asyncDeduct(int userId, BigDecimal amount) {return deductService.asyncDeduct(userId, amount);}}

扣费处理服务:

import org.xingo.domain.ApiResult;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/13*/
public interface DeductService {/*** 异步扣减余额* @param userId* @param amount* @return*/ApiResult asyncDeduct(int userId, BigDecimal amount);
}
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xingo.domain.ApiResult;
import org.xingo.domain.DeductResult;
import org.xingo.entity.UserData;
import org.xingo.enums.DeductType;
import org.xingo.front.config.CacheUtils;
import org.xingo.front.service.DeductService;
import org.xingo.front.service.OrderService;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/13*/
@Slf4j
@Service
public class DeductServiceImpl implements DeductService {@Autowiredprivate OrderService orderService;@Autowiredprivate CacheUtils cacheUtils;@Overridepublic ApiResult asyncDeduct(int userId, BigDecimal amount) {ApiResult result = null;UserData user = cacheUtils.getUserData(userId);if(user != null && user.isApiOpen()) {amount = amount.abs();if(user.getDeductType() == DeductType.ASYNC) {DeductResult rs = orderService.asyncDeductOrder(userId, amount);if(StringUtils.isNotBlank(rs.getMessage()) && "null".equals(rs.getMessage())) {BigDecimal rsBalance = orderService.syncBalanceToRedis(userId);if(rsBalance != null) {return this.asyncDeduct(userId, amount);} else {result = ApiResult.fail("同步余额失败");}} else {result = ApiResult.success(rs);}} else {DeductResult rs = orderService.syncDeductOrder(userId, amount);return StringUtils.isBlank(rs.getMessage()) ? ApiResult.success(rs) : ApiResult.fail(rs.getMessage());}} else {result = ApiResult.fail("用户接口未开启");}return result;}
}

订单扣减服务

import org.xingo.domain.DeductResult;import java.math.BigDecimal;/*** @Author xingo* @Date 2024/9/9*/
public interface OrderService {/*** 同步扣减订单* @param userId* @param amount* @return*/DeductResult syncDeductOrder(Integer userId, BigDecimal amount);/*** 同步余额数据到redis* @param userId* @return*/BigDecimal syncBalanceToRedis(Integer userId);/*** 同步扣减订单* @param userId* @param amount* @return*/DeductResult asyncDeductOrder(Integer userId, BigDecimal amount);
}
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xingo.common.ConstantVal;
import org.xingo.common.JacksonUtils;
import org.xingo.common.RedisKeyUtils;
import org.xingo.domain.DeductResult;
import org.xingo.entity.AccountData;
import org.xingo.entity.OrderData;
import org.xingo.front.mapper.AccountMapper;
import org.xingo.front.mapper.OrderMapper;
import org.xingo.front.service.OrderService;
import org.xingo.front.service.UserService;import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;/*** @Author xingo* @Date 2024/9/9*/
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {/*** 100倍*/BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);Logger deductLogger = LoggerFactory.getLogger("deduct");@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate KafkaTemplate<String, String> kafkaTemplate;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate UserService userService;@Override@Transactional(rollbackFor = Exception.class)public DeductResult syncDeductOrder(Integer userId, BigDecimal amount) {String lockKey = RedisKeyUtils.userLockKey(userId);RLock rlock = redissonClient.getLock(lockKey);try {boolean tryLock = rlock.tryLock(5, 5, TimeUnit.SECONDS);if(tryLock) {// 扣减账号余额boolean rs = accountMapper.deductBalanceByUserId(userId, amount);if(rs) {    // 扣减余额成功// 查找余额AccountData account = accountMapper.selectById(userId);if(account.getBalance().compareTo(BigDecimal.ZERO) <= 0) {userService.closeUserApi(userId);}// 增加订单long id = IdUtil.getSnowflake(1, 1).nextId();OrderData orderData = OrderData.builder().id(id).userId(userId).amount(amount).balance(account.getBalance()).modifyTime(LocalDateTime.now()).createTime(LocalDateTime.now()).build();orderMapper.insert(orderData);log.info("同步扣减账号余额|{}|{}|{}|{}", userId, amount, account.getBalance(), id);// 实时同步余额到redisString key = RedisKeyUtils.userBalanceKey(userId);String key1 = RedisKeyUtils.userOrderKey(userId, id);List<String> keys = Arrays.asList(key, key1);String execute = redisTemplate.execute(LuaScriptUtils.DB_DEDUCT_SCRIPT, keys,amount.multiply(ONE_HUNDRED).intValue() + "", System.currentTimeMillis() + "");if(execute.startsWith("ok")) {String[] arr = execute.split(";");int redisVersion = Integer.parseInt(arr[2]);if(redisVersion < account.getVersion()) {redisTemplate.delete(key);log.info("缓存数据版本低于数据库|{}|{}|{}|{}|{}", userId, redisVersion, account.getVersion(), arr[1], account.getBalance());}}log.info("同步扣减数据到缓存|{}|{}|{}|{}", userId, amount, account.getBalance(), execute);return DeductResult.builder().id(id).userId(userId).amount(amount).balance(account.getBalance()).build();}}} catch (InterruptedException e) {log.error("获取redisson锁异常", e);throw new RuntimeException(e);} finally {if(rlock.isHeldByCurrentThread()) {rlock.unlock();}}return DeductResult.builder().message("扣费失败").build();}@Overridepublic BigDecimal syncBalanceToRedis(Integer userId) {String lockKey = RedisKeyUtils.userLockKey(userId);RLock rlock = redissonClient.getLock(lockKey);try {boolean tryLock = rlock.tryLock(5, 5, TimeUnit.SECONDS);if(tryLock) {AccountData balance = accountMapper.selectById(userId);if(balance != null && balance.getBalance().compareTo(BigDecimal.ZERO) > 0) {String key = RedisKeyUtils.userBalanceKey(userId);List<String> keys = Collections.singletonList(key);try {String execute = redisTemplate.execute(LuaScriptUtils.SYNC_BALANCE_SCRIPT, keys,balance.getBalance().multiply(ONE_HUNDRED).intValue() + "",balance.getVersion().toString(),balance.getThreshold().multiply(ONE_HUNDRED).intValue() + "");log.info("同步账号余额到缓存|{}|{}|{}|{}|{}", userId, balance.getBalance(), balance.getVersion(), balance.getThreshold(), execute);return balance.getBalance();} catch (Exception e) {e.printStackTrace();return null;}}}} catch (InterruptedException e) {log.error("获取redisson锁异常", e);throw new RuntimeException(e);} finally {if(rlock.isHeldByCurrentThread()) {rlock.unlock();}}return null;}@Overridepublic DeductResult asyncDeductOrder(Integer userId, BigDecimal amount) {long id = IdUtil.getSnowflake(1, 1).nextId();String key = RedisKeyUtils.userBalanceKey(userId);String key1 = RedisKeyUtils.userOrderKey(userId, id);String key2 = RedisKeyUtils.userOrderZsetKey(userId);List<String> keys = Arrays.asList(key, key1, key2);try {long ts = System.currentTimeMillis();int fee = amount.multiply(ONE_HUNDRED).intValue();String execute = redisTemplate.execute(LuaScriptUtils.ASYNC_DEDUCT_SCRIPT, keys,fee + "", ts + "", id + "");log.info("异步扣减缓存余额|{}|{}|{}", userId, amount, execute);if(execute.startsWith("ok")) {      // 扣费成功return this.deductSuccess(keys, id, userId, amount, ts, execute);} else {    // 扣费失败return DeductResult.builder().message(execute).build();}} catch (Exception e) {log.error("扣费异常", e);// 扣费失败return DeductResult.builder().message("fail").build();}}/*** 扣费成功处理逻辑* @param keys* @param userId* @param amount* @param execute* @return* @throws Exception*/private DeductResult deductSuccess(List<String> keys, long id, Integer userId, BigDecimal amount, long ts, String execute) throws Exception {String[] arr = execute.split(";");BigDecimal balance = new BigDecimal(arr[1]).divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP);if(balance.compareTo(BigDecimal.ZERO) <= 0) {userService.closeUserApi(userId);}String version = arr[2];// 扣费成功发送kafka消息DeductResult deductResult = DeductResult.builder().id(id).userId(userId).balance(balance).amount(amount).build();// 发送消息队列采用同步方式,判断发送消息队列成功后才返回接口成功kafkaTemplate.send(ConstantVal.KAFKA_CONSUMER_TOPIC, userId.toString(), JacksonUtils.toJSONString(deductResult)).handle((rs, throwable) -> {if (throwable == null) {return rs;}String topic = rs.getProducerRecord().topic();log.info("异步扣减余额后发送消息队列失败|{}|{}|{}|{}", topic, userId, amount, execute);// kafka消息发送失败回滚扣费String execute1 = redisTemplate.execute(LuaScriptUtils.ROLLBACK_DEDUCT_SCRIPT, keys,amount.multiply(ONE_HUNDRED).intValue() + "", version, id + "");log.info("异步扣减余额后发送消息队列失败回滚扣费|{}|{}|{}|{}|{}", topic, userId, amount, execute, execute1);// 提示失败,调用同步扣费deductResult.setMessage("fail");return null;}).thenAccept(rs -> {if (rs != null) {deductLogger.info("{}|{}|{}|{}", userId, id, amount, ts);}}).get();return deductResult;}
}

上面的代码已经完成了扣费的第一步所有逻辑,只通过redis的高性能扣费逻辑就完成了。使用lua脚本能够保证事务,同时redis是单线程执行命令的不用考虑锁的问题。如果使用数据库完成扣费逻辑,就要考虑使用分布式锁保证服务的安全。
但是上面的代码还有一些点要优化的:

  1. 当redis集群发生了主从切换,由于redis的异步复制就有可能存在丢失数据的风险,所以我们要在业务系统中保证数据不丢失;
  2. redis与db库会存在数据差异,这是性能导致的,在某些场景中要考虑这种差异有可能引起的问题。

余额数据在redis中的结构是一个hash,金额的单位为分:

127.0.0.1:7001> hgetall user:balance:{9}
1) "balance"
2) "9970000"
3) "v"
4) "4"
5) "threshold"
6) "500000"
7) "ts"
8) "1728436908699"

订单信息是一个键值对,值的结构是多个;分隔的值,分别为本次扣减金额、时间戳、扣减后余额:

127.0.0.1:7001> get user:order:{9}:1843824075538042880
"10000;1728436890860;9990000"

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/446224.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解锁5 大无水印热门短视频素材库

想让你的抖音视频更出彩吗&#xff1f;想知道那些爆款视频的素材源头吗&#xff1f;快来了解以下 5 个超棒的视频素材下载平台。 蛙学网 国内的视频素材佼佼者&#xff0c;有大量 4K 高清且无水印的素材&#xff0c;自然风光、情感生活等类别任你选&#xff0c;不少还免费&…

关于wordpress建站遇到的问题

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

Spring WebFlux 核心原理(2-1)

1、Spring 响应式编程 1.1、早期响应式解决方案 响应式编程是构建响应式系统的主要候选方案。Spring 4.x 引入了 ListenableFuture 类&#xff0c;它扩展了 Java Future&#xff0c;并且可以基于 HTTP 请求实现异步执行操作。但是只有少数 Spring 4.x 组件支持新的 Java 8 Com…

瑞芯微RK3566/RK3568 Android11使用OTA升级固件方法,深圳触觉智能鸿蒙开发板演示,备战第九届华为ICT大赛

本文介绍瑞芯微RK3566/RK3568在Android11系统OTA升级固件方法&#xff0c;使用触觉智能的Purple Pi OH鸿蒙开发板演示&#xff0c;搭载了瑞芯微RK3566&#xff0c;Laval官方社区主荐&#xff01; 1、OTA包生成 在源码根目录上执行以下命令编译OTA包 # make installclean # …

【华为HCIP实战课程七】OSPF邻居关系排错MTU问题,网络工程师

一、MTU MUT默认1500,最大传输单元,一致性检测 [R3-GigabitEthernet0/0/1]mtu 1503//更改R3的MTU为1503 查看R3和SW1之间的OSPF邻居关系正常: 默认华为设备没有开启MTU一致性检测! [R3-GigabitEthernet0/0/1]ospf mtu-enable //手动开启MTU检测 [SW1-Vlanif30]ospf mtu…

【详细教程】如何使用YOLOv11进行图像与视频的目标检测

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

《数字信号处理》学习08-围线积分法(留数法)计算z 逆变换

目录 一&#xff0c;z逆变换相关概念 二&#xff0c;留数定理相关概念 三&#xff0c;习题 一&#xff0c;z逆变换相关概念 接下来开始学习z变换的反变换-z逆变换&#xff08;z反变化&#xff09;。 由象函数 求它的原序列 的过程就称为 逆变换。即 。 求z逆变换…

linux线程 | 线程的控制(二)

前言&#xff1a; 本节内容是线程的控制部分的第二个小节。 主要是列出我们的线程控制部分的几个细节性问题以及我们的线程分离。这些都是需要大量的代码去进行实验的。所以&#xff0c; 准备好接受新知识的友友们请耐心观看。 现在开始我们的学习吧。 ps:本节内容适合了解线程…

如何批量从sql语句中提取表名

简介 使用的卢易表 的提取表名功能&#xff0c;可以从sql语句中批量提取表名。采用纯文本sql语法分析&#xff0c;无需连接数据库&#xff0c;支持从含非sql语句的文件文件中提取&#xff0c;支持各类数据库sql语法。 特点 快&#xff1a;从成百个文件中提取上千个表名只需1…

JAVA开发中SpringMVC框架的使用及常见的404问题原因以及SpringMVC框架基于注解的开发实例

一、JAVA开发中SpringMVC框架的使用及常见的404问题原因 使用SpringMVC建立一个web项目&#xff0c;在IDEA中file->new->project建立一个空项目project。不用选择create from archetype从模板创建。然后在项目的pom.xml中添加公共的依赖包括org.springframework&#xff…

400行程序写一个实时操作系统RTOS(开篇)

笔者之前突发奇想&#xff0c;准备写一个极其微小的实时操作系统内核&#xff0c;在经过数天的努力后&#xff0c;这个RTOS诞生了。令读者比较意外的是&#xff0c;它的程序只有400行左右。但就是这短短的400行&#xff0c;完成了动态内存管理、多线程、优先级、临界区、低功耗…

【原创】Android Studio 中安装大模型辅助编码插件:通义灵码

在 Android Studio 中内置了 Ginimi 预览版&#xff0c;但需要“加速器”才可使用。 在国内有平替的软件同样可以使用&#xff0c;比如 阿里的通义灵码&#xff0c;智谱的CodeGeeX等&#xff0c;从功能和使用上来说都是大同小异。 这里我们以通义灵码为例来讲解其安装和使用 通…

最新Prompt预设词指令教程大全ChatGPT、AI智能体(300+预设词应用)

使用指南 直接复制在AI工具助手中使用&#xff08;提问前&#xff09; 可以前往已经添加好Prompt预设的AI系统测试使用&#xff08;可自定义添加使用&#xff09; SparkAi系统现已支持自定义添加官方GPTs&#xff08;对专业领域更加专业&#xff0c;支持多模态文档&#xff0…

github下载文件的两种方式(非git形式)

1.以下面的图为例 &#xff0c;可以直接点击右上方的绿色Code按键&#xff0c;在弹出的列表中选择Download Zip选项&#xff0c;即可下载。 2.如果下载的是单独的某一个文件&#xff0c;则可以按照下图的格式点击下图所示的那个下载的图标即可。

IP地址如何支持远程办公?

由于当今社会经济的飞速发展&#xff0c;各个方向的业务都不免接触到跨省、跨市以及跨国办公的需要&#xff0c;随之而来的远程操作的不方便&#xff0c;加载缓慢&#xff0c;传输文件时间过长等困难&#xff0c;如何在万里之外实现远程办公呢&#xff1f;我们以以下几点进行阐…

C3D网络介绍及代码撰写详解(总结3)

可以从本人以前的文章中可以看出作者以前从事的是嵌入式控制方面相关的工作&#xff0c;是一个机器视觉小白&#xff0c;之所以开始入门机器视觉的学习主要是一个idea&#xff0c;想把机器视觉与控制相融合未来做一点小东西。废话不多说开始正题。&#xff08;如有侵权立即删稿…

初级前端面试(2)

1.讲一下闭包相关知识&#xff0c;和普通函数有什么区别 闭包是什么&#xff1a;JS中内层函数可以访问外层函数的变量&#xff0c;外层函数无法操作内存函数的变量的特性。我们把这个特性称作闭包。 闭包的好处&#xff1a; 隔离作用域&#xff0c;保护私有变量&#xff1b;…

解决海外社媒风控问题的工具——云手机

随着中国企业逐步进入海外市场&#xff0c;海外社交媒体的风控问题严重影响了企业的推广效果与账号运营。这种背景下&#xff0c;云手机作为一种新型技术解决方案&#xff0c;正日益成为企业应对海外社媒风控的重要工具。 由于海外社媒的严格监控&#xff0c;企业经常面临账号流…

数据库的相关知识

数据库的相关知识 1.数据库能够做什么&#xff1f; 存储大量数据&#xff0c;方便检索和访问保持数据信息的一致、完整共享和安全通过组合分析&#xff0c;产生新的有用信息 2.数据库作用&#xff1f; 存储数据、检索数据、生成新的数据 3.数据库要求&#xff1f; 统一、…