Redis学习6——Redis分布式锁

引言

分布式锁

分布式锁(Distributed Lock)是一种用于分布式系统中实现互斥访问的机制,在分布式系统中,多个节点同时访问共享资源可能导致数据不一致或竞态条件的问题,分布式锁通过协调多个节点之间的访问,确保在同一时间只有一个节点能获得对共享资源的独占访问权限,从而解决并发访问问题。

分布式锁实现方式

常用的分布式锁实现方式有:
1)基于数据库的分布式锁(乐观锁):使用数据库的事务特性和唯一约束来实现分布式锁。通过在数据库中创建一个特定的表或记录来表示锁的状态,节点可以通过获取或释放该记录来获取或释放锁。
2)基于缓存的分布式锁:使用分布式缓存系统(如Redis)的原子操作来实现分布式锁,节点可以通过在缓存中设置一个特定的键值对来获取锁,并利用缓存的原子性操作来保证锁的互斥性。
3)基于zookeeper的分布式锁:zookeeper是一个分布式协调服务,可以用于实现分布式锁,节点可以通过在zookeeper中创建一个临时有序节点来表示锁的占用状态,通过比较节点 的序号来确定锁的拥有权。

基于数据库的分布式锁(乐观锁)

基于数据库的分布式锁实现方案,一般是在表中加一个字段,用于表示版本号,当读取数据时,会读取对应的版本号,在更新数据的时候,也会相应的更新版本号(比如版本号递增),且在更新数据的时候,会判断当前版本号是否正确,以账户余额修改为例,具体流程如下:
1)查询账户信息(此时从数据库中查出的版本号为version1)
2)根据请求对账户对象进行操作
3)更新数据库(update t_account set 字段=新值, version = version + 1 where id = #{accountId} and version = version1的值)
在这个过程中,最重要的就是更新sql的语句,也就是在更新的时候,判断版本号是否被修改过,只有没有被修改过,我们才能更新成功。
示例如下:
首先,我们创建一个账户表:
image.png
然后在账户表上插入一条数据,假设账户中有1000元
image.png
对应的实体类和mapper:


@TableName(value = "t_account")
@Data
public class Account implements Serializable {@TableId(type = IdType.AUTO)private Integer id;private Integer userId;private Integer balance;private Date createTime;private Date updateTime;private Integer version;
}@Mapper
public interface AccountMapper extends BaseMapper<Account> {
}

然后我们创建一个AccountService,先演示没有乐观锁时,会造成的问题

package org.example.service;import org.example.mapper.AccountMapper;
import org.example.pojo.Account;
import org.example.request.account.TakeOutMoneyRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Date;@Service
public class AccountService {@Autowiredprivate AccountMapper accountMapper;public boolean takeOutMoneyWithoutOpLock(TakeOutMoneyRequest request) throws InterruptedException {Integer accountId = request.getAccountId();Account account = accountMapper.selectById(accountId);if (account.getBalance() - request.getMoney() < 0) {System.out.println("余额不足==============");return false;}Thread.sleep(1000);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;}
}

添加一个测试类,用于演示并发情况下,账户余额的减少

package org.example.service;import org.example.mapper.AccountMapper;
import org.example.pojo.Account;
import org.example.request.account.TakeOutMoneyRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Date;@Service
public class AccountService {@Autowiredprivate AccountMapper accountMapper;public boolean takeOutMoneyWithoutOpLock(TakeOutMoneyRequest request) throws InterruptedException {Integer accountId = request.getAccountId();Account account = accountMapper.selectById(accountId);if (account.getBalance() - request.getMoney() < 0) {System.out.println("余额不足==============");return false;}Thread.sleep(1000);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;}
}

运行测试方法,结果如下图所示,说明都更新成功了
image.png
然后查看数据库:
image.png
如上图所示,原先我们的账户余额是1000元,每次扣除100元,经过10次扣减后,账户余额应该为0,但是因为并发问题,导致查询的时候,有多个请求查询到同一个值,最后导致数据不一致。
我们修改Account,添加使用乐观锁进行扣减余额的方法:

   public boolean takeOutMoneyWithOpLock(TakeOutMoneyRequest request) throws InterruptedException {Integer accountId = request.getAccountId();Account account = accountMapper.selectById(accountId);if (account.getBalance() - request.getMoney() < 0) {System.out.println("余额不足==============");return false;}Thread.sleep(1000);LambdaUpdateWrapper<Account> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();lambdaUpdateWrapper.set(Account::getVersion, account.getVersion() + 1).set(Account::getUpdateTime, new Date()).set(Account::getBalance, account.getBalance() - request.getMoney()).eq(Account::getVersion, account.getVersion()).eq(Account::getId, request.getAccountId());return accountMapper.update(account, lambdaUpdateWrapper) > 0;}

我们把金额修改回1000,然后添加测试方法:

 @Testpublic void testTakeoutMoneyWithOpLock() throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);Callable<Boolean> takeoutTask = () -> {TakeOutMoneyRequest request = new TakeOutMoneyRequest();request.setAccountId(1);request.setMoney(100);try {return accountService.takeOutMoneyWithOpLock(request);} catch (InterruptedException e) {return false;}};List<Future<Boolean>> futureList = new ArrayList<>();for (int i = 0; i < 10; i++) {Future<Boolean> future = executorService.submit(takeoutTask);futureList.add(future);}for (Future<Boolean> future : futureList) {System.out.println(future.get());}}

运行测试方法,结果如下,说明只有两次更新成功了,其余的更新,都以为乐观锁被修改了,导致更新失败
image.png
然后我们查看数据库,结果如下,因为扣减了两次,所有余额为800,这个数据对的上。
image.png
乐观锁的实现思路,是基于对并发更新的乐观假设,也就是认为冲突的概率较低,因此在读取和提交数据时进行版本号或时间戳的比较,而不是在数据访问阶段进行加锁操作,避免了显示的锁竞争,提高了并发性能。但乐观锁并不能完全消除并发冲突,只是在提交数据时进行冲突检测和处理,如果系统中的并发冲突非常频繁,乐观锁的效率可能会下降。

基于Redis的分布式锁

基于Redis的SETNX实现分布式锁

SETNX指的是set if not exist,也就是当key不存在的时候,设置key的值,存在的话,什么都不做, 其语法为:

set key value nx

如果我们要设置过期时间的话,可以使用

set key value ex 时间 nx

如下图所示,在使用nx指令的时候,只有在该key不存在的时候,才能设置成功
image.png
我们修改之前的RedisUtils工具类,添加上和这两条指令相关的方法:

 public boolean setIfAbsent(String key, Object value) {return redisTemplate.opsForValue().setIfAbsent(key, value);}/*** 不存在时设置值,适用与分布式锁的场景* @param key* @param value* @param time* @return*/public boolean setIfAbsent(String key, Object value, long time) {return setIfAbsent(key, value, time, TimeUnit.SECONDS);}public boolean setIfAbsent(String key, Object value, long time, TimeUnit timeUnit) {return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);}
通过setnx首先分布式锁

具体流程如下:

我们修改AccountService,添加和该指令相关的方法:

  @Autowiredprivate RedisUtils redisUtils;public boolean takeOutMoneyWithSetnx(TakeOutMoneyRequest request) {Integer accountId = request.getAccountId();String key = "lock::" + accountId;boolean lock = redisUtils.setIfAbsent(key, request.getAccountId());if (!lock) {// 加锁失败,返回return false;}// 加锁成功try {Account account = accountMapper.selectById(accountId);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;} finally {// 释放锁redisUtils.removeKey(key);}}

添加测试方法:

@Testpublic void testTakeoutMoneyWithSetnx() throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);Callable<Boolean> takeoutTask = () -> {TakeOutMoneyRequest request = new TakeOutMoneyRequest();request.setAccountId(1);request.setMoney(100);return accountService.takeOutMoneyWithSetnx(request);};List<Future<Boolean>> futureList = new ArrayList<>();for (int i = 0; i < 10; i++) {Future<Boolean> future = executorService.submit(takeoutTask);futureList.add(future);}for (Future<Boolean> future : futureList) {System.out.println(future.get());}}

测试结果如下:
image.png
我们查看数据库,确实只扣减了100
image.png
这里冲突次数比较多,因此更新的效率有点低,我们可以将对应的方法修改一下,加上重试,修改代码如下:

  @Autowiredprivate RedisUtils redisUtils;public boolean takeOutMoneyWithSetnx(TakeOutMoneyRequest request) {Integer accountId = request.getAccountId();String key = "lock::" + accountId;boolean lock = redisUtils.setIfAbsent(key, request.getAccountId());if (lock) {// 加锁成功try {Account account = accountMapper.selectById(accountId);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;} finally {// 释放锁redisUtils.removeKey(key);}}// 加锁失败,进行重试try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return takeOutMoneyWithSetnx(request);}

这里休眠了一段时间,是因为要涉及到递归调用,可能会导致栈空间溢出,我们再次执行测试代码,结果如下,经过重试后,执行成功率变高。
image.png
查看数据库,确实扣减了10次。
image.png
但是,使用set key value nx存在一个问题,如果setnx占锁成功,但是服务器宕机了,没有执行删除锁的逻辑,那么就会造成这个锁一直没有被释放,最终导致死锁。

setnx with expire

为解决setnx造成的死锁问题,我们在setnx的基础上,加上过期时间,来解决上述问题。我们给AccountService添加上对应的方法如下:

 public boolean takeOutMoneyWithSetnxExpire(TakeOutMoneyRequest request) {Integer accountId = request.getAccountId();String key = "lock::" + accountId;// 占有锁并设置过期时间boolean lock = redisUtils.setIfAbsent(key, request.getAccountId(), TTL);if (lock) {// 加锁成功try {Account account = accountMapper.selectById(accountId);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;} finally {// 释放锁redisUtils.removeKey(key);}}// 加锁失败,进行重试try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return takeOutMoneyWithSetnx(request);}

添加上对应的测试方法:

@Testpublic void testTakeoutMoneyWithSetnxExpire() throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);Callable<Boolean> takeoutTask = () -> {TakeOutMoneyRequest request = new TakeOutMoneyRequest();request.setAccountId(1);request.setMoney(100);return accountService.takeOutMoneyWithSetnxExpire(request);};List<Future<Boolean>> futureList = new ArrayList<>();for (int i = 0; i < 10; i++) {Future<Boolean> future = executorService.submit(takeoutTask);futureList.add(future);}for (Future<Boolean> future : futureList) {System.out.println(future.get());}}

测试结果如下:
image.png
我们 查看数据库,数据库也确实扣减了10次
image.png
但是,这个方案还是有一定缺陷,因为我们设置的这个过期时间,是根据我们的经验设置的,而业务代码的执行时长,是不确定的,那么可能存在这种情况,假设我们现在有三个请求过来,我们设置的过期时间是100ms
1)请求A占锁成功,执行业务代码
2)请求A执行100ms后,锁过期,但此时请求A的业务代码还未执行完毕
3)请求B占锁成功,执行业务代码
4)请求A执行完毕,执行释放锁的逻辑,导致把B占有的锁打开了
5)请求C占锁成功,执行业务代码
6)请求B执行完毕,执行释放锁的逻辑,导致把C占有的锁打开了
这里是因为,这三个请求占有的锁的key都是相同的,而我们在释放锁的时候,只是执行删除key的命令,并不在意这个锁是谁占有的。
这种情况,我们可以通过lua脚本来解决,思路如下:
1)占锁的时候,设置value值为用户标识
2)释放锁的时候,通过lua脚本,判断此时key对应的value值,与传入值是否相同,只有相同的时候,我们才执行删除key的逻辑。
我们修改刚才的方法,如下,在占锁的时候,我们设置value值为当前的线程id(这里是为了演示,实际业务场景中,应该是多个用户抢占同一个资源,因此可以将vlaue值设置为用户的标识,比如用户id),然后在释放资源的时候,执行lua脚本,判断value值是否相同,相同则执行删除操作。

  @Autowiredprivate RedisTemplate<String, Object> redisTemplate;public boolean takeOutMoneyWithSetnxExpire(TakeOutMoneyRequest request) {Integer accountId = request.getAccountId();String key = "lock::" + accountId;long threadId = Thread.currentThread().getId();// 占有锁并设置过期时间boolean lock = redisUtils.setIfAbsent(key, threadId, TTL);if (lock) {// 加锁成功try {Account account = accountMapper.selectById(accountId);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;} finally {// 释放锁// lua脚本String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(key), threadId);}}// 加锁失败,进行重试try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return takeOutMoneyWithSetnx(request);}

再次执行测试代码:
image.png
查看数据库,确实减少10次
image.png
但这里还有一个问题没有解决,因为我们设置的TTL,是我们的经验值,不准确,所以还是会存在,某个请求占有锁后,还没执行完毕,锁过期了,被另外一个请求占有,此时会出现两个请求都认为自己占有锁的情况。

Redisson
简介

Redisson是一个在Redis基础上实现的Java驻内存数据网络,它不仅提供一系列的分布式java常用对象,还提供许多分布式服务,其宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能将精力更多集中在处理业务逻辑上。
image.png

SpringBoot 整合Redisson

引入redisson的maven依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.15.5</version></dependency>

然后自定义配置类(这里使用的是单节点Redis配置)

package org.example.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissonConfiguration {@Beanpublic RedissonClient redisson() {// 1. 创建配置Config config = new Config();// 集群模式
//        config.useClusterServers().addNodeAddress("集群ip1", "集群id2");// 2. 根据Config创建出RedissonClient示例config.useSingleServer().setAddress("redis://127.0.0.1:6379");return Redisson.create(config);}
}

我们添加测试方法,来测试redisson的一些基本操作:

package org.example;import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Test;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.concurrent.TimeUnit;@SpringBootTest
public class RedissonTest {@Autowiredprivate RedissonClient redissonClient;@Testpublic void testRedisson() {// 字符串操作RBucket<Object> rBuck = redissonClient.getBucket("name");rBuck.set("cxy", 30, TimeUnit.SECONDS);System.out.println(redissonClient.getBucket("name").get());// 哈希操作RMap<Object, Object> student = redissonClient.getMap("student");student.put("id", 1);student.put("name", "cxy");student.put("age", 20);student.expire(30, TimeUnit.SECONDS);System.out.println(redissonClient.getMap("student").get("name"));// 列表操作RList<Object> schools = redissonClient.getList("schools");schools.add("华南理工大学");schools.add("中山大学");schools.add("暨南大学");System.out.println(JSONObject.toJSONString(redissonClient.getList("schools")));// 集合操作RSet<Object> schoolSet = redissonClient.getSet("schoolSet");schoolSet.add("华南理工大学");schoolSet.add("中山大学");schoolSet.add("暨南大学");System.out.println(JSONObject.toJSONString(redissonClient.getSet("schoolSet")));// ZSet操作RScoredSortedSet<Object> schoolScoreSet = redissonClient.getScoredSortedSet("schoolScoreSet");schoolScoreSet.add(100d, "华南理工大学");schoolScoreSet.add(90d, "中山大学");schoolScoreSet.add(80d, "暨南大学");System.out.println(JSONObject.toJSONString(redissonClient.getScoredSortedSet("schoolScoreSet")));}
}

结果如下:
image.png

Redisson分布式锁

redisson加锁,可以使用lock方法,注意,在加锁的时候,处理完业务逻辑后要记得释放锁,测试代码如下:

  @Testpublic void testLock() throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(2);Runnable lockTask = () -> {try {lock();} catch (InterruptedException e) {e.printStackTrace();}};List< Future> futureList = new ArrayList<>();for (int i = 0; i < 2; i++) {futureList.add(executorService.submit(lockTask));}for (Future future : futureList) {future.get();}}private void lock() throws InterruptedException {RLock myLock = redissonClient.getLock("myLock");myLock.lock();try {System.out.println("currentTime:" + System.currentTimeMillis());Thread.sleep(2000);System.out.println("执行业务代码");} finally {myLock.unlock();}}

测试结果如下,从执行结果可以看出,当多个线程抢占锁时,后面的锁,需要等待,即这个锁是阻塞的。
image.png
如果不想阻塞的话,我们可以使用tryLock来上锁,结合刚才的accountService,我们先修改accountService,加上对应的方法

 @Autowiredprivate RedissonClient redissonClient;public boolean takeoutMoneyWithRedissonTryLock(TakeOutMoneyRequest request) throws InterruptedException {Integer accountId = request.getAccountId();String key = "lock::" + accountId;RLock lock = redissonClient.getLock(key);if (lock.tryLock(2, 4, TimeUnit.SECONDS)) { // 过期时间为2秒,最长存活时间为4秒// 上锁成功try {Thread.sleep(1000);Account account = accountMapper.selectById(accountId);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;} finally {// 释放锁lock.unlock();}}return false;}

添加测试方法:

   @Testpublic void testTakeoutMoneyWithRedissonTryLock() throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);Callable<Boolean> takeoutTask = () -> {TakeOutMoneyRequest request = new TakeOutMoneyRequest();request.setAccountId(1);request.setMoney(100);return accountService.takeoutMoneyWithRedissonTryLock(request);};List<Future<Boolean>> futureList = new ArrayList<>();for (int i = 0; i < 10; i++) {Future<Boolean> future = executorService.submit(takeoutTask);futureList.add(future);}for (Future<Boolean> future : futureList) {System.out.println(future.get());}}

测试结果如下:
image.png
查看数据库,减少的次数与上面的次数一致
image.png
不过上面这种,成功率比较低,因此我们可以将tryLock改为lock方法,来上锁,我们修改accountService,添加相关方法:

  public boolean takeoutMoneyWithRedissonLock(TakeOutMoneyRequest request) throws InterruptedException {Integer accountId = request.getAccountId();String key = "lock::" + accountId;RLock lock = redissonClient.getLock(key);lock.lock(2, TimeUnit.SECONDS);// 上锁成功try {Thread.sleep(1000);Account account = accountMapper.selectById(accountId);account.setBalance(account.getBalance() - request.getMoney());account.setUpdateTime(new Date());return accountMapper.updateById(account) > 0;} finally {// 释放锁lock.unlock();}}

添加测试方法:

 @Testpublic void testTakeoutMoneyWithRedissonLock() throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);Callable<Boolean> takeoutTask = () -> {TakeOutMoneyRequest request = new TakeOutMoneyRequest();request.setAccountId(1);request.setMoney(100);return accountService.takeoutMoneyWithRedissonLock(request);};List<Future<Boolean>> futureList = new ArrayList<>();for (int i = 0; i < 10; i++) {Future<Boolean> future = executorService.submit(takeoutTask);futureList.add(future);}for (Future<Boolean> future : futureList) {System.out.println(future.get());}}

测试结果如下:
image.png
查看数据库,扣减次数确实为10次。
image.png

watch dog 看门狗机制


Redisson中的分布式锁自带自动续期机制,其提供了一个专门用来监控和续期锁的Watch Dog(看门狗),如果操作共享资源的线程还没有执行完成的话,Watch Dog会不断延长锁的过期时间,从而保证锁不会因为超时而被释放。

参考文章

https://zhuanlan.zhihu.com/p/374306005
https://my.oschina.net/u/4499317/blog/5039486
https://blog.csdn.net/qq_15071263/article/details/101277474
https://www.cnblogs.com/jelly12345/p/14699492.html

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

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

相关文章

运动控制“MC_MoveVelocity“功能块详细应用介绍

1、运动控制单位u/s介绍 运动控制单位[u/s]介绍-CSDN博客文章浏览阅读91次。运动控制很多手册上会写这样的单位,这里的u是英文单词unit的缩写,也就是单位的意思,所以这里的单位不是微米/秒,也不是毫米/秒,这里是一个泛指,当我们的单位选择脉冲时,它就是脉冲/秒,也就是…

懒人网址导航源码v3.9源码及教程

懒人网址导航源码v3.9源码及教程 效果图使用方法部分源码领取源码下期更新预报 效果图 使用方法 测试环境 宝塔Nginx -Tengine2.2.3的PHP5.6 MySQL5.6.44为防止调试错误&#xff0c;建议使用测试环境运行的php与mysql版本首先用phpMyAdmin导入数据库文件db/db.sql 如果导入不…

25-ESP32-S3 内置的真随机数发生器(RNG)

ESP32-S3 内置的真随机数发生器&#xff08;RNG&#xff09;&#x1f60e; 引言 &#x1f4da; 在许多应用中&#xff0c;随机数发生器&#xff08;RNG&#xff09;是必不可少的。无论是在密码学&#x1f512;、游戏&#x1f3ae;、模拟&#x1f9ea;或其他领域&#xff0c;随…

初期Linux

一&#xff0c;系统分为 1.1window系统 个人 &#xff1a;win7&#xff0c;win8&#xff0c;Win10&#xff0c;Win11服务器版&#xff1a;window server 2003&#xff0c;window server 2008 1.2Linux系统 centos7redhatubantukali 1.3什么是Linux&#xff1f; Linux是基…

武汉星起航:精准布局,卓越服务——运营交付团队领跑亚马逊

在全球电商浪潮中&#xff0c;亚马逊平台以其独特的商业模式和全球化的市场布局&#xff0c;吸引了无数商家和创业者的目光。在这个充满机遇的市场中&#xff0c;武汉星起航电子商务有限公司凭借其专业的运营交付团队&#xff0c;以其独特的五对一服务体系和精准的战略布局&…

【从零开始学习Minio | 第一篇】快速介绍什么是Minio

前言&#xff1a; 在当今数字化时代&#xff0c;数据的存储和管理已经成为了企业发展中的关键一环。随着数据量的不断增长和数据安全性的日益受到重视&#xff0c;传统的数据存储解决方案往往面临着诸多挑战。为了应对这些挑战&#xff0c;云存储技术应运而生&#xff0c;并在…

【C++】滑动窗口:将x减到0的最小操作数

1.题目 2.算法思路 这个题目难在要转化一下才能用滑动窗口。 题意是需要在数组的前后两段区间进行解题&#xff0c;但同时对两段区间进行操作是比较困难的&#xff0c;我们可以将中间这段区间只和与nums_sum-x&#xff08;数组总和-x&#xff09;进行比较&#xff0c;这样就可…

【PCIE】基于PCIE4C的数据传输(四)——使用MSIX中断

基于PCIE4C的数据传输&#xff08;三&#xff09;——遗留中断与MSI中断 一文介绍了遗留中断与MSI中断两种中断方式的代码实现&#xff0c;本文继续基于Xilinx UltrascaleHBM VCU128开发板与linux&#xff08;RHEL8.9&#xff09;&#xff0c;介绍MSIX中断方式的代码实现。本文…

PDF文档如何签名?用Adobe信任的文档签名证书

为PDF文档电子签名的方式有多种多样&#xff0c;但并非所有方案都是可靠的。我们在市面看到的电子图章、电子印章等仅在文档中置入印章图片的方式&#xff0c;并不具有任何法律上的有效性&#xff0c;它只是显示印章的图形效果&#xff0c;随时可以被篡改、伪造。PDF文档如何签…

在QEMU上运行OpenSBI+Linux+Rootfs

在QEMU上运行OpenSBILinuxRootfs 1 编译QEMU2 安装交叉编译工具3 编译OpenSBI4 编译Linux5 创建根文件系统5.1 编译busybox5.2 创建目录结构5.3 制作文件系统镜像5.3.1 创建 ext2 文件5.3.2 将目录结构拷贝进 ext2 文件5.3.3 取消挂载 6 运行OpenSBILinuxRootfs 本文所使用的版…

TitanIDE安装常见问题解答

在软件开发和编程的世界里&#xff0c;集成开发环境&#xff08;IDE&#xff09;扮演着至关重要的角色。TitanIDE作为一款功能强大的开发工具&#xff0c;深受广大开发者的喜爱。然而&#xff0c;在安装和使用TitanIDE的过程中&#xff0c;开发者们往往会遇到一些问题和挑战。针…

PostgreSQL连接拒绝如何解决和排查?

1. 服务器未运行 解决方案&#xff1a;确保 PostgreSQL 服务已启动。在 Linux 上&#xff0c;你可以使用如下命令来检查服务状态&#xff1a;sudo systemctl status postgresql如果服务未运行&#xff0c;使用以下命令启动它&#xff1a;sudo systemctl start postgresql2. Po…

设计宝典与速查手册,设计师必备资料合集

一、资料描述 本套设计资料&#xff0c;大小194.34M&#xff0c;共有13个文件。 二、资料目录 01-《商业设计宝典》.pdf 02-《色彩速查宝典》.pdf 03-《配色宝典》.pdf 04-《解读色彩情感密码》.pdf 05-《行业色彩应用宝典》.pdf 06-《构图宝典》.pdf 07-《创意宝典》…

绘唐ai工具怎么获取

这款产品的最大亮点在于其高度精准的语音克隆能力&#xff0c;利用先进的模型&#xff0c;能够捕捉到用户独特的音调、音高和调制方式&#xff0c;使用户能够以前所未有的方式复制和利用自己的声音。仅需10秒钟的录制时间&#xff0c;即可实现声音的克隆&#xff0c;相当便捷。…

代码随想录刷题随记30-贪心4

代码随想录刷题随记30-贪心4 860.柠檬水找零 leetcode链接 比较显然 class Solution {public boolean lemonadeChange(int[] bills) {int []accountnew int[3];for(int cur:bills){if(cur5)account[0];else if(cur10){account[0]--;if(account[0]<0)return false;account…

基于svm的手写数字识别程序介绍(matlab)

1、程序界面介绍 该程序GUI界面包括手写板、手写数字可视化&#xff08;原图&#xff09;、对图像进行灰度处理&#xff08;灰度图&#xff09;、图像二值化处理&#xff08;二值化&#xff09;、图像特征可视化&#xff08;HOG特征&#xff08;方向梯度直方图&#xff09;&…

鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务

在鸿蒙的内核线程就是任务&#xff0c;系列篇中说的任务和线程当一个东西去理解. 一般二种场景下需要切换任务上下文: 在线程环境下&#xff0c;从当前线程切换到目标线程&#xff0c;这种方式也称为软切换&#xff0c;能由软件控制的自主式切换.哪些情况下会出现软切换呢? 运…

【C 数据结构-动态内存管理】2. 边界标识法管理动态内存

文章目录 【 1. 边界标识法的结构设计 】【 2. 分配算法 】【 3. 回收算法 】3.1 空闲块两侧是占用块3.2 空闲块左侧是空闲块3.3 空闲块右侧是空闲块3.3 空闲块两侧是空闲块 边界标识法 可以解决系统中内存碎片过多而无法使用的问题。 【 1. 边界标识法的结构设计 】 使用边界…

用PowerPoint创建毛笔字书写动画

先看看下面这个毛笔字书写动画&#xff1a; 这个动画是用PowerPoint创建的。下面介绍创建过程。 1、在任何一款矢量图片编辑软件中创建一个图片&#xff0c;用文字工具输入文字内容。我用的是InkScape。排好版后将图片保存为.svg格式的矢量图片文件。 2、打开PowerPoint&…

【C++泛型编程】(二)标准模板库 STL

文章目录 标准模板库 STL容器算法迭代器仿函数/函数对象适配器分配器示例 标准模板库 STL C 的标准模板库&#xff08;Standard Template Library&#xff0c;STL&#xff09;旨在通过模板化的设计&#xff0c;提供一种通用的编程模式&#xff0c;使程序员能方便地实现和扩展各…