redis应用

redis应用

介绍

官网:redis.io tutorial

REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。

Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库,其具备如下特性:

  • 基于内存运行,性能高效
  • 支持分布式,理论上可以无限扩展
  • key-value存储系统
  • 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API

相比于其他数据库类型,Redis具备的特点是:

  • C/S通讯模型
  • 单进程单线程模型
  • 丰富的数据类型
  • 操作具有原子性
  • 持久化
  • 高并发读写
  • 支持lua脚本

redis单线程问题
所谓的单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。
redis采用多路复用机制:即多个网络socket复用一个io线程,实际是单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流.

Redis应用:token生成、session共享、分布式锁、自增id、验证码等。

安装

Linux下安装

可从http://redis.io/download下载最新稳定版本,并安装。

$ wget http://download.redis.io/releases/redis-2.8.17.tar.gz
$ tar xzf redis-2.8.17.tar.gz
$ cd redis-2.8.17
$ make
$ cd src
$ ./redis-server
$ ./redis-server ../redis.conf

make完后 redis-2.8.17目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli,两个程序位于安装目录 src 目录下。

redis.conf 是一个默认的配置文件。我们可以根据需要使用自己的配置文件。

ubuntu安装

$sudo apt-get update
$sudo apt-get install redis-server
$ redis-server

redis-cli使用

$ redis-cli -h host -p port -a password   //远程
$ redis-cli
127.0.0.1:6379> auth 123456  // 默认没有密码,当设置密码时需要auth 
OK
redis 127.0.0.1:6379>ping
PONG
127.0.0.1:6379> help
redis-cli 3.0.6
Type: "help @<group>" to get a list of commands in <group>
      "help <command>" for help on <command>
      "help <tab>" to get a list of possible help topics  // 按tab可以切换不同topics
      "quit" to exit
redis 127.0.0.1:6379> CONFIG SET loglevel "notice"
OK
redis 127.0.0.1:6379> CONFIG GET loglevel

1) "loglevel"
2) "notice"
redis 127.0.0.1:6379> CONFIG GET *

基础

redis通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。

redis的数据结构

String类型

它是一个二进制安全的字符串,意味着它不仅能够存储字符串、还能存储图片、视频等多种类型, 最大长度支持512M。

支持的命令:SET、GET

127.0.0.1:6379> set wstrings wang
OK
127.0.0.1:6379> get wstrigns
(nil)
127.0.0.1:6379> get wstrings
"wang"

哈希类型

该类型是由field和关联的value组成的map,特别适合存储对象。其中,field和value都是字符串类型的。

支持的命令: hmset、hget、hgetall、hkeys

127.0.0.1:6379> hmset whash f1 v1 f2 v2 name wang id 100 score 100
OK
127.0.0.1:6379> hgetall whash
 1) "f1"
 2) "v1"
 3) "f2"
 4) "v2"
 5) "name"
 6) "wang"
 7) "id"
 8) "100"
 9) "score"
10) "100"
127.0.0.1:6379> hget whash f1
"v1"
127.0.0.1:6379> hget whash name
"wang"
127.0.0.1:6379> hget whash score
"100"

列表类型

该类型是一个插入顺序排序的字符串元素集合, 基于双链表实现。

支持的命令:lpush、rpush、lrange、llen

127.0.0.1:6379> lpush wlist redis
(integer) 1
127.0.0.1:6379> lpush wlist mongodb
(integer) 2
127.0.0.1:6379> rpush wlist mysql
(integer) 3
127.0.0.1:6379> lrange wlist 0 3
1) "mongodb"
2) "redis"
3) "mysql"

集合类型

Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。Set类型的底层是通过哈希表实现的。Set类型主要应用于:在某些场景,如社交场景中,通过交集、并集和差集运算,通过Set类型可以非常方便地查找共同好友、共同关注和共同偏好等社交关系。

支持的命令:sadd、smembers

127.0.0.1:6379> sadd wset redis
(integer) 1
127.0.0.1:6379> sadd wset mysql
(integer) 1
127.0.0.1:6379> sadd wset redis
(integer) 0
127.0.0.1:6379> smembers wset
1) "redis"
2) "mysql"
127.0.0.1:6379> scard wset
(integer) 2

顺序集合类型

ZSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。与Set类型一样,其底层也是通过哈希表实现的。

支持的命令:zadd、zrange、zcard

127.0.0.1:6379> zadd wzset 0 redis
(integer) 1
127.0.0.1:6379> zadd wzset 3 mysql
(integer) 1
127.0.0.1:6379> zadd wzset 2 mongodb
(integer) 1
127.0.0.1:6379> zcard wzset
(integer) 3
127.0.0.1:6379> zrange wzset 0 3
1) "redis"
2) "mongodb"
3) "mysql"
127.0.0.1:6379> zrange wzset 0 4
1) "redis"
2) "mongodb"
3) "mysql"
127.0.0.1:6379> zrange wzset 0 4 withscores
1) "redis"
2) "0"
3) "mongodb"
4) "2"
5) "mysql"
6) "3"

key命令

支持的命令:keys, type, del, exists

127.0.0.1:6379> keys *
1) "chen"
2) "www"
3) "get"
4) "runoob"
5) "wwang"
127.0.0.1:6379> type chen
list
127.0.0.1:6379> type get
hash
127.0.0.1:6379> del www
(integer) 1
127.0.0.1:6379> exists www
(integer) 0

发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。客户端可以订阅任意数量的频道。

支持的命令: subscribe、publish

127.0.0.1:6379> subscribe redischat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redischat"
3) (integer) 1
1) "message"
2) "redischat"
3) "redis is a great caching technique"
1) "message"
2) "redischat"
3) "redis is nosql db"

// another client
127.0.0.1:6379> publish redischat "redis is a great caching technique"
(integer) 1
127.0.0.1:6379> publish redischat "redis is nosql db"
(integer) 1

golang驱动

推荐go-redis和redigo库,其中edgex使用了redigo库,go-redis封装好。

github.com/go-redis/redis/v8

github.com/gomodule/redigo/redis

// main.go
package main

import (
	_ "fmt"
	"log"
	"time"

	"testredis/gredis"
)

const RNETWORK = "tcp"
const RPASSWD = "123456"
const RADDRESS = "172.61.1.240:6379"
const RKEY = "wstring"

func main() {
	cli, err := gredis.NewClient(RNETWORK, RADDRESS, RPASSWD)
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()
	log.Println("Client create...")

	if _, err = cli.Exists(RKEY); err != nil {
		log.Fatal(err)
	}

	{
		log.Println(RKEY + " exists")
		data, err := cli.Get(RKEY)
		if err != nil {
			log.Println(err)
		} else {
			log.Println("old data: ", data)
		}
	}

	cli.Delete(RKEY)
	cli.Set(RKEY, "CHINA", 3600)
	data, _ := cli.Get(RKEY)
	log.Println("new data: ", data)

	time.Sleep(1 * time.Second)
}

// gredis/redis.go
package gredis

import (
	_ "encoding/json"
	"errors"
	"log"
	"sync"
	"time"

	"github.com/gomodule/redigo/redis"
)

var once sync.Once

type Client struct {
	Pool *redis.Pool
}

func NewClient(network, address, passwd string) (*Client, error) {
	var redisClient Client
	once.Do(func() {
		redisClient = Client{
			Pool: &redis.Pool{
				MaxIdle:     10, // Maximum number of idle connections in the pool
				MaxActive:   10, // Maximum number of connections allocated by the poll at a given time.
				IdleTimeout: 10 * time.Second, // close connection
				Dial: func() (redis.Conn, error) {
					c, err := redis.Dial(network, address)
					if err != nil {
						return nil, err
					}
					if passwd != "" {
						if _, err := c.Do("AUTH", passwd); err != nil {
							c.Close()
							return nil, err
						}
					}
					return c, nil
				},
				TestOnBorrow: func(c redis.Conn, t time.Time) error {
					_, err := c.Do("PING")
					return err
				},
			},
		}
	})

	return &redisClient, nil
}

func (c *Client) Set(key string, data interface{}, time int) (err error) {
	conn := c.Pool.Get()
	defer conn.Close()

	//	value, err := json.Marshal(data)
	value, ok := data.(string)
	if !ok {
		return errors.New("Set No string")
	}

	_, err = conn.Do("SET", key, value)
	if err != nil {
		return err
	}

	_, err = conn.Do("EXPIRE", key, time)
	if err != nil {
		return err
	}

	return nil
}

func (c *Client) Exists(key string) (bool, error) {
	conn := c.Pool.Get()
	defer conn.Close()

	exists, err := redis.Bool(conn.Do("EXISTS", key))
	if err != nil {
		log.Println(err)
		return false, err
	}

	return exists, nil
}

func (c *Client) Get(key string) (string, error) {
	conn := c.Pool.Get()
	defer conn.Close()

	reply, err := redis.Bytes(conn.Do("GET", key))
	if err != nil {
		return "", err
	}

	return string(reply), nil
}

func (c *Client) Delete(key string) (bool, error) {
	conn := c.Pool.Get()
	defer conn.Close()

	return redis.Bool(conn.Do("DEL", key))
}

func (c *Client) LikeDeletes(key string) error {
	conn := c.Pool.Get()
	defer conn.Close()

	keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
	if err != nil {
		return err
	}

	for _, key := range keys {
		_, err = c.Delete(key)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c *Client) Close() {
	c.Pool.Close()
	once = sync.Once{}
}

问题

1. redis的过期策略以及内存淘汰机制

分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?

回答:redis采用的是定期删除+惰性删除策略。

为什么不用定时删除策略?

定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除+惰性删除是如何工作的呢?

定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?

不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

在redis.conf中有一行配置

# maxmemory-policy allkeys-lru

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)

1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。

2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用。

3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。

4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐

5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐

6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐

ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

2. redis和数据库双写一致性问题

分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

回答:首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。


参考:

1 Redis 教程 runoob

2 一文看懂redis

3 redis全面解析

4 用 Go 来了解一下 Redis 通讯协议 煎鱼

5 golang中使用redis 简书 推荐go-redis和redigo库

6.Redis持久化和备份数据

7.redis数据结构 简书

原文地址:https://www.cnblogs.com/embedded-linux/p/13335992.html