目录
一、Redis的应用场景
1、限流
2、分布式锁
3、点赞
4、消息队列
二、Redis类型的命令及用法
1、String类型
2、Hash类型
3、List类型
4、Set类型
5、Zset类型
6、Redis工具类
Redis使用缓存的目的就是提升读写性能
实际业务场景下,我们就可以把 Mysql 中的热点数据缓 存到 Redis 中,提升读取性能,同时也减轻了 Mysql 的读取压力
一、Redis的应用场景
Redis除了做缓存以外,还可以用
- 限流
- 分布式锁
- 点赞/排行榜
- 消息队列
还有其他场景:计数器、互关好友、购物车和商品标签等等
1、限流
利用Redis的过期键和计数器功能,实现API的限流功能,防止服务被滥用
方法一:记录IP在某个时间段访问某接口的次数
使用IP作为Key和其他信息作为Key,访问次数作为Value,访问一次Incr增加一次,超过规定次数则返回false
但问题是:限流时间段是固定的
比如:某接口在1分钟内请求次数不超过1000次
就是00:59分,用户已经访问了999次,1:00key值过期,1:01又访问了999次
看起来好像是没问题,但是00:59—1:01的仅2s时间段内,接口被访问了1000+999次,明显错误
方法二:滑动窗口
为了避免方法一种由于key过期导致短期内访问量增大的情况,将时间改成动态的
在每次接口访问时,记录当前访问的时间点,并计算前1min内用户访问该接口的总次数,如果总次数大于1000次,则不允许用户访问该接口
以上两种利用redis实现限流的方式基本能满足我们大部分的业务需要,对于部分要求限流粒度更高更准的业务,可以引入Sentinel来满足业务需要
2、分布式锁
为什么使用分布式锁?
在单机部署的时候,可以使用 Java 中提供的 JUC 锁机制避免多线程同时操作一个共享变量产生的安全问题,通过锁(synchronzied
或 lock
)来锁住自己的线程资源,从而防止缓存击穿
Redis的缓存问题:缓存穿透、缓存击穿和缓存雪崩-CSDN博客
这是一种本地加锁
的方式,在分布式
情况下会带来数据不一致的问题
比如:服务 A 获取数据后,更新缓存 key =100,服务 B 不受服务 A 的锁限制,并发去更新缓存 key = 99,最后的结果可能是 99 或 100,但这是一种未知的状态,与期望结果不一致
①基础版
Redis的SET
命令有一个NX
参数,可以实现「key不存在才插入」,因此可以用它来实现分布式锁:
SetNX(key,value)
redisTemplate.opsForValue().setIfAbsent(“k”, “v”)
也就是
SET lockKey requestId NX PX expireTime
-
lockKey
表示锁的资源, -
requestId 全局唯一的业务id,避免存在加锁和释放锁乱掉的情况
-
NX
:表示只有lockKey
不存在的时候才能SET
成功,从而保证只有一个客户端可以获得锁。 -
PX expireTime
设置锁的超时时间,单位是毫秒;也可以使用EX seconds
以秒为单位设置超时时间
伪代码:
try {
if (jedis.set(lockKey, requestId, "NX", "PX", expireTime))) {
//业务处理
return true;
}
} finally{
//判断是不是当前线程加的锁,是才释放
if(requestd.equals.(jedis.get(lockKey))){
//释放锁
unlock(lockKey,requestId);
}
}
return false;
②Redisson实现
Redisson 是 Redis 的 Java 客户端之一,支持原子性加/解锁、锁重试、可重入锁、RedLock 等功能
// 获取分布式锁
RLock lock = redissonClient.getLock("myLock");
try {
// 尝试加锁,最多等待 10 秒,加锁后的锁有效期为 30 秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
// 成功获取锁,执行业务逻辑
System.out.println("获取锁成功,执行业务逻辑...");
} else {
// 获取锁失败,可能是超时等待或者其他原因
System.out.println("获取锁失败...");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
// 关闭 Redisson 客户端
redissonClient.shutdown();
}
Redisson存储分布式锁是通过Hash结构进行存储的,内置的键值对是< 线程标识,重入次数>,其中重入次数便可用于实现可重入机制
3、点赞
点赞就需要用到Redis的Set类型,Set类型是Redis中的一个无序集合,它可以存储一组字符串元素,并且每个元素都是唯一的
【点赞】SADD key member:向集合中添加一个或多个元素
redisTemplate.opsForSet().add(key, values)
【取消点赞】SREM key:删除元素
redisTemplate.opsForSet().remove(key,value)
【点赞的所有用户】SMEMBERS key:返回集合中的所有元素
redisTemplate.opsForSet().members(key)
【是否点赞】SISMEMBER key member:判断元素是否在集合中
redisTemplate.opsForSet().isMember(key, value)
【点赞数】scard(key):元素长度
redisTemplate.opsForSet().size(key)
场景:对活动的相册图片进行点赞且对点赞数量进行计数的功能
public void like(Long activityId) {
LzmUserInfo userInfo = UserContextHolder.getLzmUserInfo();
String key = ACTIVITY.ACTIVITY_LIKE_KEY + activityId;
Boolean member = stringRedisTemplate.opsForSet().isMember(key, userInfo.getUserId().toString());
if (BooleanUtil.isFalse(member)) {
//未点赞,点赞数+1
this.update(Wrappers.<ActivityManageEntity>lambdaUpdate()
.setSql("like_num = like_num + 1")
.eq(ActivityManageEntity::getActivityId, activityId)
.eq(ActivityManageEntity::getIsDelete, IsDeleteEnum.NORMAL.getCode()));
redisTemplate.opsForSet().add(key, userInfo.getUserId().toString());
} else {
//取消点赞,点赞数-1
this.update(Wrappers.<ActivityManageEntity>lambdaUpdate()
.setSql("like_num = like_num - 1")
.gt(ActivityManageEntity::getLikeNum, 0)
.eq(ActivityManageEntity::getActivityId, activityId)
.eq(ActivityManageEntity::getIsDelete, IsDeleteEnum.NORMAL.getCode()));
redisTemplate.opsForSet().remove(key, userInfo.getUserId().toString());
}
}
4、消息队列
Redis可以实现简单的队列。在生产者端,使用LPUSH加入到某个列表中;在消费端,不断的使用RPOP指令取出这些数据,或者使用阻塞的BRPOP指令获取数据,用于处理异步任务,例如邮件发送、后台任务处理,小规模的抢购需求等
场景:邮件发送
生产者发布消息
LPUSH queue msg1
消费者 拉取消息
RPOP queue
二、Redis类型的命令及用法
1、String类型
操作 | 命令 | 用法 |
---|---|---|
设置 | set(“k”,“v”) | template.opsForValue().set(“k”,“v”) |
获取 | get(“k”) | template.opsForValue().get(“k”) |
增1 | incr(“k”) | template.boundValueOps(“k”).increment(1) |
减1 | decr(“k”) | template.boundValueOps(“k”).increment(-1) |
设置时效 | setex(“k”,seconds,“v”) | template.opsForValue().set(“k”,“v”,20, TimeUnit.SECONDS) |
key不存在就设置 | setnx(“k”,“v”) | template.opsForValue().setIfAbsent(“k”, “v”) |
获取key过期时间 | ttl(“k”) | template.getExpire(“k”) |
删除 | del(“k”) | template.delete(“k”) |
2、Hash类型
操作 | 命令 | 用法 |
---|---|---|
设置 | hset(“k1”,“k2”,“k3”) | template.opsForHash().put(“k1”,“k2”,“k3”) |
获取 | hget(“k1”,“k2”) | template.opsForHash().get(“k1”,“k2”) /template.opsForHash().values(“k1”) |
删除 | hdel(“k1”,“k2”) | template.opsForHash().delete(“k1”,“k2”) |
是否存在 | hexists(“k1”,“k2”) | template.opsForHash().hasKey(“k1”,“k2”) |
3、List类型
操作 | 命令 | 用法 |
---|---|---|
从右侧添加 | rpush(“k”,“v”) | template.opsForList().rightPush(“k”,“v”) |
从右侧移除 | rpop(“list”) | template.opsForList().rightPop(“k”) |
长度 | llen(“k”) | template.opsForList().size(“k”) |
获取指定范围的元素 | lrange(“list”,0,-1) -1指全部 | template.opsForList().range(“list”, 0, -1) |
4、Set类型
操作 | 命令 | 用法 |
---|---|---|
添加 | sadd(“k”,“v”) | template.opsForSet().add(“k”,“v”) |
值移除 | srem(“k”,“v”) | template.opsForSet().remove(“k”,“v”) |
直接移除 | spop(“k”) | template.opsForSet().pop(“k”) |
获取所有 | smembers("k") | template.opsForSet().members("k") |
是否存在 | sismember("k") | emplate.opsForSet().isMember("k","v") |
长度 | scard(“k”) | template.opsForSet().size(“k”) |
交集 | sinter(“k1”,“k2” ) | template.opsForSet().intersect(“k1”, “k2”) |
并集 | sunion(“k1”,“k2” ) | template.opsForSet().union(“k1”, “k2”) |
差集 | sdiff(“k1”,“k2” ) | template.opsForSet().difference(“k1”, “k2”) |
5、Zset类型
操作 | 命令 | 用法 |
---|---|---|
增加 | zadd(“k”,1,“a”) | template.opsForZSet().add(“k”,“1”,a) |
排名结果 | zrevrange(“k”, 0, -1) | template.opsForZSet().reverseRange(“k”, 0, -1) |
排名分数 | zrevrangeByScoreWithScores(“k”, 1, 10); | template.opsForZSet().reverseRangeByScore(“k”, 1, 100) |
修改分数 | zincrby(“k”,20,“a”) | template.opsForZSet().incrementScore(“k”,“20”,a) |
数量 | zcard(“k”) | template.opsForZSet().zCard(“k”) |
获取排名 | zrank(“k”,“a”) | template.opsForZSet().rank(“k”,“a”) |
6、Redis工具类
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 添加数据
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 如果key不存在,则设置key的值为value并返回true,否则返回false
*/
public boolean setNx(String key, String value) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value));
}
/**
* 如果key不存在,则设置key的值为value以及过期时间,并返回true,否则返回false,
*/
public boolean setNx(String key, String value, long timeout, TimeUnit unit) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit));
}
/**
* 根据key获取数据
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 根据key删除数据
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 向集合中添加一个或多个元素
*/
public void add(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
/**
* 将 value 插入到 key 对应的列表的头部
*/
public void leftPush(String key, Object value) {//。
redisTemplate.opsForList().leftPush(key, value);
}
/**
* 将 value 插入到 key 对应的列表的尾部
*/
public void rightPush(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
}
/**
* 从 key 对应的列表的头部删除并返回一个元素
*/
public Object leftPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
/**
* 从 key 对应的列表的尾部删除并返回一个元素
*/
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
/**
* 返回列表中指定范围内的元素
*/
public List<Object> range(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 返回集合中的所有元素
*/
public Set<Object> members(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 判断元素是否在集合中
*/
public Boolean isMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 向有序集合中添加一个元素并指定分数
*/
public void add(String key, double score, Object value) {
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 获取指定score范围内元素
*/
public Set<ZSetOperations.TypedTuple<Object>> rangeWithScores(String key, long start, long end) {
return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
}
}