美文网首页算法
Redis(二):实战场景及实现方式

Redis(二):实战场景及实现方式

作者: 雪飘千里 | 来源:发表于2019-08-14 20:14 被阅读0次

1、String 类型使用场景

  • 场景一:商品库存数
    从业务上,商品库存数据是热点数据,交易行为会直接影响库存,而redis自身String类型提供了下面这几个命令
incr key && decr key  递增/递减
incrby key increment && decrby key decrement  增加/减少指定值

set goods_id 10; 设置id为good_id的商品的库存初始值为10;
decr goods_id ; 当商品被购买时,库存数据减1

依次类推同样的场景,商品的浏览次数,问题或者回复的点赞次数等,这种计数的场景都可以考虑利用Redis 来实现

  • 场景二:时效信息存储
    这个也就是我们平时用的最多的场景,存储value的时候设置过期时间,时间一到,自动删除

  • 实现方式
    String在redis内部存储默认就是一个字符串(SDS),被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

2、List类型使用场景

List是按照插入数据排序的字符串链表,可以在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为O(1))

  • 场景一:最新上架商品
    在交易网站首页,经常会有推荐新上架产品的模块,这个模块是存储了最新上架前100名

这时候使用Redis的list数据结构,来进行 TOP 100新上架产品的存储,下面以伪代码演示:

// 1、把新上架的商品添加到链表中
ret = r.lpush("new:goods",goodsId)
// 2、通过ltrim裁减list链表,使之包含 指定范围内的指定元素 
r.ltrim("new:goods",0,99)
// 3、获取前100个最新上架的商品 id 列表
new_goods_list = r.lrange("new:goods",0,99)

  • 场景二:消息队列实现
    根据List 链表的特性,可以实现消息队列的要求,当然,目前有很多专业的消息队列组件,kafka、rabbitMQ等;

List 存储就是一个队列的存储形式:
1、lpush key value;在key对应list的头部添加字符串元素;
2、rpop key;移除key对应list列表的最后一个元素,返回值为移除的元素

  • 实现方式
    redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。

    如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为快速链表 quicklist 的一个结构。

    首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
    当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prev 和 next 。

    所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3、set类型使用场景

Set也是存储了一个集合列表的功能,但是,和list不同,set具备去重功能。当需要存储一个列表信息,同时要求列表内的元素不能有重复,这时候使用set比较适合。与此同时,set还可以实现交集、并集、差集。

例如,在交易网站,我们会存储用户感兴趣的商品信息,在进行相似性用户分析的时候,可以通过计算两个不同用户之间感兴趣商品的数量来提供一些依据。

下面以伪代码演示:

//userId为用户id,goodid为感兴趣的商品id
sadd  "user:userId"  goodId;

sadd  "user:101"  1;
sadd  "user:101"  2;
sadd  "user:102"  1;
sadd  "user:102"  3;

interResult = sinter "user:101"  "user:102"  返回直接定集合的交集;1
unionResult = sinter "user:101"  "user:102"  返回直接定集合的并集;1,2,3
diffResult = sinter "user:101"  "user:102"  返回直接定集合的差集;2,3

获取到两个用户相似的产品,然后确定相似产品的类目就可以进行用户分析。

类似的场景还有,社交场景下共同关注好友,相似兴趣tag 等场景的支持。

  • 实现方式
    Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。

4、Hash类型使用场景

Redis在存储对象(例如,用户信息)的时候,需要对对象进行系列化转换然后进行存储。

还有一种形式,就是将对象数据转换为JSON结构数据,然后存储json字符串到Redis。

其实,对于一些对象类型,还有一种比较方便的类型,那就是按照Redis的Hash类型进行存储

例如,我们存储一些网站用户的基本信息,我们可以使用

hset key field value

hset user101 name "小明"
hset user101 sex "男"
hset user101 phone "123456"

这样我们就存储了一个用户的基本信息,存储信息有{name : 小明,sex : 男,phone : 123456}
当然这种类似场景还非常多,比如存储订单的数据,产品的数据,商家基本信息等。

  • 实现方式
    Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

5、Sorted Set 类型使用场景

Redis sorted set的使用场景与set类似,区别是set不是自动有序,而sorted set可以通过提供一个score参数来为存储数据排序,并且是自动排序,插入即有序。

业务中,如果需要一个有序且不重复的集合列表,就可以选择sorted set这种数据结构

比如,商品的购买热度可以将购买总量num当做商品列表的score,这样获取最热门的商品时,就可以自动按售卖总量排好序。

sorted set适合有排序需求的集合存储场景。

  • 实现方式
    有序集合的编码可以是ziplist和skiplist之一。

有序集合对象使用ziplist编码需要满足两个条件:一是所有元素长度小于64字节;二是元素个数小于128个;不满足任意一条件将使用skiplist编码。
以上两个条件可以在Redis配置文件中修改zset-max-ziplist-entries选项和zset-max-ziplist-value选项。ziplist编码结构如下

image.png

skiplist编码的有序集合对象底层实现是跳跃表和字典两种:
一个是 dict(字典),key是成员,value是分值,用于支持 O(1) 复杂度的按成员取分值操作
一个是 skiplist(跳跃表),按分值排序成员,用于支持平均复杂度为O(log N)的按分值定位成员的操作,以及范围操作;

备注:上面提到的ziplist quicklist skiplist数据结构详解见Redis(七):Redis底层数据类型

6、分布式环境下常见的应用场景之分布式锁

当多个进程不在同一个系统中时,用分布式锁控制多个进程对资源的操作或者访问。

分布式锁可以避免不同进程重复相同的工作,减少资源浪费。同时分布式锁可以避免破坏数据正确性的发生,例如多个进程对同一个订单操作,可以导致订单状态错误覆盖。。。

  • 定时任务重复执行
    随着业务的发展,业务系统势必发展为集群分布式模式,如果我们需要一个定时任务来进行订单状态的统计,比如,每15分钟统计一下所有未支付的订单数量,那么我们启动定时任务的时候,肯定不能同一时刻多个业务后台服务都去执行定时任务,这样就会带来重复计算以及业务逻辑混乱的问题。

    这时候,就需要使用分布式锁,进行资源的锁定。那么在执行定时任务的函数中,首先进行分布式锁的获取,如果可以获取的到,那么这台机器就执行正常的业务数据统计逻辑计算,如果获取不到则证明目前已有其他的服务进程执行这个定时任务,就不用自己操作执行了,只需要返回就行了,如下图所示:

image.png

上面的这种业务场景,也可以使用分布式任务调度框架xxjob来实现。

  • 避免用户重复下单
    分布式锁的实现方式有很多种:
    1、数据库乐观锁方式(数据库加一个版本号)
    2、基于Redis的分布式锁
    3、基于ZK的分布式锁(Zookeeper基础(五):分布式锁

分布式锁的实现要保证几个基本点:
1、互斥性:任意时刻,只有一个资源能够获取到锁
2、容灾性:能够在未成功释放锁的情况下,一定时限内能够恢复锁的正常功能
3、统一性:加锁和解锁保证同一资源来进行操作

image.png image.png

7、分布式环境下常见的应用场景之分布式自增id

随着用户以及交易量的增加,我们可能会针对用户数据,商品数据,以及订单数据进行分库发表的操作,这时候由于进行了分库分表的行为,所以mysql自增id的形式来唯一表示一行数据的方案不可行了,因此需要一个分布式id生成器,来提供唯一id的信息。

通常对于分布式自增id的实现方式有下面几种:
1、利用数据库自增id的属性
2、通过uuid来实现唯一id生产
3、Twitter的SnowFlake算法
4、利用Redis生成唯一id

我们使用redis的incr命令来实现唯一id,因为Redis是单进程单线程架构,不会因为多个取号方的incr命令导致取号重复,因此,基于Redis的incr命令实现序列号的生成基本能满足全局唯一与单调自增的特性

8、Redis发布订阅使用应用场景

Redis有一个发布订阅的通信方式,发送者publish发送消息,订阅者subscribe接收消息。

下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

image.png

当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

image.png

监听

/**
 * 订阅者
 */
public class RedisSubTest {
    @Test
    public void subjava() {
        System.out.println("订阅者 ");
        Jedis jr = null;
        try {
            jr = new Jedis("127.0.0.1", 6379, 0);// redis服务地址和端口号
// redis发布订阅消息监听器 需要继承JedisPubSub;当然也可以像下面这种写法
JedisPubSub jedisPubSub = new JedisPubSub(){
            @Override  //收到消息会调用
            public void onMessage(String channel, String message) {      
                System.out.println(String.format("receive redis published message, channel %s, message %s", channel, message));
            }
            @Override //订阅了频道会调用
            public void onSubscribe(String channel, int subscribedChannels) {    
                System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
                        channel, subscribedChannels));
            }
            @Override //取消订阅 会调用
            public void onUnsubscribe(String channel, int subscribedChannels) {   
                System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
                        channel, subscribedChannels));

            }
        };

            // jr客户端配置监听两个channel
            jr.subscribe(jedisPubSub, "news.share", "news.blog");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jr != null) {
                jr.disconnect();
            }
        }
    }



}
/**
 * 发布者
 */
public class RedisPubTest {
    @Test
    public void pubjava() {
        System.out.println("发布者 ");
        Jedis jr = null;
        try {
            jr = new Jedis("127.0.0.1", 6379, 0);// redis服务地址和端口号
            // jr客户端配置监听两个channel
            jr.publish( "news.share", "新闻分享");
            jr.publish( "news.blog", "新闻博客");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jr != null) {
                jr.disconnect();
            }
        }
    }
}
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

9、key设计原则

9.1 key名设计

  • 可读性和可管理性【建议】
    以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
ugc:video:1

用冒号作为分割是设计key的一种不成文的原则,遵循这种格式设计出的key在某些redis客户端下可以有效的识别

  • 简洁性【建议】
    保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:
user:{uid}:friends:messages:{mid}
简化为
u:{uid}:fr:m:{mid}。
  • 不要包含特殊字符【强制】
    反例:包含空格、换行、单双引号以及其他转义字符

9.2 value设计

  • 拒绝bigkey(防止网卡流量、慢查询)
    string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。

    反例:一个包含200万个元素的list。

    非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),删除方法

//1. Hash删除: hscan + hdel
public void delBigHash(String host, int port, String password, String bigHashKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams);
        List<Entry<String, String>> entryList = scanResult.getResult();
        if (entryList != null && !entryList.isEmpty()) {
            for (Entry<String, String> entry : entryList) {
                jedis.hdel(bigHashKey, entry.getKey());
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    
    //删除bigkey
    jedis.del(bigHashKey);
}


//2. List删除: ltrim
public void delBigList(String host, int port, String password, String bigListKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    long llen = jedis.llen(bigListKey);
    int counter = 0;
    int left = 100;
    while (counter < llen) {
        //每次从左侧截掉100个
        jedis.ltrim(bigListKey, left, llen);
        counter += left;
    }
    //最终删除key
    jedis.del(bigListKey);
}


//3. Set删除: sscan + srem
public void delBigSet(String host, int port, String password, String bigSetKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams);
        List<String> memberList = scanResult.getResult();
        if (memberList != null && !memberList.isEmpty()) {
            for (String member : memberList) {
                jedis.srem(bigSetKey, member);
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    
    //删除bigkey
    jedis.del(bigSetKey);
}


//4. SortedSet删除: zscan + zrem
public void delBigZset(String host, int port, String password, String bigZsetKey) {
    Jedis jedis = new Jedis(host, port);
    if (password != null && !"".equals(password)) {
        jedis.auth(password);
    }
    ScanParams scanParams = new ScanParams().count(100);
    String cursor = "0";
    do {
        ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams);
        List<Tuple> tupleList = scanResult.getResult();
        if (tupleList != null && !tupleList.isEmpty()) {
            for (Tuple tuple : tupleList) {
                jedis.zrem(bigZsetKey, tuple.getElement());
            }
        }
        cursor = scanResult.getStringCursor();
    } while (!"0".equals(cursor));
    
    //删除bigkey
    jedis.del(bigZsetKey);
}
  • 选择适合的数据类型【推荐】
    例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)
//反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football


//正例:
hmset user:1 name tom age 19 favor football
  • 控制key的生命周期,redis不是垃圾桶【推荐】
    建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。

相关文章

网友评论

    本文标题:Redis(二):实战场景及实现方式

    本文链接:https://www.haomeiwen.com/subject/wzuagqtx.html