Redis学习笔记(三):Redis应用之投票、红包

Redis基本数据类型及基本命令的使用都已经做完笔记了,接下来就需要将这些笔记实际运用到项目中。经常在项目中用到的就是缓存常量数据,还有一些基本的计数等操作,比如我的博客里面访问量、文章阅读量都是缓存在Redis中的,累加阅读量、访问量都是在Redis中完成,夜间定时刷入数据库的,这样就不用每次访问都去数据库中查询。基本应用没有问题,那来点稍微复杂的呢,这篇文章就让我们一起来看看其他的应用场景,将从文章投票排行榜、红包出发来依次说说具体使用何种数据结构合适。

叙述

在面试的时候经常会被用问Redis用到哪些数据类型,很明显大多数使用过redis的人都是可以回答上来,但是也仅仅是回答上来几种数据类型的名字和存储结构。如果我是面试官,这个回答只能得到5分。为什么?因为几种基本数据类型谁都可以说的出来,就是几个名词和释义而已,5分钟就可以背下来。

如果是我回答我会按这样的流程来说(个人理解):

  • 5种数据类型,分别是什么
  • 5种数据类型的存储结构是什么样的,以及存储特点(str、hash、set、zset、list,zset有分数机制,可排序,set元素不重复,可以做交并差集计算等)
  • 针对不同的存储特点说明不同数据类型能够应用在什么场景下(具体场景细节可以不说,等面试官深挖)

前两个点应该没什么难度。后面的有点难度,特别是没有怎么用过redis其他数据类型的,因为很多公司使用redis不会很深。下面简单介绍一下:

  • string:字符串类型,可用来缓存文章访问量、IP访问量,存储方便,空间占用不高,节省内存,同时可以通过incr命令来实现自增,不需要繁杂的操作(查询、修改再插入);也可以用来做常量的缓存,对全局使用的常量数据进行缓存,这种常量数据往往是存储在数据库中,且被访问很频繁,为了降低数据库的访问压力,采用此方式可以更高效。
  • hash:使用的是键值的结构,有filed和value,往往可以将filed看成是字段名,value看成是字段对应的值,可以用来做对象的存储,也可以应用在购物车上,记录当前用户购物车上的商品信息。
  • set:相当于Java的set集合,元素不重复是最常用的一个特点,可用来排重,如投票系统,一个人只能给一篇文章投一票,就可以用set集合来记录当前文章投过票的人员信息。同时set集合也可以做交并差集的计算(如给用户定向推送文章,获取多个用户的共同爱好,同时批量给多个用户推送)。
  • zset:有分数机制,可排序,可以用在需要排序的地方,如购物车商品的加入时间排序,投票排行榜的排序等。
  • list:可用来实现队列,可以用在红包上面等。

当然这些数据结构应用的场景不止这么多,可以根据自己项目中实际实用情况调整。

这些都介绍完了,下面我们一起看看其在投票排行榜、红包上的应用是怎么做的吧。

文章投票

文章投票主要包含的几个功能有以下几种。

  • 发布文章
  • 投票
  • 展示投票信息
发布文章
//准备的常量信息、Jedis连接池、发表的文章集合
private static JedisPool JEDIS_POOL = new JedisPool("host", 6379);
private final static String ARTICLE_ID = "article:id:incr";
private final static String ARTICLE_PREFIX = "article:";
private final static String ARTICLE_QUEUE = "article:queue";
private final static String ARTICLE_VOTE = "article:vote:";
public static List<Long> artiles = new ArrayList<>();

发布文章的代码:

// 发表文章
public static void publish(Map<String, String> article) {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //自增生成ID(使用str)
        Long id = resource.incr(ARTICLE_ID);
        artiles.add(id);
        article.put("id", id.toString());
        article.put("viewCount", "0");//访问量
        //存储文章信息(使用str)
        resource.hmset(ARTICLE_PREFIX + id, article);
        //将文章加入排行榜中(使用zset)
        resource.zadd(ARTICLE_QUEUE, 0D, ARTICLE_PREFIX + id);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}
  • 通过redis字符串类型实现ID的自增长操作,incr方法执行后会将当前自增的ID值返回,因为redis是单线程的,所以这个ID的自增即使在高并发、分布式系统也是安全可靠的。
  • 存储文章信息,采用redis的hash类型存储,因为文章本身有一个访问量的属性,每次被访问就会做累加,可以使用hincrby命令直接实现。(如果是序列化成JSON,就需要查出来,反序列化成对象,对访问量累加,再插入,会有两次连接redis操作和反序列化过程,相对于hash结构,性能和效率都是逊色的)
  • 文章创建后,会将其放在zset中,默认分数是0,每次投票对分数做累加操作
投票
//投票,一篇文章一个人只能投票一次
public static void vote(Long articleId, Long userId) {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //检查当前用户是否已经投过票(使用set)
        Long addResult = resource.sadd(ARTICLE_VOTE + articleId, userId.toString());
        if (addResult == 0) {
            System.out.println("此用户已为此文章投过票,请勿重复投票!");
            return;
        }
        resource.zincrby(ARTICLE_QUEUE, 1D, ARTICLE_PREFIX + articleId);
        System.out.println(String.format("投票成功,用户:【%s】,文章:【%s】", userId, articleId));
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}
  • 一个人不能重复给一篇文章投票,那么就针对每篇文章创建一个set集合,每次投票后,将用户ID放到对应的set集合中,如果添加成功,累加一票,如果添加失败,表示已经投过票。
  • 使用set集合的zincrby命令做累加一票的操作。
获取排行榜信息

将投票的排行榜信息展示出来。

//获取排行榜信息
public static void rank() {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //获取排行榜
        Set<Tuple> tuples = resource.zrevrangeWithScores(ARTICLE_QUEUE, 0L, -1L);
        for (Tuple tuple : tuples) {
            System.out.println("文章编号:" + tuple.getElement() + ",文章分数:" + tuple.getScore());
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}

zset的默认排序是正序排列,按照分数从低到高,但是这里需要看排行榜,就需要使用倒序排列。使用zrevrange命令,同时将具体得分信息获取。

模拟发布和投票过程

模拟发布文章,然后模拟100个用户投出500票,包括重复投票,最后将排行榜信息输出。

public static void main(String[] args) {
    //创建文章(创建10篇文章,id:1~10)
    for (int i = 1; i <= 10; i++) {
        Map<String, String> article = new HashMap<>();
        article.put("title", "文章标题");
        article.put("content", "文章内容");
        publish(article);
    }
    //投票(随机100个用户,总共投500票)
    for (long i = 0; i < 500; i++) {
        vote(artiles.get(new Random().nextInt(10)), (long) new Random().nextInt(100));
    }
    //获取排行榜
    rank();
}

到这里一个简易的投票和排行榜实现过程就结束啦。其中使用到了四种数据类型,分别是set、zset、字符串和hash,每种数据类型各司其职,都发挥了自己的优势。

红包

看了投票排行榜的套路,红包也是类似的,选择合适的数据结构做合适的事。这里设计的发红包逻辑简单一点,就两个功能点,分别是发红包和抢红包。

发红包
//准备的常量信息、Jedis连接池
private static JedisPool JEDIS_POOL = new JedisPool("host", 6379);
private static final String RED_PACKET_LIST = "redpacket:list";
private static final String RED_PACKET_USER = "redpacket:user";
private static final String RED_PACKET_QUEUE = "redpacket:queue";

发红包实现过程:

//发红包
public static void publish(Integer money) {
    try (Jedis resource = JEDIS_POOL.getResource()) {
        //模拟红包分配(使用list)
        resource.lpush(RED_PACKET_LIST, 0.13 * money + "",
                0.30 * money + "", 0.23 * money + "", 0.15 * money + "", 0.19 * money + "");
    } catch (Exception e) {
        e.printStackTrace();
    }
}
抢红包
//抢红包
public static void rob(Long userId) {
    try (Jedis resource = JEDIS_POOL.getResource()) {
        //判断用户是否已经抢过红包(使用set)
        Long result = resource.sadd(RED_PACKET_USER, userId.toString());
        if (result == 0) {
            System.out.println("用户【" + userId + "】已经抢过红包!");
            return;
        }
        String redpacket = resource.rpop(RED_PACKET_LIST);
        if (StringUtils.isBlank(redpacket)) {
            System.out.println("红包已经抢完!");
            return;
        }
        System.out.println("恭喜用户【" + userId + "】抢到红包,金额:【" + redpacket + "元】");
        //记录抢红包的顺序
        resource.zadd(RED_PACKET_QUEUE, (double) System.currentTimeMillis(), userId.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

抢红包之前的双重判断,是否已经抢过、红包是否已经抢完。当抢到红包后,将红包和用户信息存储到zset集合中。用时间戳作为分数,用来记录抢到红包的先后顺序。当然这里可以使用list队列来记录,效果是一样的。

模拟发红包和抢红包过程
public static void main(String[] args) {
    //发红包
    publish(100);
    //抢红包(10个用户抢红包)
    for (int i = 0; i < 10; i++) {
         new Thread(() -> rob((long) new Random().nextInt(10)));
    }
}

这里就发一个红包,分为5个小红包,模拟10个用户去抢,使用多个线程来模拟多个用户。

抢红包整个实现逻辑使用到3种数据类型,分别是zset、list、set。

上面的两个示例将5种基本类型都囊括在内了,不同的数据类型根据存储数据结构、存储特性的不同,被用来存储不同的数据。其实不管用在什么场景下,整体思路是不变的,那就是用合适的数据类型存储对应的数据。

除了上面的两个示例,其实还有很多种,比如说购物车,未登陆状态下加入购物车,登陆后如何将购物车合并到用户下原有的购物车中,购物车内商品加入的顺序,每个商品加入的个数,商品的属性信息,购物车有效时间等等。

Source Code

码云(gitee):https://gitee.com/itcrud/itcrud-note/tree/master/itcrud-redis

两个示例分别在com.itcrud.redis.repacketcom.itcrud.redis.vote两个包中!!!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿洞晓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值