yaml
title: Redis-05Redis应用场景
keywords: Redis
cover: [https://z1.ax1x.com/2023/10/01/pPLPggO.png]
banner: type: img bgurl: https://z1.ax1x.com/2023/10/01/pPLPggO.png bannerText: Redis应用场景
categories: Redis
tags: - Redis
toc: false # 无需显示目录
1、缓存
Redis作为key-value形式的内存数据库,最先想到的应用场景就是作为数据缓存。
使用Redis来缓存数据的好处如下:
-
减少对数据库的访问
-
提高系统响应速度
String类型:
-
热点数据缓存
-
对象缓存
-
把相应的热点对象进行缓存到Redis
-
用户对象
-
商品对象
-
订单对象
-
-
-
页面缓存
- 通过在手动渲染得到的html页面缓存到redis,下次访问相同页面时直接从redis中获取进行返回,减少服务端处理的压力
2、数据共享分布式
String类型:
-
分布式Session
- Redis是分布式的独立服务,可以在多个应用之间共享
3、分布式锁
由于Redis单线程的特性,可以避免分布式部署之后的数据污染问题
Redisson:java分布式锁终极解决方案之 redisson_java redisson-CSDN博客
实现一个简易的锁:
java
public String acquireLock(Jedis conn, String lockName) {
return acquireLock(conn, lockName, 10000);
}
/**
* 简易锁
*
* 如果程序在尝试获取锁的时候失败,那么它将不断地进行重试,知道成功地取得锁或者超过给定的时间限制为止
* @param conn
* @param lockName
* @param acquireTimeout
* @return
*/
public String acquireLock(Jedis conn, String lockName, long acquireTimeout) {
String identifier = UUID.randomUUID().toString();
// 持有锁时间
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
// 尝试获得锁
if (conn.setnx("lock:" + lockName, identifier) == 1) {
return identifier;
}
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
return null;
}
/**
* 释放锁操作
*
* @param conn
* @param lockName
* @param identifier
* @return
*/
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
String lockKey = "lock:" + lockName;
while (true) {
// 检查进程是否仍然持有锁
conn.watch(lockKey);
if (identifier.equals(conn.get(lockKey))) {
// 释放锁
Transaction trans = conn.multi();
trans.del(lockKey);
List<Object> results = trans.exec();
if (results == null) {
continue;
}
return true;
}
conn.unwatch();
break;
}
return false;
}
带有超时限制特性的锁:
java
/**
* 带有超时限制特性的锁
*
* @param conn
* @param lockName
* @param acquireTimeout
* @param lockTimeout
* @return
*/
public String acquireLockWithTimeout(
Jedis conn, String lockName, long acquireTimeout, long lockTimeout) {
String identifier = UUID.randomUUID().toString();
String lockKey = "lock:" + lockName;
// 过期时间必须是整数
int lockExpire = (int) (lockTimeout / 1000);
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
// 获取锁并设置过期时间
if (conn.setnx(lockKey, identifier) == 1) {
// 检查过期时间
conn.expire(lockKey, lockExpire);
return identifier;
}
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(1);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
// null indicates that the lock was not acquired
return null;
}
/**
* 释放锁操作
*
* @param conn
* @param lockName
* @param identifier
* @return
*/
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
String lockKey = "lock:" + lockName;
while (true) {
// 检查进程是否仍然持有锁
conn.watch(lockKey);
if (identifier.equals(conn.get(lockKey))) {
// 释放锁
Transaction trans = conn.multi();
trans.del(lockKey);
List<Object> results = trans.exec();
if (results == null) {
continue;
}
return true;
}
conn.unwatch();
break;
}
return false;
}
String 类型setnx方法,只有不存在时才能添加成功,返回true
java
# 加锁操作
public static boolean getLock(String key) {
Long flag = jedis.setnx(key, "1");
if (flag == 1) {
jedis.expire(key, 10);
}
return flag == 1;
}
# 释放
public static void releaseLock(String key) {
jedis.del(key);
}
4、计数器
int类型,incr方法
使用场景:
-
记录各个页面的被访问次数
-
时间序列计数器(time series counter)
时间序列计数器实现如下:
java
// 以秒为单位的计数器精度,分别为1秒、5秒、60秒、300秒、3600秒、18000秒、86400秒。
public static final int[] PRECISION = new int[]{1, 5, 60, 300, 3600, 18000, 86400};
/**
* 更新计数器信息
*
* @param conn
* @param name
* @param count
* @param now
*/
public void updateCounter(Jedis conn, String name, int count, long now) {
Transaction trans = conn.multi();
// 为我们记录的每种精度都创建一个计数器
for (int prec : PRECISION) {
long pnow = (now / prec) * prec;
// 创建负责存储计数信息的散列
String hash = String.valueOf(prec) + ':' + name;
// 将计数器的引用信息添加到有序集合里面,并将其分值设置为0,以便在之后执行清理操作
trans.zadd("known:", 0, hash);
// 对给定名字和精度的计数器进行更新
trans.hincrBy("count:" + hash, String.valueOf(pnow), count);
}
trans.exec();
}
/**
* 获取计数器内容
*
* @param conn
* @param name
* @param precision
* @return
*/
public List<Pair<Integer, Integer>> getCounter(
Jedis conn, String name, int precision) {
// 获取存储计数器数据的键名
String hash = String.valueOf(precision) + ':' + name;
// 从Redis里面取出计数器数据
Map<String, String> data = conn.hgetAll("count:" + hash);
ArrayList<Pair<Integer, Integer>> results =
new ArrayList<Pair<Integer, Integer>>();
// 将计数器数据转换成指定的格式
for (Map.Entry<String, String> entry : data.entrySet()) {
results.add(new Pair<Integer, Integer>(
Integer.parseInt(entry.getKey()),
Integer.parseInt(entry.getValue())));
}
// 对数据进行排序,把旧的数据样本排在前面
Collections.sort(results);
return results;
}
5、限流
int类型,incr方法
Rate Limit实现案例:
- Spring AOP + Redis + Lua 实现自定义限流注解
6、购物车
String或者Hash
7、时间轴(Timeline)
list作为双向链表,不光可以作为队列使用。
如果将它用作栈便可以成为一个公用的时间轴。
当用户发完微博后,都通过lpush将它存放在一个 key 为LATEST_WEIBO的list中,之后便可以通过lrange取出当前最新的微博。
8、消息队列
List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间
- blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
- brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
9、点赞
文章ID:t1001
用户ID:u3001
用 like:t1001 来维护 t1001 这条文章的所有点赞用户
- 点赞了这条文章:sadd like:t1001 u3001
- 取消点赞:srem like:t1001 u3001
- 是否点赞:sismember like:t1001 u3001
- 点赞的所有用户:smembers like:t1001
- 点赞数:scard like:t1001
10、排行榜
ZSET:有序set
-
zrevrangebyscore:获得以分数倒序排列的序列
-
zrank:获取成员在该排行榜的位置
id 为6001 的新闻点击数加1:
javascript
zincrby hotNews:20190926 1 n6001
获取今天点击最多的15条:
javascript
zrevrange hotNews:20190926 0 15 withscores
11、共同好友
通过Set的交集、并集、差集操作来实现查找两个人共同的好友
12、秒杀
流程如下:
- 提前预热数据,放入Redis
- 商品列表放入Redis List
- 商品的详情数据 Redis hash保存,设置过期时间
- 商品的库存数据Redis sorted set保存
- 用户的地址信息Redis set保存
- 订单产生扣库存通过Redis制造分布式锁,库存同步扣除
- 订单产生后发货的数据,产生Redis list,通过消息队列处理
- 秒杀结束后,再把Redis数据和数据库进行同步
实现Demo: