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 artiles = new ArrayList<>();
发布文章的代码:
// 发表文章
public static void publish(Map 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的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 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 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.repacket
和com.itcrud.redis.vote
两个包中!!!
本文作者:程序猿杨鲍
版权归作者所有,转载请注明出处