Redis实战--文章投票功能redis实现

1.特性

  • 速度非常快的非关系型数据库
  • 可以存储键与5种不同类型的值之间的映射
  • 可以将内存中键值对数据持久化到硬盘
  • 可以使用赋值特性来扩展读性能
  • 可以使用客户端分片来扩展写性能

2.注意点

  • 在向列表推入新元素之后,会返回列表当前的长度。
  • lindex key index 获取指定索引位置的值
  • 如果集合元素非常多,使用SMEMBERS这个命令可能会非常慢
  • REDIS事务和关系型数据库有很大的不同,REDIS事务中某个命令因数据结构而出错时,该命令不会执行,而事务中的其他命令依然会执行

3.示例

为了更好的理解redis的用处,可以根据多个实例来理解它。

  • 3.1 对文章进行投票

    • 3.1.1 redis数据类型设计,db.py

      import redis
      
      
      def get_redis_client():
          return redis.Redis(host='****', port=*, db=*)
      
      
      if __name__ == '__main__':
          # 测试连接
          redis_cli: redis = get_redis_client()
          # rs: bool = redis_cli.set("name", "bob")
          # rs1: bytes = redis_cli.get('name')
          # print(rs, rs1.decode('utf-8'))
      
          # 实例化命令管道
          pipe = redis_cli.pipeline()
          try:
              # 开启事务
              pipe.multi()
              # 使用hash结构存储每篇文章信息
              article_entity = {
                  'title': '活着',
                  'link': 'http://huozhe.com',
                  'poster': '余华',
                  'time': 1559185143.32,
                  'votes': 528
              }
              pipe.hmset('article:10086', article_entity)
      
              # 根据发布时间排序文章有序集合(zset结构)
              article_time = {
                  'article:10086': 1559185143.32,
                  'article:10087': 1559185155.32,
                  'article:10088': 1559185188.32
              }
              pipe.zadd('time:', article_time)
      
              # 根据文章分数排序文章有序集合
              article_score = {
                  'article:10086': 7463,
                  'article:10087': 19431,
                  'article:10088': 5732
              }
              pipe.zadd('score:', article_score)
      
              # 每篇文章已投票用户组(使用set结构)
              article_user = ('user:23', 'user:34', 'user:77', 'user:45')
              pipe.sadd('voted:10086', *article_user)
      
              # 执行事务
              pipe.execute()
              print('redis数据填充成功')
          except Exception as e:
              print(e)
              pass
          finally:
              # 返回连接
              pipe.reset()
      
    • 3.1.2 实现文章投票逻辑,article_vote.py

import time

from db import get_redis_client


class ArticleVote(object):
    # 一周的秒数
    ONE_WEEK_IN_SECONDS = 7 * 86400
    # 一票的分数
    VOTE_SCORE = 432
    # 每页文章数
    ARTICLE_PER_PAGE = 25

    def __init__(self, conn):
        self.conn = conn

    def article_vote(self, user: str, article: str):
        """
        文章投票函数
        :param user: 投票用户    user:22
        :param article: 文章   article:10088
        :return:
        """
        # 计算一周前的时间点
        cutoff = time.time() - self.ONE_WEEK_IN_SECONDS
        # 判断文章发布时间是否过了一周
        if self.conn.zscore('time:', article) < cutoff:
            return False

        article_id = article.partition(':')[-1]
        try:
            # 将用户添加到该文章已投票用户列表中去,根据返回bool判断是否投过票
            if self.conn.sadd('voted:' + article_id, user):
                # 增加对应文章的分数
                self.conn.zincrby(name='score:', value=article, amount=self.VOTE_SCORE)
                # 增加对应文章的票数
                self.conn.hincrby(article, 'votes', 1)
                return True
            return False
        except Exception as e:
            print(e)

    def post_article(self, user, title, link):
        """
           发布新文章
           :param user:
           :param title:
           :param link:
           :return:
           """
        # 字符出自增生成文章id
        article_id = self.conn.incr('article:')

        # 将作者添加到以投票用户组,并设置过期时间为一周
        voted = 'voted:' + article_id
        self.conn.sadd(voted, user)
        self.conn.expire(voted, self.ONE_WEEK_IN_SECONDS)

        # 添加文章,添加文章分数和时间有序集合
        now = time.time()
        article = 'article:' + article_id
        self.conn.hmset(article, {
            'title': title,
            'link': link,
            'poster': user,
            'time': now,
            'votes': 1
        })

        self.conn.zadd('score:', article, now + self.VOTE_SCORE)
        self.conn.sadd('time:', article, now)

        return article_id

    def get_articles(self, page, order='score:'):
        """
        获取分数高的文章
        :param page:
        :param order:
        :return:
        """
        # 获取起始页
        start = (page - 1) * self.ARTICLE_PER_PAGE
        end = start + self.ARTICLE_PER_PAGE - 1

        ids = self.conn.zrevrange(order, start, end)
        # 文章列表
        articles = []
        for _id in ids:
            article_detail = self.conn.hgetall(_id)
            article_detail['id'] = _id
            articles.append(article_detail)

        return articles

    def add_remove_group(self, article_id: str, add_group: list, remove_group: list):
        """
        添加移除文章到分组
        :param article_id:
        :param add_group:
        :param remove_group:
        :return:
        """
        article = 'article:' + article_id
        # 将该文章添加到所属分组中
        for group in add_group:
            self.conn.sadd('group:' + group, article)

        # 将文章从分组中移除
        for group in remove_group:
            self.conn.srem('group:' + group, article)

    def get_group_articles(self, group: str, page: int, order: str = 'score:'):
        """
        获取分组文章
        :param group:
        :param page:
        :param order:
        :return:
        """
        key = order + group
        # 如果缓存中没有该数据,就去redis中获取
        if not self.conn.exists(key):
            self.conn.zinterstore(key, ['group:' + group, order], aggregate='max')
            # 将结果缓存60s
            self.conn.expire(key, 60)

        return self.get_articles(page, key)


if __name__ == '__main__':
    conn = get_redis_client()
    article_vote = ArticleVote(conn)
    # 36号用户投票10086
    result = article_vote.article_vote('user:36', 'artile:10086')
    print(result)

这个例子时redis实战里面的例子,它上面推荐说sadd,zincrby,hincrby这三个命令要使用事务,但是redis的事务和普通关系型数据事务很不一样,事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。这一切都因为redis的高性能。

我在编写程序时,zincrby这个命令因为参数的错误导致没有添加成功,而其他两条命令成功了,即使添加了事务也不能解决问题,并且在程序中我是用到了某条命令的返回值,但是这条命令时加入了批处理事务中的,在当时那瞬间时不会执行到redis的,这也是redis事务的弊端。如果我们在程序中要想安全又想使用到某条命令的返回值来做选择性操作,我们可以使用redis的分布式锁redlock。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355