# 以指定的配置文件启动redis服务
# 这里的/etc/redis-conf是在原始的redis-conf上cp过来的,
# 修改后在已改文件运行,不会破坏原有的那个文件
redis-server /etc/redis-conf
#Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
# 查看所得键
keys *
# 查看具体某一个键是否存在
exists keyname
# 查看键的类型
type keyname
# 删除指定key数据
del keyname
# 根据value选择非阻塞删除[仅将key从keyspace元数据中删除,真正的删除会在后续的异步操作]
unlink keyname
# 为key设置过期时间
expire keyname 10
# 查看key还有多少秒过期,-1表示永不过期,-2表示已经过期
ttl keyname
# 切换库[redis中默认有16个库,一般默认使用0号库]
select databaseNum
# 查看当前数据库key的数量
dbsize
# 清空当前库
flushdb
# 通杀全部库
flushall
简介
常用命令
# 当key不存在时候,可以往数据库中添加一对key-value
# 如果key存在,相当于修改了对应key的value【覆盖】
set <key><value>
# 如果想要实现key不存在的时候才设置成功,也就是不覆盖设置
setnx <key><vale>
# 用新值替换旧值,并返回旧值
getset <key><value>
get <key>
# 相当于在对应key的value后面追加新的内容,返回追加后的长度
append <key><value>
strlen <key>
# value值必须为数值型
# keyname对应的value值增加1,如果原来空,则值变为1。返回加后的值
incr <key>
# 减同理
decr <key>
# keyname对应value增加increasement
incrby <key><increasement>
# keyname对应value减少decreasement
decrby <key><decreasement>
mset <key1><value1><key2><value2>....
msetnx <key1><value1><key2><value2>....
mget <key1><key2><key3>....
# 获取值的范围,类似java中substring,前包,后包
getrange <key><起始位置><结束位置>
# 有一点抽线看下图中的展示
setrange <key><起始位置><value>
# 例子:
127.0.0.1:6379> get name
"lucytom"
127.0.0.1:6379> setrange name 3 abc
(integer) 7
127.0.0.1:6379> get name
"lucabcm"
setex <key><过期时间><value>
# 从左边/右边插入一个或多个值
lpush/rpush <key><value1><value2>...
# 值在键在,值亡人亡
# 从左边/右边吐出一个值
lpop/rpop <key>
# 从左到右
lrange <key><start><stop>
# 从左到右
lindex <key><index>
llen <key>
linsert <key> before <value><newvalue>
# (从左到右)
lrem <key><n><value>
lset <key><index><value
一个算法,随着数据的增加,执行时间的长度如果是O(1),数据增加,查找数据的时间不变
# 讲一个或多个member元素加入到集合key中,已经存在的元素将被忽略
sadd <key><value1><value1>
# 取出集合中的所有值
smembers <key>
# 从key对应的集合中依次进行删除
srem <key><value1><value2>...
# 把集合中一个值从一个集合移动到另一个集合
smove <source><destination><value>
# 判断key对应的集合中是否有value,有1,没有0
sismember <key><value>
scard <key>
# 随机从该集合中吐出count个值
spop <key><count>
# 随机从该集合中取出n个值,不会从集合中删除
srandmember <key><count>
# 例子
127.0.0.1:6379> smembers name1
1) "jack"
2) "xiaogang"
127.0.0.1:6379> smembers name2
1) "tompsen"
2) "kevin"
3) "ross"
127.0.0.1:6379> smove name2 name1 kevin
(integer) 1
127.0.0.1:6379> smembers name1
1) "jack"
2) "xiaogang"
3) "kevin"
sinter <key1><key2>
sunion <key1><key2>
# key1中的不包含key2中的
sdiff <key1><key2>
简介
常用命令
hset <key><filed><value>
# 当然也可以使用hsetnx
hsetnx <key><filed><value>
hget <key><filed>
hmset ...
# 存在返回1,不存在返回0
hexits <key><filed>
hkeys <key>
hvals <key>
# 前提是filed必须是数值型
hincrby <key><filed><increment>
数据结构
Hash类型对应的数据结构两种:ziplist(压缩列表),HashTable(哈希表)。当filed-value长度比较短且个数较少时,使用ziplist,否则使用hashtable。
# 将一个或多个member元素及其score值加入到有序集key当中
zadd <key><score1><value1><score2><value2>
# 返回有序集合key中,下标在之间的元素
# 带WITHSCORES,可以让分数和值一起返回到结果集
zrange <key><start><stop> [WITHSCORES]
# 返回min和max之间的结果集
zrangebyscore <key><min><max>
zrevrangebyscore <key><min><max>
zincrby <key><increment><member>
zrem <key><member>
zcount <key><min><max>
zrank <key><member>
# 注释了来允许远程连接
bind 127.0.0.1 -::1
# 关闭保护模型
protected-mode yes
redis配置文件涉及到非常多的功能,在这里先不具体得去了解细节。等日后有需要再去具体的了解即可。
SUBSCRIBE <channel...>
publish <channel><message>
简介
Redis提供了Bitmaps这个“数据类型”,可以实现对位的操作:
常用命令
setbit <key><offset><value>
1.很多应用的用户id以一个指定的数字(例如10000)开头,直接将用户的id和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次setbit操作时将用户id减去一个指定的数字。
2.在第一次初始化Bitmaps时候,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞。
# 获取键的第offset位的值(从0开始算)
getbit <key><offset>
# 统计个位置上值为1的个数
bitcount <key>
# 在上面的例子基础之上
bitcount <key><start><end>
# operation可以是and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中
bitop <operation><destkey><key...>
Bitmap和Set的对比
在针对活跃用户存储方面,Bitmap在内存和查询时间上天生具有非常大的优势。但是在活跃用户较少或者僵尸用户较多的时候,使用该类型来保存非产生非常大的冗余。
# 添加指定元素到HyperLogLog中
pfadd <key><element...>
pfcount <key>
# 将多个key进行融合
pfmerge <key...>
# 添加地理位置(经度、纬度、名称)
# 有效的精度从-180度到180。有效的纬度从-85.05度到85.05度
# 当坐标位置超出指定位置时候,该命令将返回一个错误
# 已经添加的错误是无法再次往里面添加的
geoadd <key><longtitude><latitude><member>...
# 获取指定地区的坐标值
geopos <key><member>
# 以给定的经纬度为中心,找出某一半径内的元素
georadius <key><longitude><latitude>radius m|km|ft|mi
本文使用go-redis作为连接工具
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
// Background返回一个非空的Context。 它永远不会被取消,没有值,也没有期限。
// 它通常在main函数,初始化和测试时使用,并用作传入请求的顶级上下文。
var ctx = context.Background()
var DB *redis.Client
func main() {
rdb := redis.NewClient(&redis.Options{
// 需要修改成你的配置,本地无需修改
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
_, err := rdb.Ping(ctx).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("连接成功")
// 成功连接将其赋值给全局变量
DB = rdb
}
func OperateString() {
// 测试添加
res1, err := DB.Set(ctx, "name", "kevin", time.Minute).Result()
if err != nil {
fmt.Println(err)
}
fmt.Printf("插入成功:%s\n", res1)
// 同时多行插入
res2, err := DB.MSet(ctx, "car", "BMW", "flower", "ross").Result()
if err != nil {
fmt.Println(err)
}
fmt.Printf("插入多行成功:%s\n", res2)
// 获取
res3, err := DB.Get(ctx, "name").Result()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("key=name value=%s\n", res3)
// 追加
res4, err := DB.Append(ctx, "car", "benz").Result()
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("追加成功:%d", res4)
// 获取长度
res5, err := DB.StrLen(ctx, "car").Result()
if err != nil {
fmt.Println(err)
}
fmt.Printf("长度为:%d\n", res5)
// value值增加1
DB.MSet(ctx, "age", 15).Result()
res6, err := DB.Incr(ctx, "age").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res6)
// 按照指定的值增加
res7, err := DB.IncrBy(ctx, "age", 4).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("增长后的age值:", res7)
// 一次获取多对值
res8, err := DB.MGet(ctx, "age", "car", "flower").Result()
if err != nil {
fmt.Println(err)
}
for _, v := range res8 {
fmt.Println(v)
}
// 从指定的位置获取值
res9, err := DB.GetRange(ctx, "car", 0, 1).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("值为:", res9)
// 设置键的过期时间
res10, err := DB.SetEX(ctx, "animal", "bird", 30*time.Minute).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res10)
}
func OperateList() {
// 添加
DB.LPush(ctx, "name", "marry")
DB.LPush(ctx, "name", "tom")
DB.LPush(ctx, "name", "jack")
// 查看
res2, err := DB.LRange(ctx, "mame", 0, -1).Result()
if err != nil {
fmt.Println(err)
}
for _, v := range res2 {
fmt.Println(v)
}
// 导出一个
// 获取列表长度
length, err := DB.LLen(ctx, "name").Uint64()
fmt.Println("长度为:", length)
res3, err := DB.LPop(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Printf("pop的结果是:%s\n", res3)
length, err = DB.LLen(ctx, "name").Uint64()
fmt.Println("长度为:", length)
//从左边删除一个
n, err := DB.LRem(ctx, "name", 1, "jack").Result()
if err != nil {
fmt.Println(err)
}
fmt.Printf("删除了%d\n", n)
// 将下标为index的value改为指定的值
res4, err := DB.LSet(ctx, "name", 1, "ross").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("改值后", res4)
}
func OperateSet() {
// 添加
DB.SAdd(ctx, "name", "xh")
DB.SAdd(ctx, "name", "ross")
DB.SAdd(ctx, "name", "kevin")
DB.SAdd(ctx, "name", "jack")
DB.SAdd(ctx, "name", "scoot")
DB.SAdd(ctx, "car", "scoot")
DB.SAdd(ctx, "car", "ross")
DB.SAdd(ctx, "car", "dz")
// 获取
res1, err := DB.SMembers(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
for _, v := range res1 {
fmt.Println(v)
}
// 删除
res2, err := DB.SRem(ctx, "name", "xh").Result()
if err != nil {
fmt.Println(err)
}
fmt.Printf("删除%d\n个", res2)
// 判断集合中是否有该值
res3, err := DB.SIsMember(ctx, "name", "kevin").Result()
if err != nil {
fmt.Println(err)
}
if res3 {
fmt.Println("该集合中有kevin")
}
// 获取集合元素的个数
res4, err := DB.SCard(ctx, "name").Uint64()
if err != nil {
fmt.Println(err)
}
fmt.Printf("此时集合中有%d个\n", res4)
// 随机吐出1个
res5, err := DB.SPop(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("随机突出的一个元素是:", res5)
// 随机吐出n个
res6, err := DB.SRandMemberN(ctx, "name", 3).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res6)
// 返回两个集合的交集
res7, err := DB.SInter(ctx, "name", "car").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res7)
// 返回两个集合的并集
res8, err := DB.SUnion(ctx, "name", "car").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res8)
// 返回两个集合的差集【name中有,而car中没有】
res9, err := DB.SDiff(ctx, "name", "car").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res9)
}
func OperateHash() {
// 添加
DB.HSet(ctx, "user001", "name", "kevin")
DB.HSet(ctx, "user001", "age", 15)
DB.HSet(ctx, "user001", "year", 1999)
DB.HSet(ctx, "user001", "country", "China")
DB.HSet(ctx, "user002", "name", "ross")
DB.HSet(ctx, "user002", "age", 17)
DB.HSet(ctx, "user002", "year", 2000)
DB.HSet(ctx, "user002", "country", "American")
// 获取指定filed
name002, err := DB.HGet(ctx, "user002", "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("user002_name=", name002)
// 批量设置
res1, err := DB.HMSet(ctx, "user003", "name", "zhangsan", "age", 12).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res1)
// 判断key是否存在
res2, err := DB.HExists(ctx, "user002", "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res2)
// 查看该hash集合的所有value
res3, err := DB.HVals(ctx, "user001").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res3)
// 在指定的值加上增量1
res4, err := DB.HIncrBy(ctx, "user001", "age", 1).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res4)
}
func OperateZset() {
// 添加
DB.ZAdd(ctx, "name", &redis.Z{
Score: 2,
Member: "kevin",
})
DB.ZAdd(ctx, "name", &redis.Z{
Score: 3,
Member: "jack",
})
DB.ZAdd(ctx, "name", &redis.Z{
Score: 1,
Member: "ross",
})
DB.ZAdd(ctx, "name", &redis.Z{
Score: 5,
Member: "tom",
})
//获取
res1, err := DB.ZRange(ctx, "name", 1, 3).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res1)
//返回指定分数之间结果集
res2, err := DB.ZRangeByScore(ctx, "name", &redis.ZRangeBy{
Min: "0",
Max: "3",
}).Result()
fmt.Println(res2)
//删除某一个元素
res3, err := DB.ZRem(ctx, "name", "jack").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res3)
// 返回集合中的排名
res4, err := DB.ZRank(ctx, "name", "tom").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res4)
}
func OperateDB() {
// 查看所有的键
res, err := DB.Keys(ctx, "*").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(res)
// 查看某一个键是否存在
n, err := DB.Exists(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(n)
// 查看键的类型
str, err := DB.Type(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(str)
// 为键设置过期时间
success, err := DB.Expire(ctx, "name", 5*time.Minute).Result()
if err != nil {
fmt.Println(err)
}
if success {
fmt.Println("设置时间成功")
}
//查看键的过期时间
t, err := DB.TTL(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(t)
time.Sleep(5 * time.Second)
t, err = DB.TTL(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println(t)
//更换数据库
n, err = DB.DBSize(ctx).Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("数据库的大小为", n)
//删除键
n, err = DB.Del(ctx, "name").Result()
if err != nil {
fmt.Println(err)
}
fmt.Println("删除成功")
//删除库中所有的数据
DB.FlushDB(ctx).Result()
}
从输入Multi命令开始,输入的命令都会依次进入命令队列当中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
演示1如下
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set user011 dz
QUEUED
127.0.0.1:6379(TX)> set user012 zs
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
演示2如下
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a1 a
QUEUED
127.0.0.1:6379(TX)> set a2 b
QUEUED
127.0.0.1:6379(TX)> discard
OK
注意,,如果组队中出现了报告错误,执行时整个的所有队列都会取消(如例子1)。如果组队中没有报错,而是逻辑错误,不会取消整个队列,只有执行到那个命令的时候才出错,其余地方都正常执行,见下面这个例子。
# 例子1
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name1 kevin
QUEUED
127.0.0.1:6379(TX)> set name2 ross
QUEUED
127.0.0.1:6379(TX)> set name3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
# 例子2
127.0.0.1:6379> set name dz
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set name1 kevin
QUEUED
127.0.0.1:6379(TX)> incr name
QUEUED
127.0.0.1:6379(TX)> set name2 ross
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
WATCH key [key...]
unwatch key [key...]
连接池的配置详细见csdn博客
func MsCode(uuid, prodid string) bool {
// 1、对uuid和prodid进行非空判断
if uuid == "" || prodid == "" {
return false
}
//2、获取连接
rdb := DB
//3、拼接key
kcKey := "kc:" + prodid + ":qt"
userKey := "sk:" + prodid + ":user"
//4、获取库存
str, err := rdb.Get(ctx, kcKey).Result()
if err != nil {
fmt.Println(err)
fmt.Println("秒杀还未开始.......")
return false
}
// 5、判断用户是否重复秒杀操作
flag, err := rdb.SIsMember(ctx, userKey, userKey).Result()
if err != nil {
fmt.Println(err)
}
if flag {
fmt.Println("你已经参加了秒杀,无法再次参加。。。。")
return false
}
// 6、判断商品数量,如果库存数量小于1,秒杀结束
str, err = rdb.Get(ctx, kcKey).Result()
if err != nil {
fmt.Println(err)
}
n, err := strconv.Atoi(str)
if err != nil {
fmt.Println(err)
}
if n < 1 {
fmt.Println("秒杀结束,请下次再来吧。。。。")
return false
}
// 7、秒杀过程
// 7.1、库存减1
num, err := rdb.Decr(ctx, kcKey).Result()
if err != nil {
fmt.Println(err)
}
if num != 0 {
// 7.2、添加用户
rdb.SAdd(ctx, userKey, uuid)
}
return true
}
版本1会有一个问题,就是如果接受并发抢购问题,会造成多卖的情况。为了解决这个情况,我们需要监视库存,然后开启事务处理,代码如下:
err = rdb.Watch(ctx, func(tx *redis.Tx) error {
n, err := tx.Get(ctx, kcKey).Int()
if err != nil && err != redis.Nil {
return err
}
if n <= 0 {
return fmt.Errorf("抢购结束了!请下次早点来。。。。")
}
_, err = tx.TxPipelined(ctx, func(pipeliner redis.Pipeliner) error {
err := pipeliner.Decr(ctx, kcKey).Err()
if err != nil {
return err
}
err = pipeliner.SAdd(ctx, userKey, uuid).Err()
if err != nil {
return err
}
return nil
})
return err
}, kcKey)
func useLua(userid, prodid string) bool {
//编写脚本 - 检查数值,是否够用,够用再减,否则返回减掉后的结果
var luaScript = redis.NewScript(`
local userid=KEYS[1];
local prodid=KEYS[2];
local qtKey="sk:"..prodid..":qt";
local userKey="sk:"..prodid..":user";
local userExists=redis.call("sismember",userKey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num=redis.call("get",qtKey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtKey);
redis.call("SAdd",userKey,userid);
end
return 1; `)
//执行脚本
n, err := luaScript.Run(ctx, DB, []string{userid, prodid}).Result()
if err != nil {
return false
}
switch n {
case int64(0):
fmt.Println("抢购结束")
return false
case int64(1):
fmt.Println(userid, ":抢购成功")
return true
case int64(2):
fmt.Println(userid, ":已经抢购了")
return false
default:
fmt.Println("发生未知错误!")
return false
}
return true
}
通过下面的程序进行并发请求:
func main() {
// 并发的版本
for i := 0; i < 20; i++ {
go func() {
uuid := GenerateUUID()
prodid := "1023"
time.Sleep(10 * time.Second)
useLua(uuid, prodid)
}()
}
time.Sleep(15 * time.Second)
写时复制技术:指的是先将数据写入到一个文件中,然后需要写入数据的时候直接用新的文件将原始的文件覆盖。
什么是AOF?
以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容讲写指令从前到后执行一次以完成数据的恢复工作。
AOF持久化流程
AOF文件默认不开启
AOF和RDB同时开启,redis听谁的?
AOF和RDB同时开启,系统默认取AOF的数据(数据不存在丢失)
AOF启动/修复/恢复
AOF同步频率设置
ReWrite重写
优势
劣势
官方推荐两个都启用。如果对数据不敏感,可以选单独用RDB。不建议单独用AOF,因为可能会出现Bug。如果只是单纯用内存缓存,可以都不用。
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,master以写为主,Slave以读为主。
创建文件
复制redis.conf
配置一主两从,创建三个配置文件:redis6379.conf、redis6380.conf、redis6381.conf
在三个配置文件写入内容
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
修改appendonly为no
分别以指定的文件启动
# 使用本命令查看当前进程的情况
ps aux | grep redis
查看三台主机运行情况
info replication
配置主从关系
slaveof 主机ip 端口号
同样将6379添加为6381的从服务器,最终搭建好的一个效果:
# 在master上添加一个key
127.0.0.1:6381> set name kevin
OK
127.0.0.1:6381>
# 在slave上获取key,注意slave不能设置key
127.0.0.1:6379> keys *
1) "name"
127.0.0.1:6379> get name
"kevin"
127.0.0.1:6379>
127.0.0.1:6380> keys *
1) "name"
127.0.0.1:6380> get name
"kevin"
127.0.0.1:6380>
这种架构简单来说就是将slaver1作为slaver2的master,这样以来就直接串连起来。但注意,如果slaver1出现问题,slaver2就无法进行消息的复制。
如果master挂掉,那么slaver就会变成master【手动】
slaveof no one
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6381
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:4817
127.0.0.1:6380> slaveof no one
OK
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:ffc0b52b77203b169b1e8bb02a0f28463fe6ee46
master_replid2:8cea75e4638b6a2cd625c2ff5c64c69e7545714c
新建一个sentinel.conf文件(文件名绝对不能错)
配置哨兵模式
# mymaster为监控对象起的服务器名称,1为至少有多少个哨兵同意迁移的数量
sentinel monitor mymaster 127.0.0.1 6379 1
启动哨兵
redis-sentinel sentinel.conf
当主机挂掉,从机选举中产生新的主机
(大概10秒钟左右会看到哨兵窗口日志,切换了新的主机)
哪个主机会被选举为主机呢?根据优先级别:slave-priority
原主机重启就会变成从机,下面就是master(6381)出错,然后slaver(6379)被哨兵选作为新的master,然后将(6380)作为(6379)的slaver。
127.0.0.1:6379> info replication
# 带等一会就好了
Error: Broken pipe
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=20290,lag=0
master_failover_state:no-failover
127.0.0.1:6380> info replication
# 出现这个错误等一会儿就好了
Error: Broken pipe
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使得这个问题更加严重。
将rdb、aof文件都删除掉
这6个实例的端口分别是:6379、6380、6381、6389、6390、6391
配置基本信息
redis cluster配置修改
cluster-enabled yes 打卡集群模式
cluster-config-file nodes-6379.conf 设置节点配置文件名
cluster-node-timeout 15000 设定节点关联时间,超过该时间(毫秒),集群自动进行主从切换
配置文件的大致信息如下:
include /home/bigdata/redis.conf
port 6379
pidfile /var/run/redis_6379.pid
dbfile dump6379.rdb
dir /home/bigdata/redis_cluster
logfile /home/bigdata/redis_cluster/redis_err_6379.log
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
在vim中的一个操作技巧:将指定的部分替换可以使用以下命令:%s/6379/6389
最终启动所有的服务
MacBook-Pro ~ % ps aux | grep redis
3844 0.0 0.0 408940096 2016 s002 S+ 3:09下午 0:00.25 redis-server 127.0.0.1:6389 [cluster]
3815 0.0 0.0 408790592 2016 ?? S 3:08下午 0:00.31 redis-server 127.0.0.1:6380 [cluster]
3746 0.0 0.0 408781376 2000 ?? S 3:06下午 0:00.39 redis-server 127.0.0.1:6379 [cluster]
4002 0.0 0.0 408626880 1296 s004 S+ 3:14下午 0:00.00 grep redis
4000 0.0 0.1 408930880 4672 s001 S+ 3:14下午 0:00.01 redis-server 127.0.0.1:6391 [cluster]
3970 0.0 0.1 408930880 4704 s000 S+ 3:14下午 0:00.03 redis-server 127.0.0.1:6390 [cluster]
3938 0.0 0.1 408790592 4384 s003 S+ 3:13下午 0:00.06 redis-server 127.0.0.1:6381 [cluster]
合体
cd /usr/local/bin/
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6389 127.0.0.1:6390 127.0.0.1:6391
此命令中,尽量不要用127.0.0.1,要用真实的IP地址。但是如果你的真实ip无法找到,报错connect refuse则可以再尝试使用127.0.0.1
--replices 1采用最简单的方式配置集群,一台主机,一台从机,正好三组
查看节点信息
# 集群中任意一个端口都可以进去
redis-cli -c -p 6379
# 查看集群信息
127.0.0.1:6379> cluster nodes
b92fb808235845755a5bf7b0efb4d243732641cd 127.0.0.1:6391@16391 slave f3d761edcb54bc8c4315748e4a7766a3c52ef791 0 1669621253796 3 connected
724d45a2f2afb7c4384d0f2a2dc8ebf4e90a9b19 127.0.0.1:6389@16389 slave b0294ee276b22b7d11ae0c404d55b86d7efe104a 0 1669621252785 1 connected
03f22e7b1c9c6f14aa4497369ffb92d3f6dab5d1 127.0.0.1:6390@16390 slave 6468a7f8491a2b4c4c753dcff4296ce2ed343287 0 1669621252000 2 connected
6468a7f8491a2b4c4c753dcff4296ce2ed343287 127.0.0.1:6380@16380 master - 0 1669621252000 2 connected 5461-10922
b0294ee276b22b7d11ae0c404d55b86d7efe104a 127.0.0.1:6379@16379 myself,master - 0 1669621251000 1 connected 0-5460
f3d761edcb54bc8c4315748e4a7766a3c52ef791 127.0.0.1:6381@16381 master - 0 1669621253000 3 connected 10923-16383
一结点至少要有三个主结点。选项–cluster-replicas 1表示我们希望为集群中的每个主结点创建一个从结点。分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
一个Redis集群包含16384个插槽(hash slot),数据库中的每个键都属于这个16384个插槽的其中一个,集群使用公式CRC16(key)%16384来计算键key属于哪个槽,其中CRC16(key)语句用于计算键key和CRC16校验和。集群中的每个节点负责处理一部分插槽。举个例子,如果一个集群可以有主结点,其中节点A负责0号至5460号插槽。
这个地方的一个简单理解就是将key能够平均得分配到集群当中去。下面11.7的实际案列能够更加清晰得理解。
127.0.0.1:6379> set name kevin
-> Redirected to slot [5798] located at 127.0.0.1:6380
OK
127.0.0.1:6380> mset name{user} tom age{user} 10
OK
# 计算name键对应的slot值
127.0.0.1:6380> cluster keyslot name
(integer) 5798
# 计算5798中有多少个值
127.0.0.1:6380> cluster countkeysinslot 5798
(integer) 1
127.0.0.1:6380> cluster getkeysinslot 5798 1
1) "name"
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?这个取决于自身的配置。如果某一段插槽的主从都挂掉,cluster-require-full-coverage为yes,那么,整个集群都挂掉。如果cluster-require-full-coverage为no那么该插槽数据全部不能使用,也无法存储。redis.conf中的参数cluster-require-full-coverage
这里,我们就基于redis实现分布式锁
redis:命令
# EX second:设置键的过期时间为second秒。
# SET key value EX second 效果等同于SETEX key second value
setnx <key><value>
del <key>
setnx <key><value>
# 到期后自动释放
expire <key> <time>
# EX second:设置键的过期时间为second秒。
# SET key value EX second 效果等同于SETEX key second value
# 上锁的同时设置过期时间
set <key><value> NX EX 10000
自己只能删除自己的锁,不能删除别人的锁。
简单来说,就是一个有一个服务器在要删除但是还没有删除锁的时候,锁到期了,自动释放了,然后另一个服务器又拿到这个锁开始操作,因此丧失了原子性。同时为了保证分布式锁可用性,需要满足一下四个条件:
io-thread-do-reads yes
io-threads 4
之前老版Redis想要搭建集群需要单独安装ruby环境,Redis 5将redis-trib.rb的功能集成到redis-cli。另外官方redis-benchmark工具开始支持cluster模式了,通过多线程的方式对多个分布进行压测。