06、Redis实战:优惠券秒杀、分布式锁Redission、可重入、重试、看门狗、MutiLock

news2025/1/15 21:08:42

5、分布式锁-redission

5.1 分布式锁-redission功能介绍

基于setnx实现的分布式锁存在下面的问题:

  1. 重入问题
    • 重入问题是指获取锁的线程,可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,例如在HashTable这样的代码中,它的方法都是使用synchronized修饰的,加入它在一个方法内调用另一个方法,如果此时是不可重入的,那就死锁了。所以可重入锁的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的
  2. 不可重试
    • 我们编写的分布式锁只能尝试一次,失败了就返回false,没有重试机制。但合理的情况应该是:当线程获取锁失败后,他应该能再次尝试获取锁
  3. 超时释放
    • 我们在加锁的时候增加了TTL,这样我们可以防止死锁,但是如果卡顿(阻塞)时间太长,也会导致锁的释放。虽然我们采用Lua脚本来防止删锁的时候,误删别人的锁,但现在的新问题是没锁住,也有安全隐患
  4. 主从一致性
    • 如果Redis提供了主从集群,那么当我们向集群写数据时,主机需要异步的将数据同步给从机,万一在同步之前,主机宕机了(主从同步存在延迟,虽然时间很短,但还是发生了),那么又会出现死锁问题

1653546070602

那么什么是Redission呢

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redission提供了分布式锁的多种多样的功能

1653546736063

5.2 分布式锁-Redission快速入门

引入依赖:

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

配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        //添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

如何使用Redission的分布式锁

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
        
    }
    
}

在 VoucherOrderServiceImpl

注入RedissonClient

@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 这个代码不用了,因为我们现在要使用分布式锁
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

5.3 分布式锁-redission可重入锁原理

1653548087334

  • method1在方法内部调用method2,method1和method2出于同一个线程,那么method1已经拿到一把锁了,想进入method2中拿另外一把锁,必然是拿不到的,于是就出现了死锁
  • 所以我们需要额外判断,method1和method2是否处于同一线程,如果是同一个线程,则可以拿到锁,但是state会+1,之后执行method2中的方法,释放锁,释放锁的时候也只是将state进行-1,只有减至0,才会真正释放锁
  • 由于我们需要额外存储一个state,所以用字符串型SET NX EX是不行的,需要用到Hash结构,但是Hash结构又没有NX这种方法,所以我们需要将原有的逻辑拆开,进行手动判断
  • 为了保证原子性,所以流程图中的业务逻辑也是需要我们用Lua来实现的
    • 获取锁的逻辑
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 锁不存在
if (redis.call('exists', key) == 0) then
    -- 获取锁并添加线程标识,state设为1
    redis.call('hset', key, threadId, '1');
    -- 设置锁有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
-- 锁存在,判断threadId是否为自己
if (redis.call('hexists', key, threadId) == 1) then
    -- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长
    redis.call('hincrby', key, thread, 1);
    -- 设置锁的有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
  • 释放锁的逻辑

    local key = KEYS[1];
    local threadId = ARGV[1];
    local releaseTime = ARGV[2];
    -- 如果锁不是自己的
    if (redis.call('HEXISTS', key, threadId) == 0) then
        return nil; -- 直接返回
    end;
    -- 锁是自己的,锁计数-1,还是用hincrby,不过自增长的值为-1
    local count = redis.call('hincrby', key, threadId, -1);
    -- 判断重入次数为多少
    if (count > 0) then
        -- 大于0,重置有效期
        redis.call('expire', key, releaseTime);
        return nil;
    else
        -- 否则直接释放锁
        redis.call('del', key);
        return nil;
    end;
    

5.4 分布式锁-redission锁重试和WatchDog机制

说明:由于课程中已经说明了有关tryLock的源码解析以及其看门狗原理,所以笔者在这里给大家分析lock()方法的源码解析,希望大家在学习过程中,能够掌握更多的知识

开门狗的前提就是不设置有效期,如果自己设置了有效期,则没有看门狗机制

ttl代表锁剩余有效时间,如果ttl为空,代表获取锁成功,

image-20240822104011041

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);//返回锁剩余有效期
        //判断ttl是否为null,如果为null说明获取锁成功
        if (ttl == null) {
            return true;
        } else {
            //最大等待时间-获取锁所用的时间=剩余等待时间
            time -= System.currentTimeMillis() - current;
            //如果消耗时间太长了,直接返回false,获取锁失败
            if (time <= 0L) {
                this.acquireFailed(waitTime, unit, threadId);
                return false;
            } else {
                //等待时间还有剩余,再次获取当前时间
                current = System.currentTimeMillis();
                //订阅别人释放锁的信号
                RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
                //在剩余时间内,等待这个信号,如果已经等待超时,取消订阅,并获取锁失败
                if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
                    //
                    if (!subscribeFuture.cancel(false)) {
                        subscribeFuture.onComplete((res, e) -> {
                            if (e == null) {
                                //取消订阅
                                this.unsubscribe(subscribeFuture, threadId);
                            }

                        });
                    }
                    //剩余时间内没等到,返回false
                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                } else {
                    try {
                        //如果剩余时间内等到了别人释放锁的信号,再次计算当前剩余最大等待时间
                        time -= System.currentTimeMillis() - current;
                        if (time <= 0L) {
                            //如果剩余时间为负数,则直接返回false
                            this.acquireFailed(waitTime, unit, threadId);
                            boolean var20 = false;
                            return var20;
                        } else {
                            boolean var16;
                            do {
                                //如果剩余时间等到了,dowhile循环重试获取锁
                                long currentTime = System.currentTimeMillis();
                                ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                                if (ttl == null) {
                                    var16 = true;
                                    return var16;
                                }

                                time -= System.currentTimeMillis() - currentTime;
                                if (time <= 0L) {
                                    this.acquireFailed(waitTime, unit, threadId);
                                    var16 = false;
                                    return var16;
                                }

                                currentTime = System.currentTimeMillis();
                                if (ttl >= 0L && ttl < time) {
                                    ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                                } else {
                                    ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                                }

                                time -= System.currentTimeMillis() - currentTime;
                            } while(time > 0L);

                            this.acquireFailed(waitTime, unit, threadId);
                            var16 = false;
                            return var16;
                        }
                    } finally {
                        this.unsubscribe(subscribeFuture, threadId);
                    }
                }
            }
        }
    }

tryAcquire继续调用tryAcquireAsync

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    
    if (leaseTime != -1L) {//查看是否设定默认失效时间
        return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        // 如果没有指定释放时间时间,则指定默认释放时间为getLockWatchdogTimeout,底层源码显示是30*1000ms,也就是30秒
        RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        //当上面的函数完成,则回调下面的函数
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
                //如果获取锁成功
                if (ttlRemaining == null) {
                    this.scheduleExpirationRenewal(threadId);
                }

            }
        });
        return ttlRemainingFuture;
    }
}

进入tryLockInnerAsync

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
    return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, 
"if (redis.call('exists', KEYS[1]) == 0) then 
     redis.call('hincrby', KEYS[1], ARGV[2], 1); 
     redis.call('pexpire', KEYS[1], ARGV[1]); 
     return nil; 
     end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
     redis.call('hincrby', KEYS[1], ARGV[2], 1); 
     redis.call('pexpire', KEYS[1], ARGV[1]); 
     return nil; 
end; 
return redis.call('pttl', KEYS[1]);", -- 代码走到这里,说明获取锁的不是自己,获取锁失败
Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
}

return redis.call('pttl', KEYS[1]);:代表锁获取失败,返回锁剩余有效期,毫秒级别

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 锁不存在
if (redis.call('exists', key) == 0) then
    -- 获取锁并添加线程标识,state设为1
    redis.call('hset', key, threadId, '1');
    -- 设置锁有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
-- 锁存在,判断threadId是否为自己
if (redis.call('hexists', key, threadId) == 1) then
    -- 锁存在,重入次数 +1,这里用的是hash结构的incrby增长
    redis.call('hincrby', key, thread, 1);
    -- 设置锁的有效期
    redis.call('expire', key, releaseTime);
    return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

回调函数到期更新:

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();  
    //不存在,才put,表明是第一次进入,不是重入,保证同一个锁
    ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        //如果是第一次进入,则跟新有效期
        entry.addThreadId(threadId);
        this.renewExpiration();
    }
}

更新有效期:

private void renewExpiration() {
    ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
    if (ee != null) {
        //Timeout是一个定时任务
        Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = (ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                if (ent != null) {
                    Long threadId = ent.getFirstThreadId();
                    if (threadId != null) {
                        //重置有效期
                        RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                        future.onComplete((res, e) -> {
                            if (e != null) {
                                RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                            } else {
                                if (res) {
                                    //然后调用自己,递归重置有效期
                                    RedissonLock.this.renewExpiration();
                                }

                            }
                        });
                    }
                }
            }
            //internalLockLeaseTime是之前WatchDog默认有效期30秒,那这里就是 30 / 3 = 10秒之后,才会执行
        }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
        ee.setTimeout(task);
    }
}

刷新有效期:

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
}

image-20240822104223035

5.5 分布式锁-redission锁的MutiLock原理

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

1653553998403

为了解决这个问题,redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

1653554055048

那么MutiLock 加锁原理是什么呢?笔者画了一幅图来说明

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

1653553093967

  • 我们先使用虚拟机额外搭建两个Redis节点
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.137.130:6379")
                .setPassword("root");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient2() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://92.168.137.131:6379")
                .setPassword("root");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient3() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://92.168.137.132:6379")
                .setPassword("root");
        return Redisson.create(config);
    }
}
  • 使用联锁,我们首先要注入三个RedissonClient对象
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;

private RLock lock;

@BeforeEach
void setUp() {
    RLock lock1 = redissonClient.getLock("lock");
    RLock lock2 = redissonClient2.getLock("lock");
    RLock lock3 = redissonClient3.getLock("lock");
    //创建联锁,multiLock
    lock = redissonClient.getMultiLock(lock1, lock2, lock3);
}

@Test
void method1() {
    boolean success = lock.tryLock();
    redissonClient.getMultiLock();
    if (!success) {
        log.error("获取锁失败,1");
        return;
    }
    try {
        log.info("获取锁成功");
        method2();
    } finally {
        log.info("释放锁,1");
        lock.unlock();
    }
}

void method2() {
    RLock lock = redissonClient.getLock("lock");
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,2");
        return;
    }
    try {
        log.info("获取锁成功,2");
    } finally {
        log.info("释放锁,2");
        lock.unlock();
    }
}
小结

image-20240822111447450

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

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

相关文章

【赵渝强老师】执行MySQL的冷备份与冷恢复

冷备份是指发生在数据库已经正常关闭的情况下进行的备份。由于此时数据库已经关闭&#xff0c;通过冷备份可以将数据库的关键性文件拷贝到另外存储位置。冷备份因为只是拷贝文件&#xff0c;因此备份的速度非常快。在执行恢复时&#xff0c;只需将文件再拷贝回去就可以很容易恢…

Vxe UI vue vxe-table 常用功能使用分享

Vxe UI vue vxe-table 常用功能使用分享 表格需求 如果你需要的是一个能够渲染简单场景的表格&#xff0c;那么使用主流 UI 库就够了&#xff0c;例如element ui自带的表格等。 如果你需要是一个能同时满足简单场景以及各类复杂场景、大数量的全功能表格&#xff0c;那么推荐…

YOLOv5改进 | 融合改进 | C3 融合Dilated Reparam Block提升检测效果【附代码+小白可上手】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录&#xff1a; 《YOLOv5入门 改…

电脑照片删除了怎么恢复回来?轻松三步,找回珍贵记忆

在数字时代&#xff0c;我们的电脑里储存了无数珍贵的照片&#xff0c;它们记录着生活的点滴&#xff0c;承载着美好的回忆。然而&#xff0c;一不小心将照片删除&#xff0c;往往会让人心急如焚。别担心&#xff0c;本文将为你详细介绍如何恢复电脑中被删除的照片&#xff0c;…

制造企业WMS库存盘点的应用

1.库存盘点的重要性与挑战 1.1 盘点目的与意义 库存盘点是制造工厂WMS系统中的一项基础性工作&#xff0c;其目的在于确保库存数据的准确性&#xff0c;从而为生产计划、物料需求计划、成本核算等提供可靠的数据支持。盘点的意义主要体现在以下几个方面&#xff1a; - 确保数…

PYUSD跃升为第六大稳定币:借势Solana和高APY的成功之道

随着加密市场的不断发展&#xff0c;稳定币在数字资产生态系统中的重要性日益凸显。PayPal旗下的美元稳定币PYUSD凭借强大的市场背景和策略性扩展&#xff0c;已经迅速成长为第六大稳定币。特别是在与Solana区块链的深度合作&#xff0c;以及高APY&#xff08;年化收益率&#…

数据可视化~~看板的切换设置+光滑折线图

目录 1.问题背景 2.安装模块 3.绘制柱状图的实现 4.对于图表的完善 5.重新思索 1.问题背景 我们想要通过这个用户的获得点赞的数量和她的粉丝数量的比值作为一个指标&#xff0c;我们想要绘制一个柱状图取值管的进行这个排名&#xff1b; 下面的这个是今天最后我们实现的…

飞睿智能10km无人机WiFi中继图传模块,视界无界,高速传输画质高清不卡顿、抗干扰

在无人机技术日新月异的今天&#xff0c;我们时常被那些高空翱翔的“小眼睛”所震撼。它们不仅为我们带来了未有的视觉体验&#xff0c;更在诸多领域发挥着举足轻重的作用。然而&#xff0c;要让无人机真正发挥其潜力&#xff0c;一个稳定、高效的图传系统至关重要。今天&#…

vue2 动态组件

文章目录 实现思路&#xff1a;组件注册动态组件嵌入的位置动态的tabPanes动态组件 - listComponent实际嵌入的组件 - invoiceListComponent 实现思路&#xff1a; 组件注册 组件的地址存储在表中&#xff0c;在xxx_components表中配置组件url components_key&#xff1a;组…

2024年最新股指期货交易手续费标准是多少?

股指期货交易手续费是指投资者在进行股指期货买卖时需要支付的费用&#xff0c;主要包括开仓手续费和平仓手续费。这些费用是交易所和期货公司为了维持市场运行和提供服务而收取的。 一、沪深300、上证50、中证500、中证1000股指期货手续费 日内交易&#xff1a;只要你在交易…

计算机中的「null」怎么读?

今天咱们来聊一个让无数程序员纠结的问题&#xff1a;“null”这个词到底该怎么读&#xff1f; 在开始讨论这个问题之前&#xff0c;我觉得有必要先带大家回忆一下我们曾经踩过的那些发音雷区。 尤其是那些英文术语&#xff0c;真的是一个坑接一个。比如这些常见的发音错误&am…

常见DDoS攻击之零日漏洞Zero-day Attacks

目录 一、什么是零日漏洞Zero-day Attacks 二、零日漏洞是如何转化为零日攻击的 三、常见的零日攻击类型 四、为什么零日攻击很危险 五、著名的零日攻击事件 六、如何降低零日攻击的风险 七、DDoS攻击防御解决方案&#xff08;定制化&#xff09; 7.1 产品优势 7.2 产品…

Vue - 详细介绍wow.js滚动触发动画组件(Vue2 Vue3)

Vue - 详细介绍wow.js滚动触发动画组件&#xff08;Vue2 & Vue3&#xff09; 在日常网页中&#xff0c;我们难免会用到CSS动画来对某些元素进行显示隐藏&#xff0c;在wowjs中可根据浏览器滚动来触发对应的CSS动画&#xff0c;并且可设置多种animate动画类型、动画持续时间…

速卖通自养号测评:安全高效提升产品销量的秘诀

速卖通自养号测评是跨境电商卖家为了提升产品销量、评价数量及排名而采取的一种策略。以下是对速卖通自养号测评的详细解析&#xff1a; 一、自养号测评的定义 自养号测评&#xff0c;顾名思义&#xff0c;是指卖家自行培养并管理买家账号&#xff0c;通过模拟真实买家的购物…

重新认识一下,从BIO到NIO,再到AIO,响应式编程

Netty 的高性能架构&#xff0c;是基于一个网络编程设计模式 Reactor 进行设计的。现在&#xff0c;大多数与 I/O 相关的组件&#xff0c;都会使用 Reactor 模型&#xff0c;比如 Tomcat、Redis、Nginx 等&#xff0c;可见 Reactor 应用的广泛性。 Reactor 是 NIO 的基础。为什…

WordPress中最佳免费WooCommerce主题推荐:专家级指南

在电商领域&#xff0c;每个创业者的梦想是拥有一个既功能强大又美观的在线商店。对于已经具备一定建站经验的专家级用户来说&#xff0c;重点是选择一款功能强大且灵活性高的WooCommerce主题。在这篇文章中&#xff0c;我将为大家推荐几款适合专家级用户的免费WooCommerce主题…

javaer选手快速入门grpc

本文前置内容 需要学过java微服务开发,至少知道使用过openfeign和dubbo等rpc微服务组件的相关内容 相信已经学习到微服务开发的对grpc或多或少都有了解,高效的性能和protobuf这样轻量序列化的方式 无论是go开发必学还是java 使用dubbo或者其他深入也是需要了解的 相关概念 Pro…

使用js代码模拟React页面中input文本框输入

遇到的问题&#xff1a; 使用js代码模拟input框中输入指定的字符串&#xff0c;在浏览器调试页面能看到输入框的文字已经变成我需要的文字&#xff0c;但是只要我点击输入框&#xff0c;或者页面上的其他输入框&#xff0c;输入框的文字就清空了。 解决过程和方法&#xff1a; …

火语言RPA流程组件介绍--睡眠等待

&#x1f6a9;【组件功能】&#xff1a;休眠等待指定时间后再恢复执行 阻塞当前流程继续向下运行&#xff0c;暂停等待指定时间&#xff0c;一般当上一组件操作需要缓冲时间或完成太快需要休眠观测时使用此组件。 配置预览 配置说明 延迟时间 支持T或# 输入仅支持整型 阻塞…

[Linux#44][线程] CP模型2.0 | 信号量接口 | 基于环形队列

目录 1.回忆 Task.hpp 1. #pragma once 2. 头文件和命名空间 3. 类 CallTask 4. 操作符字符串 5. 回调函数 mymath 阻塞队列 BlockQueue 的实现 BlockQueue 生产者和消费者线程 生产者productor 消费者 consumer 主函数 main 代码整体说明 2. 信号量 2.1 回忆&…