Redis常见命令
Redis启动
redis-cli -h 192.168.105.130 -p 6379
输入密码镜像验证
AUTH 123321
检验是否连接成功
ping //返回PONG
Redis数据结构
Redis是一个key-value的数据库
基本类型:
- String
- Hash
- List
- Set
- SortedSet
特殊类型
- GEO
- BitMap
- Hyperlog
Redis通用命令
- KEYS:查看符合模板的所有key 不建议在生产环境设备上使用
KEYS *
- DEL:删除一个指定的key
del name
- EXISTS:判断key是否存在
EXISTS age
- EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
EXPIRE age 20
- TTL:查看一个KEY的剩余有效期
TTL age
String类型
String类型可以根据字符串的格式不同分为三类:
- string:普通字符串
- int:整型,可以自增、自减操作
- float:浮点型,可以自增自减操作
String的常见命令有:
- SET:添加或者修改已经存在的一个String类型的键值对
- GET:根据key获取String类型的value
- MSET:批量添加多个String类型的键值对
- MGET:根据多个key获取多个String类型的value
- INCR:让一个整型的key自增1
INCR age
- INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
INCRBY age 2
- INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
- SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
- SETEX:添加一个String类型的键值对,并且指定有效期
key结构
Redis的key允许有多个单词形成层级结构,多个单词之间用’:'隔开
set xyz:user:1 '{"id":1, "name":"Jack", "age": 21}'
set xyz:user:2 '{"id":2, "name":"Rose", "age": 18}'
set xyz:product:1 '{"id":1, "name":"小米11", "price": 4999}'
set xyz:product:2 '{"id":2, "name":"荣耀6", "price": 2999}'
Hash类型
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
- HVALS:获取一个hash类型的key中的所有的value
- HINCRBY:让一个hash类型key的字段值自增并指定步长
- HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
HSET xyz:user:3 name Lucy
(integer) 1
192.168.105.130:6379> HSET xyz:user:3 age 21
(integer) 1
192.168.105.130:6379> HSET xyz:user:3 age 19
(integer) 0
192.168.105.130:6379> HGET xyz:user:3 name
"Lucy"
192.168.105.130:6379> HGET xyz:user:3 age
"19"
192.168.105.130:6379> HMSET xyz:user:4 name LiLei age 20 sex male
OK
192.168.105.130:6379> HMGET xyz:user:4 name age sex
1) "LiLei"
2) "20"
3) "male"
192.168.105.130:6379> HGETALL xyz:user:4
1) "name"
2) "LiLei"
3) "age"
4) "20"
5) "sex"
6) "male"
192.168.105.130:6379> HKEYS xyz:user:4
1) "name"
2) "age"
3) "sex"
192.168.105.130:6379> HVALS xyz:user:4
1) "LiLei"
2) "20"
3) "male"
192.168.105.130:6379> HINCRBY xyz:user:4 age 2
(integer) 22
192.168.105.130:6379> HINCRBY xyz:user:4 age -2
(integer) 20
192.168.105.130:6379> HSETNX xyz:user:4 sex female
(integer) 0
192.168.105.130:6379> HSETNX xyz:user:3 sex female
(integer) 1
192.168.105.130:6379> HGETALL xyz:user:3
1) "name"
2) "Lucy"
3) "age"
4) "19"
5) "sex"
6) "female"
List类型
192.168.105.130:6379> LPUSH users 1 2 3
(integer) 3
192.168.105.130:6379> RPUSH 4 5 6
(integer) 2
192.168.105.130:6379> RPUSH users 4 5 6
(integer) 6
192.168.105.130:6379> RPOP users 1
1) "6"
192.168.105.130:6379> LRANGE users 1 2
1) "2"
2) "1"
192.168.105.130:6379> RPOP users 1
1) "5"
192.168.105.130:6379> BLPOP user2 100
1) "user2"
2) "jack"
Set类型
特征:
- 无序
- 元素不可重复
- 查找快
- 支持交集、并集、差集等功能
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的并集
案例:
- 张三的好友有:李四、王五、赵六
- 李四的好友有:王五、麻子、二狗
- 计算张三的好友有几人
- 计算张三和李四有哪些共同好友
- 查询哪些人是张三的好友却不是李四的好友
- 查询张三和李四的好友总共有哪些人
- 判断李四是否是张三的好友
- 判断张三是否是李四的好友
- 将李四从张三的好友列表中移除
192.168.105.130:6379> SADD zs lisi wangwu zhaoliu
(integer) 3
192.168.105.130:6379> SADD ls wangwu mazi ergou
(integer) 3
192.168.105.130:6379> SCARD zs
(integer) 3
192.168.105.130:6379> SINTER zs ls
1) "wangwu"
192.168.105.130:6379> SDIFF zs ls
1) "zhaoliu"
2) "lisi"
192.168.105.130:6379> SUNION zs ls
1) "wangwu"
2) "lisi"
3) "zhaoliu"
4) "mazi"
5) "ergou"
192.168.105.130:6379> SISMEMBER zs lisi
(integer) 1
192.168.105.130:6379> SISMEMBER ls zhangsan
(integer) 0
192.168.105.130:6379> SREM zs lisi
(integer) 1
SortedSet类型
SortedSet是一个可排序的set集合,SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。
特性:
- 可排序
- 元素不重复
- 查询速度快
常见命令:
- 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:求差集、交集、并集
所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可
案例
将班级的下列学生得分存入Redis的SortedSet中:
Jack 85, Lucy 89, Rose 82, Tom 95, Jerry 78, Amy 92, Miles 76
并实现下列功能:
- 删除Tom同学
- 获取Amy同学的分数
- 获取Rose同学的排名
- 查询80分以下有几个学生
- 给Amy同学加2分
- 查出成绩前3名的同学
- 查出成绩80分以下的所有同学
192.168.105.130:6379> ZADD stus 85 Jack 89 Lucy 82 Rose 95 Tom 78 Jerry 92 Amy 76 Miles
(integer) 7
192.168.105.130:6379> ZREM stus Tom
(integer) 1
192.168.105.130:6379> ZSCORE stus Amy
"92"
192.168.105.130:6379> ZRANK stus Rose
(integer) 2
192.168.105.130:6379> ZREVRANK stus Rose
(integer) 3
192.168.105.130:6379> ZCOUNT stus 0 80
(integer) 2
192.168.105.130:6379> ZINCRBY stus 2 Amy
"94"
192.168.105.130:6379> ZRANGE stus 0 2
1) "Miles"
2) "Jerry"
3) "Rose"
192.168.105.130:6379> ZREVRANGE stus 0 2
1) "Amy"
2) "Lucy"
3) "Jack"
192.168.105.130:6379> ZRANGEBYSCORE stus 0 80
1) "Miles"
2) "Jerry"
Redis客户端
Jedis
引入依赖
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
JedisTest.java
package com.xyz;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import java.util.Map;
public class JedisTest {
private Jedis jedis;
@BeforeEach
void setUp() {
//1.建立连接
jedis = new Jedis("192.168.105.130", 6379);
//2.设置密码
jedis.auth("123321");
//3.选择库
jedis.select(0);
}
@Test
void testString() {
//存入数据
String result = jedis.set("name", "虎哥");
System.out.println("result = "+result);
//获取数据
String name = jedis.get("name");
System.out.println("name = "+name);
}
@Test
void testHash() {
//插入hash数据
jedis.hset("user:1","name","Jack");
jedis.hset("user:1","age","21");
//获取
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
@AfterEach
void tearDown() {
if (jedis!=null){
jedis.close();
}
}
}
线程池
com.xyz.jedis.utils.JedisConnectionFactory
package com.xyz.jedis.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisConnectionFactory {
private static final JedisPool jedisPool;
static {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMaxWaitMillis(1000);
//创建连接池对象
jedisPool=new JedisPool(poolConfig,"192.168.105.130",6379,1000,"123321");
}
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
SpringDataRedis
SpringDataRedis的使用步骤:
-
引入spring-boot-starter-data-redis依赖
<!--Redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.7.6</version> </dependency> <!--连接池依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--Jackson依赖--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency>
-
在application.yml配置Redis信息
spring: redis: host: 192.168.105.130 port: 6379 password: 123321 lettuce: pool: max-active: 8 #最大连接 max-idle: 8 #最大空闲连接 min-idle: 0 #最小空闲连接 max-wait: 100 #连接等待时间(ms)
-
注入RedisTemplate
RedisTemplate的两种序列化实践方案:
方案一:
- 自定义RedisTemplate
- 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
方案二:
- 使用StringRedisTemplate
- 写入Redis时,手动把对象序列化为JSON
- 读取Redis时,手动把读取到的JSON反序列化为对象
RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//创建Template
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置反序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//key和 hashKey采用String序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
//value和hashValue采用JSON序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
RedisDemoApplicationTests.java
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Test
void testString() {
//写入一条String数据源
redisTemplate.opsForValue().set("name","笨蛋");
//获取String数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = "+name);
}
@Test
void testSaveUser() {
//写入数据
redisTemplate.opsForValue().set("user:100",new User("胖虎",21));
//获取数据
User user = (User) redisTemplate.opsForValue().get("user:100");
System.out.println("user = " + user);
}
}
RedisStringTests.java
@SpringBootTest
class RedisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void testString() {
//写入一条String数据源
stringRedisTemplate.opsForValue().set("name","张三");
//获取String数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = "+name);
}
private static final ObjectMapper mapper=new ObjectMapper();
@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("李四", 23);
// 手动序列化
String json = mapper.writeValueAsString(user);
//写入数据
stringRedisTemplate.opsForValue().set("user:200",json);
// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
//手动反序列化
User user1 = mapper.readValue(jsonUser, User.class);
System.out.println("user1 = " + user1);
}
@Test
void testHash() {
stringRedisTemplate.opsForHash().put("user:400","name","王五");
stringRedisTemplate.opsForHash().put("user:400","age","20");
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:400");
System.out.println("entries = " + entries);
}
}
黑马点评项目
短信登录
发送验证码
UserService.sendCode()
校验手机号 -> RegexUtils.isPhoneInvalid()
生成验证码 -> RandomUtil.randomNumbers()
保存Session
发送验证码 -> 使用 log.debug() 模拟发送验证码
log 的 debug()、 error()、 info()方法的区别
- Debug
这个级别的信息,可以随意的使用,任何觉得有利于在调试时更详细的了解系统运行状态,比如变量的值等等,都输出来看看也无妨。
当然,在每一个 Debug 调用之前,一定要加上 If 判断。
- Info
这个应该用来反馈系统的当前状态给最终用户的,所以,在这里输出的信息,应该对最终用户具有实际意义,也就是最终用户要能够看得明白是什么意思才行。
- Error
所谓错误,就是说可以进行一些修复性的工作,但无法确定系统会正常的工作下去,系统在以后的某个阶段,很可能会因为当前的这个问题,导致一个无法修复的错误(例如宕机),但也可能一直工作到停止也不出现严重问题。
实现验证码登录
UserService.login()
-
校验手机号
-
校验验证码(验证码放常量池 -> utils.SystemConstants)
- 一致
- 不一致
-
根据手机号查询用户
- 不存在,则创建用户
- 存在,则保存用户信息到Session
持久层接口 | MyBatis-Plus (baomidou.com)
登录验证功能
- 编写登录拦截器 LoginInterceptor
- 获取Session
- 获取Session中的用户
- 判断用户是否存在,不存在则拦截,返回401状态码
- 将用户信息保存在ThreadLocal
- 放行
- 配置添加拦截器
- 完成 /user/me 接口
用户信息脱敏
import cn.hutool.core.bean.BeanUtil;
session.setAttribute("user",BeanUtil.copyProperties(user, UserDTO.class));
基于Redis的短信登录
验证码相关
将原先保存在session中的验证码,改为存入redis中,设置过期时间2min
定义Redis相关常量RedisConstants
登录相关
-
从redis中获取验证码,并校验
-
随机生成token作为登录令牌(UUID)
-
将User对象转为Hash存储
BeanUtil.beanToMap()
-
将用户信息存储至redis,设置有效期30min
拦截器相关
- 获取请求头中的token
- 基于token获取redis中的用户
- 刷新token有效期
登录拦截器的优化
新增token刷新拦截器
商户查询缓存
缓存
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
缓存的作用
- 降低后端负载
- 提高读写效率,降低响应时间
缓存的成本
- 数据一致性成本
- 代码维护成本
- 运维成本
添加商户缓存
shopService.queryShopById()
- 从redis查询商户缓存
- 判断是否存在
- 存在则直接返回
JSONUtil.toBean()
- 不存在则根据id查询数据库
getById()
- 数据库不存在,返回错误信息
- 存在则写入redis
缓存更新策略
缓存更新策略的最佳实践方案
- 低一致性需求:使用Redis自带的内存淘汰机制
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
- 写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
- 读操作:
实现商铺缓存和数据库双写一致问题
- 设置商铺缓存时间 30min
- 修改更新商铺方法的业务逻辑
- 判断id是否为空
- 更新数据库
- 删除缓存
使用postman测试 注意事项
在Header中需要添加authorization,否则会被拦截器拦截,无法进行测试
在Boody中选择 raw 和 JSON ,并将更新数据填入
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案:
- 缓存空对象
- 优点:实现简单,维护方便
- 缺点:
- 额外的内存消耗
- 可能造成短期的不一致
- 布隆过滤
- 优点:内存占用较少,没有多余的key
- 缺点
- 实现复杂
- 存在误判可能
拓展:
- 修改代码,查询数据库中商铺信息不存在时,在redis中存入2min的空值数据
- 增加代码,判断redis命中是否为空值
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决办法:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案:
- 互斥锁
- 逻辑过期
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗 保证一致性 实现简单 | 线程需要等待,性能受影响 可能有死锁的风险 |
逻辑过期 | 线程无需等待,性能较好 | 不保证一致性 有额外的内存消耗 实现复杂 |
实现缓存重建
互斥锁
- 定义
tryLock()
获取锁方法,使用setIfAbsent()
,最后使用BooleanUtil.isTrue()
拆箱 - 获取互斥锁
- 判断是否获取成功
- 失败,则休眠后重试
- 成功则根据id查询数据库
- 不存在,返回错误
- 存在,写入数据
- 释放锁
逻辑过期
- 新建类
RedisData
设置逻辑过期时间 - 编写方法
saveShop2Redis
- 查询店铺数据
- 封装逻辑过期时间
- 写入Redis
- 编写单元测试
testSaveShop()
- 编写方法
queryWithLogicalExpire()
- redis查询商铺信息
- 未命中则返回null
- 命中则需要把json反序列化
- 判断信息是否过期
- 未过期则直接返回
- 过期则需要缓存重建
- 获取互斥锁
- 判断是否获取锁成功
- 成功,则开启独立线程,实现缓存重建
- 返回过期的商铺信息
优惠券秒杀
全局唯一ID
特性:
- 唯一性
- 高可用
- 高性能
- 递增性
- 安全性
ID的组成部分:符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
编写 RedisIdWorker
的 nextId()
方法
- 生成时间戳
- 生成序列号
- 获取当前日期,精确到天
- 自增长
- 拼接并返回
全局唯一ID生成策略:
- UUID
- Redis自增
- snowflake算法
- 数据库自增
Redis自增ID策略:
- 每天一个key,方便统计订单量
- ID构造是 时间戳 + 计数器
实现秒杀下单
- 查询优惠券
- 判断秒杀是否开始
- 判断秒杀是否结束
- 判断库存是否充足
- 扣除库存
- 创建订单
- 订单id
- 用户id
- 代金券id
- 涉及两张表,添加事务
超卖问题
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
- 悲观锁
- 乐观锁
- 版本号法
- CAS法
- 悲观锁:添加同步锁,让线程串行执行
- 优点:简单粗暴
- 缺点:性能一般
- 乐观锁:不加锁,在更新时判断是否有其它线程在修改
- 优点:性能好
- 缺点:存在成功率低的问题
一人一单
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
- 查询订单
- 判断是否存在
TODO spring事务失效问题
分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。
特点:
- 多进程可见
- 互斥
- 高可用
- 高性能
- 安全性
- 实现分布式锁
- 改进Redis分布式锁误删问题
总结
- 不可重入Redis分布式锁:
- 原理:利用setnx的互斥性;利用ex(expire)避免死锁;释放锁时判断线程标识
- 缺陷:不可重入、无法重试、锁超时失效
- 可重入的Redis分布式锁:
- 原理:利用hash接口,记录线程标识和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
- 缺陷:redis宕机硬气锁失效问题
- Redisson的multiLock:
- 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获得锁成功
- 缺陷:运维成本高、实现复杂
消息队列
认识消息队列
消息队列由三个角色组成:
- 生产者:发送信息到消息队列
- 消息队列:存储和管理消息,也被称为消息代理
- 消费者:从消息队列中获取并处理消息
使用队列的好处在于解耦
基于List实现消息队列
优点:
- 利用Redis存储,不受限于JVM内存上限
- 基于Redis的持久化机制,保障了数据的安全
- 可以满足消息的有序性
缺点:
- 无法避免数据丢失
- 只支持单消费者
基于PubSub的消息队列
优点:
- 采用发布订阅模型,支持多生产、多消费
缺点:
- 不支持数据持久化
- 无法避免数据丢失
- 消息堆积有上限,超出时数据丢失
基于Stream的消息队列
优点:
- 消息可回溯
- 一个消息可以被多个消费者读取
- 可以阻塞读取
缺点:
- 有消息漏读风险
解决重复读取报错问题
XGROUP CREATE stream.order g1 0 MKSTREAM
达人探店
查看探店笔记
实现查看发布探店笔记的接口
点赞功能
编写 likeBlog()
方法
- 获取登录用户
- 判断当前用户是否已经点赞
- 未点赞,则可以点赞
- 数据库点赞数+1
- 保存用户到Redis的set集合中
- 如果已点赞,则取消点赞
- 数据库点赞数-1
- 把用户从Redis的set中移除
- 未点赞,则可以点赞
点赞排行榜
查询点赞用户列表
- 查询top5的点赞用户
zrange key 0 4
- 解析出其中的用户id
- 根据用户id查询用户
- 返回
好友关注
关注和取关
@PutMapping("/{id}/{isFollow}")
public Result follow()
@GetMapping("/{id}/{isFollow}")
public Result follow()
follow()
- 获取登录用户
- 判断是关注还是取关状态
- 关注,则新增数据
- 取关,则删除表中数据
isFollow()
- 获取登录用户
- 查询是否关注
- 判断并返回结果
共同关注
将关注对象在放入数据库的同时,放入redis。使用数据结构 set
中的 SINTER
获取交集,即共同关注
@PathVariable
和@RequestParam
都是Spring MVC中用于将请求中的参数映射到控制器方法参数的注解,但它们有以下主要区别:
-
参数来源:
@PathVariable
:用于从URL路径中提取参数。它通常用于RESTful API设计中,可以将URL的一部分作为参数传递给控制器方法。@RequestParam
:用于从请求的查询字符串(即URL后面的?
后面的部分)中提取参数。
-
使用场景:
@PathVariable
:当你的URL设计需要包含动态部分时使用,例如:/users/{id}
,这里的{id}
就是一个路径变量。@RequestParam
:当你需要从查询字符串中获取参数时使用,例如:/users?name=John
,这里的name
就是一个请求参数。
-
默认值:
@PathVariable
:没有默认值的概念,因为它们是从URL路径中提取的,必须在请求时提供。@RequestParam
:可以设置默认值,如果请求中没有提供该参数,则使用默认值。
-
类型:
@PathVariable
:可以是任何类型,Spring会尝试将路径参数转换为方法参数的类型。@RequestParam
:同样可以是任何类型,但需要确保请求中的参数可以转换为该类型。
-
方法签名:
@PathVariable
:通常与value
属性一起使用,指定路径变量的名称,例如:@PathVariable("id") Long id
。@RequestParam
:可以与value
、name
、required
、defaultValue
等属性一起使用,例如:@RequestParam(value = "current", defaultValue = "1") Integer current
。
-
数据绑定:
@PathVariable
:Spring MVC会尝试将路径参数绑定到方法参数上。@RequestParam
:Spring MVC会将请求参数绑定到方法参数上,如果参数不存在,且设置了默认值,则使用默认值。
示例
-
使用
@PathVariable
:@GetMapping("/users/{id}") public User getUserById(@PathVariable("id") Long id) { // ... }
-
使用
@RequestParam
:@GetMapping("/users") public List<User> getUsersByPage(@RequestParam(value = "page", defaultValue = "1") Integer page) { // ... }
在实际开发中,根据API的设计和需求选择合适的注解来处理请求参数。
关注推送
将作者的笔记推送到各个关注者的redis邮箱中,同时实现Feed的滚动分页
Feed流产品有两种常见模式:
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 优点:信息全面,不会有缺失。并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
Timeline模式的实现方案有三种:
- 拉模式
- 推模式
- 推拉结合
用户签到
BitMap的操作命令有:
- SETBIT:向指定位置(offset)存入一个0或1
- GETBIT :获取指定位置(offset)的bit值
- BITCOUNT :统计BitMap中值为1的bit位的数量
- BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
- BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回
- BITOP :将多个BitMap的结果做位运算(与 、或、异或)
- BITPOS :查找bit数组中指定范围内第一个0或1出现的位置
UV统计
@Test
void testHyperLog(){
String[] values = new String[1000];
int j=0;
for (int i = 0; i < 1000000; i++) {
j=i%1000;
values[j]="user_"+i;
if (j==999){
stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
}
}
Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");
System.out.println("count = " + count);
}