【自学笔记】Redis 飞快入门

news/2025/10/1 15:47:01/文章来源:https://www.cnblogs.com/slgkaifa/p/19122481

【自学笔记】Redis 飞快入门

前言

本文主要是以快速了解Redis、快速写出用Redis解决常见业务代码为主,不会涉及到太多的细节,即使不敲一行代码也能让你有所收获,适合面向快速上手Redis的小伙伴。

视频教程:黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目_哔哩哔哩_bilibili

学习的源码:

本文主要是记录本人自学的过程,有参考其他笔记~

如果对你有帮助,可以看看我的主页,里面有很多个人总结的Redis的面试热题,喜欢的话点个赞或者关注贝~

一、基础篇

1.Redis是什么

RedisRemote Dictionary Server)是一个开源的、基于内存的键值存储系统(Key-Value Store),也是一种键值型的NoSql数据库。它通常被用作数据库缓存消息中间件流引擎

Redis的官方网站地址:Redis - The Real-time Data Platform

主要特点:

  • 键值(key-value)型,value支持多种不同数据结构,功能丰富

  • 单线程,每个命令具备原子性

  • 低延迟,速度快(基于内存.IO多路复用.良好的编码)。

  • 支持数据持久化

  • 支持主从集群.分片集群

  • 支持多语言客户端

2.Redis有什么用

  1. 缓存: 最常用场景,加速数据访问(如数据库查询结果、页面片段)。

  2. 会话存储: 存储用户会话信息(如登录状态)。

  3. 排行榜: 利用 Sorted Set(如点赞最早用户)。

  4. 计数器: 利用 INCR/DECR(如浏览量、点赞数)。

  5. 消息队列: 利用 List(简单队列)或 Streams(更复杂的消息队列,支持消费者组、消息确认)。

  6. 实时系统: 利用 Pub/Sub(如简单的聊天室、通知)。

  7. 地理位置应用: 利用 Geospatial Indexes。

  8. 标签系统/社交关系: 利用 Set(如共同好友、兴趣标签)。

  9. 分布式锁: 利用 SET key value NX PX milliseconds(或 SETNX + EXPIRE,需注意原子性问题)。

  10. 限流: 利用计数器 + 过期时间(如 INCR + EXPIRE)或更复杂的算法(如滑动窗口,可用 ZSet 实现)。

3.Redis怎么用

3.1.安装Redis

这里是基于linux服务器来部署的,因为在linux服务器里才能发挥Redis的最大性能。

1)安装依赖库

Redis是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖:

yum install -y gcc tcl

注意:现在需要对yum换源才能安装成功,参考视频:CentOS更换yum源哔哩哔哩bilibili

2)上传安装包并解压

我放到了/usr/local/src 目录,选择该文件src用终端打开,然后输入命令解压:

tar -xzf redis-6.2.6.tar.gz

选择解压后的文件redis-6.2.6用终端打开,然后输入命令运行编译:

make && make install

默认的安装路径是在 /usr/local/bin目录下,该目录已经默认配置到环境变量,因此可以在任意目录下运行命令。

  • redis-cli:是redis提供的命令行客户端

  • redis-server:是redis的服务端启动脚本(默认启动)

  • redis-sentinel:是redis的哨兵启动脚本

3)相关配置

要让Redis可以后台启动,则必须修改Redis配置文件,就在我们之前解压的redis安装包下(/usr/local/src/redis-6.2.6),名字叫redis.conf:

我们先将这个配置文件备份一份:

cp redis.conf redis.conf.bck

然后修改redis.conf文件中的一些配置:(重要)

# 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问,外界访问都是拒绝。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0(已修改)
bind 0.0.0.0
# 守护进程,修改为yes后即可后台运行(已修改)
daemonize yes
# 密码,设置后访问Redis必须输入密码(已修改)
requirepass 123456

4)开机自启

首先,新建一个系统服务文件:

vi /etc/systemd/system/redis.service

内容如下:

[Unit]
Description=redis-server
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf
PrivateTmp=true
[Install]
WantedBy=multi-user.target

然后重载系统服务:

systemctl daemon-reload

执行下面的命令,可以让redis开机自启:

systemctl enable redis

5)idea连接Redis

点击右侧的database,点击“+”号

输入你的Host(IP地址)和Password

点击测试连接

3.2.Redis数据结构

Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:

基本类型

数据类型存储结构常用命令典型应用场景特性说明
String二进制安全的字符串SETGETINCRDECR缓存、计数器、分布式锁可存文本、数字(≤512MB)
Hash键值对集合(类似 Map)HSETHGETHGETALL存储对象(如用户信息、商品属性)适合存储结构化数据
List双向链表LPUSHRPOPLRANGE消息队列、最新消息列表、历史记录支持按索引操作,元素可重复
Set无序集合(元素唯一)SADDSMEMBERSSINTER标签系统、好友关系、抽奖(去重)支持交并差集运算
Sorted Set有序集合(元素唯一+分数排序)ZADDZRANGEZRANK排行榜、延迟队列、带权重的任务调度通过分数(score)自动排序

高级类型

数据类型用途示例命令场景案例
Bitmaps位操作(节省空间的布尔存储)SETBITGETBITBITCOUNT用户签到统计、实时活跃度分析
HyperLogLog近似去重计数(误差率 0.81%)PFADDPFCOUNT大规模 UV 统计(如每日访问用户数)
GEO地理位置存储与查询GEOADDGEORADIUS附近的人、商家定位
Stream消息流(支持消费者组)XADDXREADXGROUP类似 Kafka 的消息队列(Redis 5.0+)
Bloom Filter高效存在性判断(需模块扩展)BF.ADDBF.EXISTS防止缓存穿透、垃圾邮件过滤

3.3.Redis常见命令(了解)

1)通用命令:

  • KEYS:查看符合模板的所有key

  • DEL:删除一个指定的key

  • EXISTS:判断key是否存在

  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除

  • TTL:查看一个KEY的剩余有效期

2)String的常见命令有:

  • SET:添加或者修改已经存在的一个String类型的键值对

  • GET:根据key获取String类型的value

  • MSET:批量添加多个String类型的键值对

  • MGET:根据多个key获取多个String类型的value

  • INCR:让一个整型的key自增1

  • INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2

  • INCRBYFLOAT:让一个浮点类型的数字自增并指定步长

  • SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行

  • SETEX:添加一个String类型的键值对,并且指定有效期

3)Hash类型的常见命令

  • HSET key field value:添加或者修改hash类型key的field的值

  • HGET key field:获取一个hash类型key的field的值

  • HMSET:批量添加多个hash类型key的field的值

  • HMGET:批量获取多个hash类型key的field的值

  • HGETALL:获取一个hash类型的key中的所有的field和value

  • HKEYS:获取一个hash类型的key中的所有的field

  • HINCRBY:让一个hash类型key的字段值自增并指定步长

  • HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

4)List的常见命令有:

  • LPUSH key element ... :向列表左侧插入一个或多个元素

  • LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil

  • RPUSH key element ... :向列表右侧插入一个或多个元素

  • RPOP key:移除并返回列表右侧的第一个元素

  • LRANGE key star end:返回一段角标范围内的所有元素

  • BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil

5)Set类型的常见命令

  • SADD key member ... :向set中添加一个或多个元素

  • SREM key member ... : 移除set中的指定元素

  • SCARD key: 返回set中元素的个数

  • SISMEMBER key member:判断一个元素是否存在于set中

  • SMEMBERS:获取set中的所有元素

  • SINTER key1 key2 ... :求key1与key2的交集

  • SDIFF key1 key2 ... :求key1与key2的差集

  • SUNION key1 key2 ..:求key1和key2的并集

6)SortedSet的常见命令有:

  • ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值

  • ZREM key member:删除sorted set中的一个指定元素

  • ZSCORE key member : 获取sorted set中的指定元素的score值

  • ZRANK key member:获取sorted set 中的指定元素的排名

  • ZCARD key:获取sorted set中的元素个数

  • ZCOUNT key min max:统计score值在给定范围内的所有元素的个数

  • ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值

  • ZRANGE key min max:按照score排序后,获取指定排名范围内的元素

  • ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素

  • ZDIFF.ZINTER.ZUNION:求差集.交集.并集

3.4.Redis的Java客户端

在Redis官网中提供了各种语言的客户端,地址:https://redis.io/docs/clients/

标记为❤的就是推荐使用的java客户端,包括:

  • Jedis和Lettuce:这两个主要是提供了Redis命令对应的API,方便我们操作Redis,而SpringDataRedis又对这两种做了抽象和封装,因此我们后期会直接以SpringDataRedis来学习

  • Redisson:是在Redis基础上实现了分布式的可伸缩的java数据结构,例如Map.Queue等,而且支持跨进程的同步机制:Lock.Semaphore等待,比较适合用来实现特殊的功能需求。

二、实战篇

这部分主要是以黑马点评项目的后端为主,一边做项目一边学习Redis的核心知识,并且利用Redis解决常见的问题。

1.基于Redis实现短信登录

1.1.基于session实现登录

关于使用session校验登录状态:

用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行。

UserServiceImpl:

发送验证码:

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号。如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

@Override
public Result sendCode(String phone, HttpSession session) {// 1.校验手机号格式if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到 sessionsession.setAttribute("code",code);// 5.发送验证码log.debug("发送短信验证码成功,验证码:{}", code);// 返回okreturn Result.ok();
}

登录/注册:

用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.校验验证码Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if(cacheCode == null || !cacheCode.toString().equals(code)){//3.不一致,报错return Result.fail("验证码错误");}//一致,根据手机号查询用户User user = query().eq("phone", phone).one();//5.判断用户是否存在if(user == null){//不存在,则创建user =  createUserWithPhone(phone);}//6.保存用户信息到session中session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));return Result.ok();
}

1.2.Redis代替session实现

redis数据本身就是共享的(主从复制/搭建redis集群/哨兵模式) ,使用Redis解决session不能共享的问题。由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希。

使用string结构来存储验证码

使用hash结构存储登录用户状态信息

需求分析:

1)发送验证码

用户点击发送验证码时,后端会先校验手机号格式,格式正确则随机生成验证码code,以前缀+手机号作为key,以string格式存储验证码code作为value,保存在redis中,最后记得添加过期时间。

2)用户登录

当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建用户,并且以前缀+token作为key,以对象转map的hash格式作为value,最后将用户数据保存到redis。

当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截(拦截这块就不细讲了),如果存在则将其保存到threadLocal中,并且放行。

代码:

修改UserServiceImpl:

发送短信:

@Override
public Result sendCode(String phone, HttpSession session) {// 1.校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到 redis//加前缀,和其他业务区分开stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5.发送验证码log.debug("发送短信验证码成功,验证码:{}", code);// 返回okreturn Result.ok();
}

登录/注册:

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.从redis获取验证码并校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到 redis中// 7.1.随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.存储String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);
}

2.基于缓存实现查询商户

2.1.关于缓存

Redis 缓存就是介于应用程序和持久化数据库(如 MySQL、PostgreSQL)之间的一层高速内存存储。它的目的是减少应用程序直接访问慢速数据库的次数,从而显著提升系统的响应速度和吞吐量。

简单工作流程如下(最简单、最实用的旁路缓存模式):

1)读请求流程(Cache-Aside)

  1. 接收请求:应用程序收到一个数据查询请求(例如,根据商品ID查询商品信息)。

  2. 查询缓存:应用程序首先尝试从 Redis 缓存中读取该数据。

  3. 缓存命中:如果数据在缓存中存在,则直接返回给客户端,流程结束。全程不访问数据库

  4. 缓存未命中:如果数据在缓存中不存在,应用程序会去查询数据库。

  5. 数据库查询:从数据库(如 MySQL)中取出数据。

  6. 回填缓存:应用程序将从数据库取出的数据,写入到 Redis 缓存中,并设置一个过期时间。

  7. 返回数据:最后将数据返回给客户端。

2)写请求流程

  1. 更新数据库:应用程序首先直接更新数据库。

  2. 删除缓存:然后,删除 Redis 中对应的缓存数据。

项目例子:

添加商户缓存:

@Override
public Result queryById(Long id) {String key ="cache:shop:"+id;//1.先从redis查询店铺缓存String shopJson=stringRedisTemplate.opsForValue().get(key);//2.判断是否命中if(StrUtil.isNotBlank(shopJson)){//3.命中,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//4.不存在,则跟据id查询数据库Shop shop=getById(id);//5.依然不存在。返回错误if(shop==null){return Result.fail("店铺不存在!");}//6.存在,写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));//7.返回数据return Result.ok(shop);
}

2.2.缓存更新策略

1)内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

2)超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存(推荐)

3)主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题(推荐)

2.3.解决缓存一致性

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在。

缓存一致性问题主要出现在写操作,有以下几个解决方案:

方案1:先更新数据库,再删除缓存(Cache-Aside)(推荐)

方案2:延迟双删(对抗并发不一致)

方案3:订阅数据库 Binlog 异步删除(最终一致)

方案4:强一致性方案:分布式锁

如果是要考虑实时一致性的话,先写 MySQL,再删除 Redis 应该是较为优的方案,虽然短期内数据可能不一致(且发生概率较小),不过其能尽量保证数据的一致性。接下来着重讲讲Cache-Aside

采用方案一:Cache-Aside

1)为什么采用?

如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来

2)为什么是“删除”缓存而不是“更新”缓存?
这是一种保守但安全的设计。如果先更新缓存再更新数据库,或者两个操作非原子性,在并发环境下可能导致数据不一致(缓存中是的新数据,但数据库还是旧数据)。直接删除缓存,迫使下次读请求时从数据库加载最新数据(即执行上面的“读请求流程”),虽然可能有一次缓存未命中,但保证了数据的最终一致性。

3)为什么是先更新数据库再删除缓存?

原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

(注意这里右边的图,也是失败的情况,对应下面缺点的图,是一种小概率事件)

4)缺点?

Cache-Aside 的不一致场景:

但是发生条件苛刻:需要满足(1)缓存刚好失效(2)读操作比写操作慢(数据库压力大或网络延迟)两个条件。概率较低

回到项目

需求:

使用旁路缓存实现商铺和缓存与数据库双写一致

1)跟据id查询商户:根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

2)更新商品信息:根据id修改店铺时,先修改数据库,再删除缓存

代码:

修改ShopServiceImp

@Override
public Result queryById(Long id) {//...//存在,写入redis,30分钟过期stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);//...
}
@Override
@Transactional
public Result update(Shop shop) {Long id=shop.getId();if(id==null){return Result.fail("店铺id不能为空!");}//1.先修改数据库updateById(shop);//2.再删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY+id);return Result.ok();
}

2.4.解决缓存穿透

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

解决方案:

方案一:缓存空对象(推荐)

即使从数据库没查到,也把一个空值(如 null)或特殊标记写入缓存,并设置一个较短的过期时间。后续请求就会命中这个空值,防止直接打到数据库。

优点:实现简单

缺点:额外的内存消耗

方案二:布隆过滤器

隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断可能存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中;假设布隆过滤器判断这个数据不存在,则直接返回null。

优点:节约内存空间

缺点:存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

回到项目

需求:修改跟据id查询商户的业务,使用互斥锁/逻辑过期来解决返缓存穿透问题

方案一:缓存空对象解决缓存穿透

代码:

/*** 缓存空对象解决缓存穿透*/
public  R queryWithPassThrough(String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误(缓存空对象)if (r == null) {// 将空值写入redisthis.set(key, "", CACHE_NULL_TTL, TimeUnit.SECONDS);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;
}

2.5.解决缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key(热key)突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案:

方案一:互斥锁

当缓存失效时,不是所有请求都去访问数据库。而是让第一个请求去查询数据库并重建缓存,其他请求则等待或重试,等待第一个请求完成后再从缓存中读取。

可以使用 Redis 的 SETNX(SET if Not eXists)命令来实现分布式锁。

方案二:逻辑过期

不设置 Redis 的物理过期时间。而是在缓存的值对象中,额外存储一个逻辑过期时间字段。

请求命中缓存后,检查值中的逻辑过期时间。如果数据未逻辑过期,直接返回。如果数据已逻辑过期,则尝试获取互斥锁,然后由一个线程异步地去更新缓存,其他线程先返回旧的、过期的数据。

方案对比:

回到项目

需求:修改跟据id查询商户的业务,使用互斥锁/逻辑过期来解决返缓存击穿问题

方案一:利用互斥锁解决缓存击穿问题

代码:

/*** 互斥锁解决缓存击穿*/
public  R queryWithMutex(String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;//1.先从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否命中if (StrUtil.isNotBlank(json)) {//3.命中,直接返回return JSONUtil.toBean(json, type);}//判断命中的值是否是空值if (json != null) {//返回一个错误信息return null;}//4.不存在,实现缓存重构//4.1获取互斥锁String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);//4.2没拿到锁,则休眠重试(递归重试)if (!isLock) {Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}//4.3拿到锁,跟据id查询数据库r = dbFallback.apply(id);//5.不存在,返回错误,同时将控制写入redis(防止缓存穿透?),2分钟有效期if (r == null) {this.set(key, "", CACHE_NULL_TTL, TimeUnit.SECONDS);return null;}//6.将数据写入redisthis.set(key, r, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {//7.释放互斥锁unlock(lockKey);}return r;
}

方案二:利用逻辑过期事件解决缓存击穿问题

代码:

本质上这个热key永远都没有物理过期,只有不断更新逻辑过期时间(小疑问:为什么R newR = dbFallback.apply(id);后面不需要判空?)

/*** 利用逻辑过期时间解决缓存击穿*/
public  R queryWithLogicalExpire(String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否命中if (StrUtil.isBlank(json)) {// 3.不命中,直接返回nullreturn null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return r;}// 6.已过期,缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock) {// 6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R newR = dbFallback.apply(id);// 重建缓存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;
}

2.6.解决缓存雪崩

缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 随机过期时间:在设置 Key 的过期时间时,增加一个随机值(例如,基础时间 + 一个几分钟的随机数)。

  • 利用Redis集群提高服务的可用性:使用 Redis Sentinel(哨兵) 或 Redis Cluster(集群) 模式,实现主从切换和故障自动转移。即使个别节点宕机,整个缓存层依然能提供服务。

  • 给缓存业务添加降级限流策略:在应用系统中,引入 HystrixSentinel 等组件。当检测到数据库压力过大或大量请求超时时,启动服务降级策略。

  • 给业务添加多级缓存:Redis + Caffeine

总解决方案

因为后期不止查询商户需要用到缓存,其他业务也可能用到,因此将缓存穿透、击穿解决方案封装成一个工具类CacheClient

1)新建一个实体类,用于封装逻辑过期数据

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

2)基于StringRedisTemplate封装一个缓存工具类 CacheClient(核心代码将上面的放进去即可)

/*** 缓存工具类*/
@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;//创建一个固定大小的线程池,主要用于缓存重建等后台任务private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//构造器注入可以避免循环依赖public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}//存储数据到redis,设置过期时间public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}//存储数据到redis,不设置过期时间,但是有逻辑过期时间public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入/更新RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}//---------------------------------------------------------------------------------------------------------------------/*** 缓存空对象解决缓存穿透*/public  R queryWithPassThrough(String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {//...}/*** 互斥锁解决缓存击穿*/public  R queryWithMutex(String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {//...}/*** 利用逻辑过期时间解决缓存击穿*/public  R queryWithLogicalExpire(String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {//...}//---------------------------------------------------------------------------------------------------------------------private boolean tryLock(String key) {//如果键不存在则新增,存在则不改变已经有的值。同时,缓存命中返回 false,不命中返回 true。Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);//防止拆箱return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
}

在ShopServiceImpl中应用:

@Override
public Result queryById(Long id) {//以下选择一个场景即可//1.解决缓存穿透Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById,CACHE_SHOP_TTL, TimeUnit.MINUTES);//2.解决缓存击穿(互斥锁)
//        Shop shop1 = cacheClient.queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById,
//                CACHE_SHOP_TTL, TimeUnit.MINUTES);//3.解决缓存击穿(设置逻辑过期时间)
//        Shop shop2 = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById,
//                20L, TimeUnit.SECONDS);if (shop == null) {return Result.fail("店铺不存在!");}return Result.ok(shop);
}

3.优惠券秒杀(重点)

3.1.Redis实现全局唯一id

需求:

用户抢购秒杀券时,会生成订单保存到数据库中,需要保证生成的订单id规律性不能太明显,同时不能受到表单容量影响,即id不能自增长

解决方案:

我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

ID的组成部分:符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

代码:

全局id生成器,自增长(Redis Incr 命令将 key 中储存的数字值增一。)

@Component
public class RedisIdWorker {//开始时间戳private static final long BEGIN_TIMESTAMP = 1640995200L;//序列号的位数private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix) {//1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;//2.生成序列号//2.1.获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//2.2.自增长(Redis Incr 命令将 key 中储存的数字值增一。)long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);//3.拼接时间戳和序列号return timestamp << COUNT_BITS | count;}
}

测试:

使用线程池异步测试,需要保证所有分线程全部走完之后,主线程再走。最后统计总时间

Countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题,有两个重要方法:countDown和await。

使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testIdWorker() throws InterruptedException {//当CountDownLatch  内部维护的 变量变为0时,就不再阻塞CountDownLatch latch = new CountDownLatch(300);Runnable task = () -> {for (int i = 0; i < 100; i++) {long id = redisIdWorker.nextId("order");System.out.println("id = " + id);}//调用一次countDown ,内部变量就减少1latch.countDown();};long begin = System.currentTimeMillis();for (int i = 0; i < 300; i++) {es.submit(task);}//这里的await是主线程等待上面的线程结束(异步),上面的for里面的不会等待task执行完才跳下一次latch.await();long end = System.currentTimeMillis();System.out.println("time = " + (end - begin));
}

实现秒杀下单

需求:

当用户开始进行下单,我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件。比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

代码:

注意,现在这个需求会衍生出一系列问题,这是最开始的实现代码,我们会逐步修改优化

@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("库存不足!");}//5,扣减库存boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).update();if (!success) {//扣减库存return Result.fail("库存不足!");}//6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1.订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2.用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3.代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);return Result.ok(orderId);
}

3.2.乐观锁解决超卖问题

问题:

假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。

解决方案:

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁,而对于加锁,我们通常有两种解决方案:

采用乐观锁来解决:

假设现在版本号version 是1,在减少库存操作时,对版本号进行+1 操作,且要求version 如果是1 的情况下,才能操作,第一个线程来了,他自己满足version=1,因此能顺利执行,数据库的version变为2,此时即使第二个线程来了也需要加上条件version =1,但是现在由于线程1已经操作过了,所以线程2,操作时就不满足version=1 的条件了,所以线程2无法执行成功。

代码:

createVoucherOrder,结合业务,我们修改一下方案,将version+1去掉,且version条件改成stock大于0,即每次操作前都要找出对应券的id并且库存需要大于0才能修改!

注意,这里并不是又回到了最开始的超卖问题,最开始的是先判断id和stock>0,然后再修改,而现在是在准备修改之前再判断id和stock>0,是可以大幅减少失败的情况

boolean success = seckillVoucherService.update().setSql("stock= stock -1").eq("voucher_id", voucherId).gt("stock",0).update(); //where id = ? and stock > 0

3.3.分布式锁实现一人一单

3.3.1.引出一系列问题

问题:

目前的情况是,一个人可以无限制的抢这个优惠卷,所以我们应当增加一层逻辑,让一个用户只能下一个单,而不是让一个用户下多个单,但是这样会出现并发过来,查询数据库,都不存在订单

解决方案:

针对上面的问题,我们可以将seckillVoucher方法的查询订单、扣减库存、创建订单的操作额外封装成一个方法createVoucherOrder,因为现在是要确保线程安全插入数据完整,要同时添加@Transactional注解和synchronized关键字

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {//查询订单//扣减库存//创建订单
}

衍生问题:

如果你在方法外部加锁,则锁粒度太粗,会导致每个线程进来都会锁住,根本区分不了是不是同一个用户的操作

如果你在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放也会导致问题(当前方法被spring的事务@Transactional控制 )

解决方案:

我们要保证同一个用户操作需要加锁,而不同的用户之间不需要加锁因此要针对用户id来进行加锁,我们选择将当前方法整体包裹起来,确保事务不会出现问题。

要注意seckillVoucher调用createVoucherOrder方法的时候,其实是this.的方式调用的,事务不能生效,所以需要获得原始的事务对象, 来操作事务(获取代理对象,才能使得事务生效!

//intern() 这个方法是从常量池中拿到数据,
//如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象,而是new出来的对象,
//我们使用锁必须保证锁必须是同一把,所以我们需要使用intern()方法
synchronized (userId.toString().intern()) {//获取代理对象,才能使得事务生效!(不能直接使用this)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
}

衍生问题:

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。比如现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,

解决方案:

使用分布式锁,也就是接下来要讲的

3.3.2.Redis分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路

分布式锁种类:

我们选择第二种

核心思路:

我们利用redis 的setNx 方法,当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了1,如果结果是1,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的哥们,等待一定时间后重试即可

3.3.3.实现分布式锁版本一

问题:

1)锁误删操作(即锁已过期自动释放了但是依旧执行释放锁操作)

2)释放锁时恰好锁过期释放了但是卡顿过后依然执行释放操作(即无效比锁操作)

解决方案:

1)使用前缀+用户id作为key,使用随机uuid+线程id作为value,进行加锁,释放锁时,也需要比较key才能释放。

2)修改释放锁的操作,需要通过执行lua脚本来比较是否是是同一个锁,是则释放,否则返回0。即比索、删锁是一个原子性操作。

(关于lua脚本:https://www.runoob.com/lua/lua-tutorial.html)

代码:

写个接口ILock,将抢锁和释放锁写成一个工具类

public interface ILock {//获取锁boolean tryLock(long timeoutSec);//释放锁void unlock();
}

实现类

/*** setnx实现分布式锁*/
public class SimpleRedisLock implements ILock {private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";//Spring Data Redis提供的类,用于封装Redis Lua脚本private static final DefaultRedisScript UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));//设置Lua脚本文件位置UNLOCK_SCRIPT.setResultType(Long.class);//指定脚本返回结果类型为Long}private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId = ID_PREFIX + Thread.currentThread().getId();// 获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//调用lua脚本//参数分别是:要执行的Lua脚本、Redis键列表(这里只有一个键)、脚本参数(这里是线程ID)stringRedisTemplate.execute(UNLOCK_SCRIPT,//Lua脚本Collections.singletonList(KEY_PREFIX+name),// KEYS[1]:锁的键ID_PREFIX+Thread.currentThread().getId()// ARGV[1]:线程标识);}
}

lua脚本:

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then-- 释放锁 del keyreturn redis.call('del', KEYS[1])
end
return 0

修改seckillVoucher代码

@Override
public Result seckillVoucher(Long voucherId) {//...//创建锁对象(新增代码)SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁对象boolean isLock = lock.tryLock(1200);//加锁失败if (!isLock) {return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}
}

3.3.4.Redission分布式锁

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

同时Redisson 的分布式锁(RLock)是 Redis 分布式锁的生产级实现,它解决了手动实现中的诸多痛点(如锁续期、可重入、原子性等)

简单使用:

1)引入依赖

org.redissonredisson3.13.6

2)配置Redission客户端

注意这里的地址要填自己的ip,我这是linux的ip

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){// 配置Config config = new Config();config.useSingleServer().setAddress("redis://192.168.111.131:6379").setPassword("123456");// 创建RedissonClient对象return Redisson.create(config);}
}

3)测试

@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();}}
}

3.3.4.实现分布式锁版本二

问题:

上面我们解决了锁误删问题、拿锁比锁删锁原子性问题,现在还有四个问题

1)重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

2)不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁

3)超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患

4)主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

解决方案:

我们依次解决

1)Redission解决重入问题

在juc的Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1

在redission中,我们的也支持可重入锁,他采用hash结构用来存储锁,其中大key表示表示这把锁是否存在,用小key(Field )表示当前这把锁被哪个线程持有,Value 为重入计数器,同一线程多次加锁时计数器递增,解锁时递减至 0 才释放锁。

示例代码:

RLock lock = redisson.getLock("my_lock");
lock.lock();  // 第一次加锁:Hash 中 my_lock 的 "uuid:threadId" = 1
try {lock.lock();  // 第二次加锁(重入):"uuid:threadId" = 2// ...
} finally {lock.unlock();  // 第一次解锁:"uuid:threadId" = 1lock.unlock();  // 第二次解锁:计数归零 → 删除锁
}

lua脚本:

-- KEYS[1] = 锁名称(如 my_lock)
-- ARGV[1] = 锁超时时间(毫秒)
-- ARGV[2] = 客户端ID + 线程ID(如 b5a5e582-...:1)
-- 1. 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then-- 不存在:创建 Hash 并设置重入次数=1redis.call('hincrby', KEYS[1], ARGV[2], 1);-- 设置锁的超时时间redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
-- 2. 锁已存在,检查当前线程是否持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then-- 是持有者:重入次数+1redis.call('hincrby', KEYS[1], ARGV[2], 1);-- 刷新锁的超时时间redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
-- 3. 锁被其他线程持有:返回锁剩余存活时间(毫秒)
return redis.call('pttl', KEYS[1]);

2)Redission解决不可重试问题

3)Redission解决超时释放问题

加锁成功后(未显式指定超时时间时),启动后台守护线程,默认每 10 秒检查锁持有状态。若业务未完成,自动将锁过期时间续期至默认 30 秒,避免业务执行超时导致锁意外释放。

-- KEYS[1] = 锁名称
-- ARGV[1] = 续期时间(默认 30s)
-- ARGV[2] = 客户端ID + 线程ID
-- 检查当前线程是否仍持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then-- 持有锁:刷新超时时间为 30sredis.call('pexpire', KEYS[1], ARGV[1]);return 1;
end;
return 0;

3.4.阻塞队列实现秒杀优化

4.基于Set实现点赞功能

5.基于ZSet实现点赞排行榜

6.基于Set实现用户共同关注

7.基于ZSet实现Feed流

8.基于GEO实现查看附近商户

9.基于BitMap实现用户签到统计

10.UV统计

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

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

相关文章

做网站工作室名字安徽二建标准

参考链接&#xff1a; Python中的私有变量 我们这里就直奔主题&#xff0c;不做基础铺垫&#xff0c;默认你有一些Python类的基础&#xff0c;大家在看这篇博客的时候&#xff0c;如果基础知识忘了&#xff0c;可以去菜鸟教程 从一个简单的类开始 class A(): #定义一…

强连通,Tarjan,缩点

在本文中,我们用 \(f(x,y)=1\) 来表示 \(x\) 可以到达点 \(y\),用 \(g(x,y)=1\) 表示 \(f(x,y)=1\) 且 \(f(y,x)=1\)。 I、强连通 对于图 \(U\) 上的任意两点 \(x\) 和 \(y\),如果有 \(g(x,y)=1\),那么称 \(x,y\) …

实用指南:K8s日志架构:Sidecar容器实践指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

做网站时量宽度的尺子工具thinkphp做的网站源码

目录结构 全局文件 1.app.json 文件 用来对微信小程序进行全局配置&#xff0c;决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。文件内容为一个 JSON 对象。 1.1 page用于指定小程序由哪些页面组成&#xff0c;每一项都对应一个页面的 路径&#xff08;含文…

企业网站建设综合实训心得wordpress安装系统

http://home.cnblogs.com/blog/转载于:https://www.cnblogs.com/yanyanhappy/archive/2012/09/07/2675050.html

彩票网站自己可以做吗wordpress加密页面访问

文章目录 一、 Zookeeper常用命令1. zk服务命令2. zk客户端命令 二、HBASE常见运维命令1. 集群启动关闭2. 扩容增加regionserver3. 下线regionserver ing 一、 Zookeeper常用命令 例如&#xff1a;ZOOKEEPER_HOME&#xff1a;/opt/zk/zookeeper 1. zk服务命令 1. 启动ZK服务…

Python方案--交互式VR教育应用开发

Python方案--交互式VR教育应用开发pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&…

纯Qt代码实现onvif协议设备端/onvif设备模拟器/onvif虚拟监控设备/桌面转onvif

一、前言说明 在视频监控系统的开发中,ONVIF(Open Network Video Interface Forum)作为行业标准协议,被广泛应用于设备与平台之间的互联互通。通常我们认为,ONVIF 协议的设备端实现应运行在摄像头等嵌入式下位机上…

高中教做网站的软件表格制作教程 步骤

可能是明月好久没有使用境外服务器挂载境外的云盘缘故吧,今天一个代维客户需要他的Linux服务器挂载谷歌云盘好进行云备份,本来是个很简单的事儿,没想到在rclone连接谷歌云盘的时候卡壳了,可是把明月给难为坏了,搜索到的简体中文教程倒是很多,但没有一个提到这个“坑”,最…

OI 笑传 #13

zatto今天是思维题大手子。 CF2130B 左转这个东西很烦,把它规约掉。 由于是一定要到 \(n\) 的,因此左转之后必须要右转,考虑单位元,也就是左走一格之后往右走一格是怎么个事。也就是多加一倍这两个格子里的数。 考…

*补*““逆元求组合数”(费马小定理

组合数快速求法 #include <bits/stdc++.h> #define ll long long #define MAXN 1010 using namespace std; namespace SHuxinn{ll pri[MAXN];ll ni[MAXN];ll ans1 , ans2;ll pow(ll a , ll b){ll ans = 1 , base…

C# WPF中Binding的 Source属性和ElementName属性有什么区别

好的,这是一个WPF数据绑定中非常核心和常见的问题。Source 和 ElementName 都是用来设置绑定源(即数据的来源)的属性,但它们的应用场景和灵活性有显著区别。 下面通过一个对比表格和详细解释来说明它们的区别。 核…

Typora to Obsidian 迁移助手 (Typora-to-Obsidian-Migration-Helper)

一个交互式的、基于状态机模式的 Python 脚本,旨在帮助用户安全、高效地将 Typora 笔记库迁移至 Obsidian。它将多个繁琐的手动步骤整合为一个自动化的、可控的流程。本脚本基于历史文章中模块程序组合而成,能够实现…

网站怎么做关键词搜索网站建设 无法打开asp

发送报文处理 增加一个功能码映射关系 //功能码映射关系public readonly Dictionary<string, byte> ReadFuncCodes = new Dictionary<string, byte>();<

二七网站建设网站分析与优化

每当MyBatis设置PreparedStatement的参数或从ResultSet中检索值时&#xff0c;都会使用TypeHandler以适合Java类型的方式来检索值。下表描述了默认的TypeHandlers。 自MyBatis 3.4.5版本起&#xff0c;默认支持JSR-310&#xff08;日期和时间API&#xff09;。 Type HandlerJ…

台州网站建设技术支持网站上的3d产品展示怎么做

目录 一、压力的方向(FORCE) 1、为正的情况 2、为负的情况 二、压强的方向(PRESSURE)

深入解析:【APK安全】敏感数据泄漏风险与防御指南

深入解析:【APK安全】敏感数据泄漏风险与防御指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas"…

大型网站建设与维护过程学做家常菜的网站有哪些

1、过期删除策略 1.1、介绍 Redis 是可以对 key 设置过期时间的&#xff0c;因此需要有相应的机制将已过期的键值对删除&#xff0c;而做这个工作的就是过期键值删除策略。 每当我们对一个 key 设置了过期时间时&#xff0c;Redis 会把该 key 带上过期时间存储到一个过期字典…

网站设置密码最近韩国电影片

标题&#xff1a;递增三元组 给定三个整数数组 A [A1, A2, … AN], B [B1, B2, … BN], C [C1, C2, … CN]&#xff0c; 请你统计有多少个三元组(i, j, k) 满足&#xff1a; 1 < i, j, k < NAi < Bj < Ck 【输入格式】 第一行包含一个整数N。 第二行包含N个整…

详细介绍:开源 java android app 开发(十七)封库--混淆源码

详细介绍:开源 java android app 开发(十七)封库--混淆源码2025-10-01 15:05 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !impor…