redis总结【应用篇】

280 阅读9分钟

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; 
} 
// fastjson 序列化对象中存在 generic 类型时,需要使用 TypeReference 
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(); // 分配唯一的 uuid 
task.msg = msg; 
String s = JSON.toJSONString(task); // fastjson 序列化
jedis.zadd(queueKey, System.currentTimeMillis() + 5000, s); // 塞入延时队列 ,5s 后再试
} 
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); // fastjson 反序列化
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