redis可以做什么
- 1、汇总数量
- 2、缓存热点数据,减轻服务器压力
- 3、做排行榜
- 4、统计用户一年内登录天数
- 5、计数器
- 6、记录数据间的关系
- 、、、
redis应用
分布式锁
- 分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占
时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用
完了,再调用 del 指令释放茅坑。
- 但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样
就会陷入死锁,锁永远得不到释放。
于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也
可以保证 5 秒之后锁会自动释放。
- Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至
于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,
但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻
辑执行完之间拿到了锁。
为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数
据出现的小波错乱可能需要人工介入解决。
- 有个解决超时问题的方案,就是给锁设置一个唯一值可以是线程id,在删除锁时先校验线程id是否一致。但这又有个问题,就是匹配线程id和删除key不是原子操作(可能匹配的过程中,key过期然后别的线程加上了锁,然后匹配完成把这个锁删除了),这就要依靠lua脚本来处理了。
队列延时
- redis可以利用list的lpush\rpush和rpop\lpop来实现简单的消息队列。但这个队列在list数据为空时客户端会一直轮询做无用的pop浪费资源,可以通过客户端睡眠1s来缓解这个无用pop对资源的浪费
- 睡眠是一种避免资源浪费的方式,但这会导致消息延迟1s,多个消费者的化延迟低于1s(因为sleep是分散开的),这时可以用redis的阻塞读blpop\brpop来替换lpop\rpop,阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消
息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题
- 阻塞读是很好的解决了延时和资源浪费问题,但如果阻塞时间太长就会被redis服务端认为这是空闲连接而自动断开,这时 blpop/brpop会抛出异常,要注意对异常的捕捉以及相应的处理
延时队列
- 在加分布式锁或者其他操作出错时,一般有抛异常报错的、睡眠一段时间后重试、将操作加入延时队列三种方式
- 现在主要说明下redis怎么做这个延时队列,可以用有序集合Zset来实现延迟队列,当前时间加上要延迟的时间算出的值作为Zset的score,然后另起一个程序轮询获取这个Zset的score的值小于当前时间的数据进行处理,这样就达到了数据出错后延迟处理的效果。
延迟队列代码示例
public class RedisDelayingQueue<T> {
static class TaskItem<T> {
public String id;
public T msg;
}
private Type TaskType = new TypeReference<TaskItem<T>>() { }.getType();
private Jedis jedis;
private String queueKey;
public RedisDelayingQueue(Jedis jedis, String queueKey) {
this.jedis = jedis;
this.queueKey = queueKey;
}
public void delay(T msg) {
TaskItem task = new TaskItem();
task.id = UUID.randomUUID().toString();
task.msg = msg;
String s = JSON.toJSONString(task);
jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s);
}
public void loop() {
while (!Thread.interrupted()) {
Set values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
if (values.isEmpty()) {
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
break;
}
continue;
}
String s = values.iterator().next();
if (jedis.zrem(queueKey, s) > 0) {
TaskItem task = JSON.parseObject(s, TaskType);
this.handleMsg(task.msg);
}
}
}
public void handleMsg(T msg) {
System.out.println(msg);
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
RedisDelayingQueue queue = new RedisDelayingQueue<>(jedis, "q-demo");
Thread producer = new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
queue.delay("codehole" + i);
}
}
};
Thread consumer = new Thread() {
public void run() {
queue.loop();
}
};
producer.start();
consumer.start();
try {
producer.join();
Thread.sleep(6000);
consumer.interrupt();
consumer.join();
}
catch (InterruptedException e) {
}
}
}
位图
- 在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,
签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365
个,当用户上亿的时候,需要的存储空间是惊人的。
- 为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,
365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大
节约了存储空间。
- 位图指令有setbit/getbit;统计bitcount;对位进行批量操作bitfield
HyperLogLog
- HyperLogLog是基数统计的一种算法,旨在以极少的代价对庞大的数据量进行去重统计
- 对于每天用户访问量统计这样的业务来说,可以用独立的集合set通过sadd将用户id填充进去就行,通过scard就可以统计当天的用户访问量。但如果访问量非常大,有几百上千万的话,用set来进行统计就非常浪费空间。
- HyperLogLog就可以满足以小量空间获取统计数据,但这个统计数据并不是绝对精确的。(redis中的指令为pfadd和pfcount)
- HyperLogLog的原理简单来说就是通过结果推算达到这个结果的次数,例如抛硬币0正面1反面,连抛四次要达到0001这个结果要抛16次的样子;HyperLogLog大致来一个请求就是在一定范围内取随机数,然后根据这个随机数计算出大致的统计数量。
布隆过滤器
- 为了避免反复给客户推送一样的数据,一般要做数据的去重,如果是在数据库层面用exist来进行去重的话,如果频繁的话那对数据库是有比较大的压力的。
- 为了缓解数据库压力又可以想到用缓存,但用缓存就要把历史数据都缓存起来,在时间的推移下这个缓存的数据就会非常恐怖。
- 布隆过滤器就能很好的解决上述问题,布隆过滤器在数据库中是以位数组来代表历史记录的;以多个hash函数计算数据的hash值,所得值代表在位数组的下标,如果这些hash值对应的位都是1那就说明该数据已经重复,否则没有重复。
- 但布隆过滤器并不准确,hash函数存在hash碰撞,就会出现误判的情况
简单限流
- 限流算法在分布式领域是一个经常被提起的话题,当系统的处理能力有限时,如何阻止
计划外的请求继续对系统施压,这是一个需要重视的问题。除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求。比如在
UGC 社区,用户的发帖、回复、点赞等行为都要严格受控,一般要严格限定某行为在规定
时间内允许的次数,超过了次数那就是非法行为。对非法行为,业务必须规定适当的惩处策
略。
- 系统要限定用户的某个行为在指定的时间里
只能允许发生 N 次这个简单的限流策略,redis可以使用有序集合来进行处理,用Zset定义出一个时间窗口,这个窗口记录着用户的某个行为,只要保证窗口中的行为数量不超过N次就能达到一个简单限流的效果。
- 实现就是通过Zset中的score来圈出一个时间窗口,score用时间戳表示,保留时间窗口内的数据,ZREMRANGEBYSCORE指令剔除时间窗口外的数据
分布式限流redis-cell
- redis-cell 是一个用rust语言编写的基于令牌桶算法的的限流模块,提供原子性的限流功能,并允许突发流量,可以很方便的应用于分布式环境中。
- 令牌桶算法的原理是定义一个按一定速率产生token的桶,每次去桶中申请token,若桶中没有足够的token则申请失败,否则成功。在请求不多的情况下,桶中的token基本会饱和,此时若流量激增,并不会马上拒绝请求,所以这种算法允许一定的流量激增。
- 要使用redis-cell要安装对应的插件
GeoHash
- 要实现附近的人的功能,我们可以通过两点求距离然后进行排序找出附近的人,但这样要遍历所有点然后计算然后排序,就算能通过一个半径R把要遍历的点进行一部分的筛选,以及对X、Y的字段加索引的方式来进行一系列的优化,但在请求量大了还是不乐观的,毕竟数据库的性能是有限的。
- GeoHash算法就在算法的层面极大的优化了性能。该算法将经纬度二维坐标转换成一个字符串,通过字符串来比较两个点之间的距离。具体转换原理是将地球看作是铺平的二维平面,然后对该平面均分四块,秉承上0下1左0右1的原则,得出00、01、10、11四块区域,然后再对其中的一块区域进行依次切分得到16个以二进制表示的区域,然后将二进制通过base32转换成字符串,字符串前缀相同则表示在同一个区域,再切分的区域足够小时可以直接通过同一个区域来匹配附近的人。
- redis实现了GeoHash算法,相应的指令为geoadd、geodist、geopos、geohash,查询附近的人指令georadiusbymember、georadius