Memcache、Redis应用场景与高级用法

做项目用了挺多次redis,记不住,还是自己参考+总结下吧。。。

0. memcache区别

redis和memcache这两个作为目前市面上最火的两款缓存,存在即合理,虽然redis有了很多memcache不存在的功能,但是想要完全取代memcache,是很难的。他们之前的共同点肯定是在内存中存储数据,防止高并发影响数据库性能,减少数据库压力,并提高查询速度,巴拉巴拉的一堆堆的,他们之前的区别到底是什么呢?

  • 从数据结构上来说,redis在kv模式上,支持5中数据结构,String、list、hash、set、zset,并支持很多相关的计算,比如排序、阻塞等,而memcache只支持kv简单存储。所以当你的缓存中不只需要存储kv模型的数据时,redis丰富的数据操作空间,绝对是非常好的选择,另外说一句,利用redis可以高效的实现类似于单集群下的阻塞队列、锁及线程通信等功能。

  • 从可靠性的角度来说,redis支持持久化,有快照和AOF两种方式,而memcache是纯的内存存储,不支持持久化的。

  • 从内存管理方面来说,redis也有自己的内存机制,redis采用申请内存的方式,会把带过期时间的数据存放到一起,redis理论上能够存储比物理内存更多的数据,当数据超量时,会引发swap,把冷数据刷到磁盘上。而memcache把所有的数据存储在物理内存里。memcache使用预分配池管理,会提前把内存分为多个slab,slab又分成多个不等大小的chunk,chunk从最小的开始,根据增长因子增长内存大小。redis更适合做数据存储,memcache更适合做缓存,memcache在存储速度方面也会比redis这种申请内存的方式来的快。

  • 从数据一致性来说,memcache提供了cas命令,可以保证多个并发访问操作同一份数据的一致性问题。 redis是串行操作,所以不用考虑数据一致性的问题。

  • 从IO角度来说,选用的I/O多路复用模型,虽然单线程不用考虑锁等问题,但是还要执行kv数据之外的一些排序、聚合功能,复杂度比较高。memcache也选用非阻塞的I/O多路复用模型,速度更快一些。

  • 从线程角度来说,memcahce使用多线程,主线程listen,多个worker子线程执行读写,可能会出现锁冲突。redis是单线程的,这样虽然不用考虑锁对插入修改数据造成的时间的影响,但是无法利用多核提高整体的吞吐量,只能选择多开redis来解决。

  • 从集群方面来说,redis天然支持高可用集群,支持主从,而memcache需要自己实现类似一致性hash的负载均衡算法才能解决集群的问题,扩展性比较低。

另外,redis集成了事务、复制、lua脚本等多种功能,功能更全。redis功能这么全,是不是什么情况下都使用redis就行了呢?

非也,redis确实比memcache功能更全,集成更方便,但是memcache相比redis在内存、线程、IO角度来说都有一定的优势,可以利用cpu提高机器性能,在不考虑扩展性和持久性的访问频繁的情况下,只存储kv格式的数据,建议使用memcache,memcache更像是个缓存,而redis更偏向与一个存储数据的系统。但是,觉得不要拿redis当数据库用!!!

1. string

常用命令:set,get,decr,incr,mget 等。

Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字.

应用场景:String是最常用的一种数据类型,普通的key/ value 存储都可以归为此类.即可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。除了提供与 Memcached 一样的get、set、incr、decr 等操作外,Redis还提供了下面一些操作:

获取字符串长度
往字符串append内容
设置和获取字符串的某一段内容
设置及获取字符串的某一位(bit)
批量设置一系列字符串的内容

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

应用场景

1.图片、手机验证码

需要设置过期时间

@api.route('/image_code/<image_code_id>')
def get_image_code(image_code_id):
    """ 
    获取图片验证码, 并将验证值保存到redis中
    :param image_code_id: 浏览器端带入的图片验证码编号
    :return 正常: 返回图片验证码, 异常:返回json
    """
    # 1. 业务处理逻辑

    # 2. 生成图片验证码
    # 名字, 真是文本, 图片二进制数据
    name, text, image_data = captcha.generate_captcha()

    # 3. 如果使用哈希表,只能同一设置过期时间,不合适
    # 将图片验证吗保存到redis中, 字符串
    try:
        redis_store.setex(
            f'image_code_{image_code_id}',
            constants.IMAGE_CODE_REDIS_EXPIRES,
            text
        )
    except Exception as e:
        current_app.logger.error(e)
        # 图片保存失败, 返回错误json
        return jsonify(errno=RET.DBERR, errmsg='保存图片验证码失败!')

    # 4. 返回
    rsp = make_response(image_data)
    rsp.headers['Content-Type'] = 'image/jpg'
    return rsp
2.登陆限制
    # 检查用户重试登录次数, 限制ip, 而不是手机
    user_ip = request.remote_addr
    try:
        access_nums = redis_store.get(f'access_num_{user_ip}')
    except Exception as e:
        current_app.logger.error(e)
    else:
        if access_nums is not None and \
            int(access_nums) >= constants.LOGIN_ERROR_MAX_TIMES:
            return jsonify(errno=RET.IPERR, errmsg='登录次数过多,请稍后重试')

    # 3.查询用户是否存在
    try:
        user = User.query.filter_by(mobile=mobile).first()
    except Exception as e:
        current_app.logger.error(e)
        return jsonify(errno=RET.DBERR, errmsg='获取用户信息失败')

    if user is None or not check_password_hash(user.password_hash, password):
        try:
            redis_store.incr(f'access_num_{user_ip}')
            redis_store.expire(
                f'access_num_{user_ip}', 
                constants.LOGIN_ERROR_FORBID_TIME
            )
        except Exception as e:
            current_app.logger.error(e)
        return jsonify(errno=RET.USERERR, errmsg='用户名或密码错误')
    return jsonify(errno=RET.OK, errmsg='登录成功')

2. hash

常用命令:hget,hset,hgetall 等,一般用对象存储使用hash

应用场景

1.用户信息

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息。

hash类型的用户信息封装成对象,以序列化的方式存储的案例

2.购物车
# 添加购物车记录, redis购物车使用的是hash类型保存的,用到hget获取key中的字典
        conn = get_redis_connection('default')
        cart_key = 'cart_%d' % user.id
        cart_count = conn.hget(cart_key, sku_id)

        if cart_count:
            count += int(cart_count)

        # sku_id存在即更新,不存在则新建
        conn.hset(cart_key, sku_id, count)

        # 验证商品的库存
        if count > sku.stock:
            return JsonResponse({'res': 4, 'errmsg': '商品库存不足'})

        # 获取购物车的数量
        total_count = conn.hlen(cart_key)

3. list

常用命令:lpush(添加左边元素),rpush,lpop(移除左边第一个元素),rpop,lrange(获取列表片段,LRANGE key start stop)等。

应用场景

1.消息队列系统

场景: 如弹幕,使用sorted set(list类型)甚至可以构建有优先级的队列系统

4. set

set 是string类型的无序集合。集合是通过hashtable实现的,概念和数学中个的集合基本类似,可以交集,并集,差集等等,set中的元素是没有顺序的。所以添加,删除,查找的复杂度都是O(1)。

  • sadd 命令:
    添加一个 string 元素到 key 对应的 set 集合中,成功返回1,如果元素已经在集合中返回 0,如果 key 对应的 set 不存在则返回错误。

应用场景

1.关注、粉丝列表

关注、粉丝列表
Sets 集合的概念就是一堆不重复值的组合list。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。

2.共同特征

求交集、并集、差集等操作,可以实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。

5. SortedSet

常用命令:zadd,zrange,zrem,zcard等

zadd key score member
redis 127.0.0.1:6379> zadd runoob 0 redis
(integer) 1 redis 127.0.0.1:6379> zadd runoob 0 mongodb
(integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 0 redis 127.0.0.1:6379> > ZRANGEBYSCORE runoob 0 1000
1) "mongodb"
2) "rabitmq"
3) "redis"

应用场景

点击
// View 取 点击数
func (video *Video) View() uint64 {
	//返回字符串
	countStr, _ := cache.RedisClient.Get(cache.VideoViewKey(video.ID)).Result()
	//转回uint
	count, _ := strconv.ParseUint(countStr, 10, 64)
	return count
}

// AddView 视频浏览
func (video *Video) AddView() {
	// 增加视频点击数 incr()将键值++
	cache.RedisClient.Incr(cache.VideoViewKey(video.ID))
	// 增加排行点击数
	// 键名 加多少 成员(视频id)
	cache.RedisClient.ZIncrBy(cache.DailyRankKey, 1, strconv.Itoa(int(video.ID)))
}

// DeleteVideo 删除排行榜视频
func (video *Video) DeleteVideo() {
	//删除排行榜视频
	cache.RedisClient.ZRem(cache.DailyRankKey, strconv.Itoa(int(video.ID)))
}

排行榜

Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
可以借助redis的SortedSet进行热点数据的排序。

package service

import (
	"fmt"
	"giligili/cache"
	"giligili/model"
	"giligili/serializer"
	"strings"
)

// DailyRankService 每日排行的服务
type DailyRankService struct {
}

// Get 获取排行
func (service *DailyRankService) Get() serializer.Response {
	var videos []model.Video

	// 从redis读取点击前6的视频
	vids, _ := cache.RedisClient.ZRevRange(cache.DailyRankKey, 0, 5).Result()
	if len(vids) > 1 {
		// 按 id FIELD排序
		order := fmt.Sprintf("FIELD(id, %s)", strings.Join(vids, ","))
		err := model.DB.Where("id in (?)", vids).Order(order).Find(&videos).Error
		if err != nil {
			return serializer.Response{
				Status: 50000,
				Msg:    "数据库连接错误",
				Error:  err.Error(),
			}
		}
	}

	return serializer.Response{
		Data: serializer.BuildVideos(videos),
	}
}

定时任务CronJob

task/cron.go

package tasks

import (
	"fmt"
	"reflect"
	"runtime"
	"time"

	"github.com/robfig/cron"
)

// Cron 定时器单例
var Cron *cron.Cron

// Run 运行
func Run(job func() error) {
	from := time.Now().UnixNano()
	err := job()
	to := time.Now().UnixNano()
	jobName := runtime.FuncForPC(reflect.ValueOf(job).Pointer()).Name()
	if err != nil {
		fmt.Printf("%s error: %dms\n", jobName, (to-from)/int64(time.Millisecond))
	} else {
		fmt.Printf("%s success: %dms\n", jobName, (to-from)/int64(time.Millisecond))
	}
}

// CronJob 定时任务
func CronJob() {
	if Cron == nil {
		Cron = cron.New()
	}
	//每年一次 更新排行榜
	Cron.AddFunc("0 0 0 1 1 *", func() { Run(RestartDailyRank) })
	//每日一次 更新投稿数
	Cron.AddFunc("0 0 0 * * *", func() { Run(RestartPucnt) })
	Cron.Start()

	fmt.Println("Cronjob start.....")
}

cron/rank.go

package tasks

import (
	"giligili/cache"
	"giligili/model"
)

// RestartDailyRank 重启一天的排名(误)
func RestartDailyRank() error {
	return cache.RedisClient.Del("rank:daily").Err()
}

// RestartPucnt 重启一天投稿数
func RestartPucnt() error {
	// var user model.User
	err := model.DB.Table("users").Updates(map[string]interface{}{"upcnt": 0}).Error
	return err
}

Redis高级

bitmap 基本位图实现存在问题

即使只存一个数(最大的),存储空间也是512MB
对于原始的Bitmap来说,这就需要2 ^ 32长度的bit数组
通过计算可以发现(2 ^ 32 / 8 bytes = 512MB), 一个普通的Bitmap需要耗费512MB的存储空间
不管业务值的基数有多大,这个存储空间的消耗都是恒定不变的,这显然是不能接受的。
redis 的基本的位图实现就存在这个问题

BitMap通常被用作快速查询的数据结构,但它太占内存了。解决方案是,对BitMap进行压缩。

HyperLogLog

如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成。如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取 set 集合的长度就是去重阅读量。但是如果爆款文章阅读量太大,set 会浪费太多存储空间。这时候我们就要使用 Redis 提供的 HyperLogLog 数据结构来代替 set,它只会占用最多 12k 的存储空间就可以完成海量的去重统计。但是它牺牲了准确度,它是模糊计数,误差率约为 0.81%。

Roaring Bitmap实现

Roaring bitmaps是一种超常规的压缩BitMap。它的速度比未压缩的BitMap快上百倍。

我们在Redis里经常使用位图存储数据(Redis原生以字符串的形式支持位图),当然也就会遇到稀疏位图浪费存储空间的问题。但要让Redis支持RBM,需要引入专门的module,项目地址见这里。它的设计思想与Java版RBM几乎相同,不再废话了。

RBM实现源理
将32位无符号整数按照高16位分桶,即最多可能有216=65536个桶,论文内称为container。存储数据时,按照数据的高16位找到container(找不到就会新建一个),再将低16位放入container中。也就是说,一个RBM就是很多container的集合。

参考:

https://juejin.im/post/5d8882c8f265da03951a325e

https://www.cnblogs.com/ncby/p/11169127.html

https://www.jianshu.com/p/40dbc78711c8

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值