redis

194 阅读21分钟

Redis 介绍

redis 数据库:

Redis,即Remote Dictionary Server,是一个非关系型的key-value存储系统,已被广泛应用于记录型NoSQL、缓存、队列、消息系统等众多领域。

用Redis,首先需要创建一个Redis数据库,以便存储数据。

  • 创建数据库的命令为“CREATE:DATABASE”
  • 创建数据库的代码示例:
// 连接Redis

$redis = new Redis();
$redis->connect(‘localhost’, 6379);
// 创建数据库
$redis->createDb(‘newDb’);
// 切换数据库
$redis->selectDb(‘newDb’);

  • 查看当前操作数据库 “SELECT:DATABASE”
  • 删除已存在的数据库,则可以使用“DROP:DATABASE”命令。

Redis Key 定义规范,:冒号分割

public interface Constant {

    /**
     * 知识库链接的 Redis 的 key   knowledge:link:key:文档Id:文档"
     */
    String KNOWLEDGE_LINK_KEY = "knowledge:link:key:%s";
}


/**
 * 获取随机key 用于存储redis 使用 文档id 作为层级,方便以后 对该key操作,列入 取消分享
 * @return
 */
public static String getKnowledgeLinkKey(String key){
    return String.format(Constant.KNOWLEDGE_LINK_KEY,key);
}

redis-cli monitor

统计上亿用户的在线状态解决方案:

在线状态 上线、离线 、隐身都会触发数据库变更操作,上亿用户的修改操作会增加数据库IO压力,高频语句会拖垮整个数据库的性能。 解决方案: redis 新建key (login-user) 保存登录用户的UId(登录set,登出remove), 通过scard 统计数量,确定比较消耗内存。

- 一个用户4个字节,一亿个字节=0.91G=1G , 1亿用户(18bit 雪花算法) 18G

- 内存方面优化bitmap(统计字段要少) 查询去重效率很高:
        
        setbit login-status u123456 1  记录登录;
        setbit login-status u123456 0 记录登出;
        统计bigcount login-status,一个用户一个bit 
        
        
        

使用redis基础方法:

新增:先放入数据库,再放入缓存。

更新: 先删缓存,再更新数据库。

查询:请求获取缓存时,存在,直接返回,若发现没有,从db获取 ,再重新赋值到缓存,这是最典型的缓存 操作;

删除:使用expire命令

Redis 实际用法

Redis 存储 全量失败记录 ,追加记录

String allNos = redisHelper.getValue(key, redisDbName);
if (StringUtils.isNotBlank(allNos)) {
    redisHelper.setValue(key, String.format("%s,%s", allNos,newNo), 86400000, redisDbName);
} else {
    redisHelper.setValue(key, newNo, 3600*24*30, redisDbName);
}

redis 数据结构选择

Redis 提供了多种数据结构,如字符串、哈希、列表、集合、有序集合等。在使用Redis时,需要根据实际需求选择正确的数据结构,以达到最优的性能和空间利用率。

合理设置数据过期时间

在缓存场景下,我们需要设置数据的过期时间,以便及时清理过期的数据,避免内存的浪费。

Redis 的管道和批量操作 ,redis性能提升

Redis 的管道和批量操作 ,redis性能提升可以大大提高Redis的性能。通过管道和批量操作,可以将多个Redis命令打包成一个请求,减少网络请求的次数,提高Redis的性能。

合理使用缓存预热,redis性能提升

缓存预热可以在系统启动时,将热点数据预先加载到Redis中,以提高系统的响应速度。

但是,在使用缓存预热时,需要注意热点数据的选择,避免将不必要的数据预热到Redis中,增加Redis的负担。

防止redis 缓存被击穿, redis 服务稳定性

为了避免缓存击穿,可以使用互斥锁等机制,在查询缓存的同时,防止多个请求同时查询数据库。

配置Redis的最大内存限制

由于Redis是一个内存数据库,因此,需要合理配置Redis的最大内存限制,以免因内存不足而导致Redis的性能下降或者数据丢失的问题。

使用Redis事务

Redis 的事务可以将多个Redis命令打包成一个事务,保证多个命令的原子性。

在使用Redis事务时,需要注意事务的并发问题,以及事务的回滚机制

redis Key 示例


import java.util.Random;

/**
 * 常亮处理类
 *
 * @author zizhen
 * @date 2023-08-17 13:56
 */

public class ConstantUtil {


    /**
     * 获取随机key 用于存储redis 使用 报表id
     *
     * @return
     */
    public static String getReportLinkKey(String key) {
        return String.format(Constant.SETTLE_REPORT_LINK_KEY, key);
    }


    public static String getShareUrl(String reportId, String pwd) {
        String key = DigestUtil.md5Hex(
                new StringBuilder().append(reportId)
                        .append(System.currentTimeMillis())
                        .append(RandomUtil.randomInt(1000, 9999)) + "");

        // 分享链接参数:security=是否加密&key=分享key
        return buildShareLink(pwd, key);

    }

    public static String buildShareLink(String pwd, String key) {
        return new StringBuilder()
                .append("security=").append(StringUtils.isNotBlank(pwd))
                .append("&key=").append(key)
                .toString();
    }

    public static void main(String[] args) {
        System.out.println(getShareUrl("230909012222333","123456"));
    }
}

实际应用场景:车辆行程点位信息上传及处理


public Long lpush(String redisName, String key, String... values) throws Exception {
    if (redisName != null && !"".equals(redisName)) {
        return this.redisBusinessImpl.lpush(redisName, (Date)null, key, values);
    } else {
        throw new Exception("There is no available redisDBName");
    }
}

public Long rpush(String redisName, String key, String... values) throws Exception {
    if (redisName != null && !"".equals(redisName)) {
        return this.redisBusinessImpl.rpush(redisName, (Date)null, key, values);
    } else {
        throw new Exception("There is no available redisDBName");
    }
}

public String lpop(String redisName, String key) throws Exception {
    if (redisName != null && !"".equals(redisName)) {
        return this.redisBusinessImpl.lpop(redisName, (Date)null, key);
    } else {
        throw new Exception("There is no available redisDBName");
    }
}

public String rpop(String redisName, String key) throws Exception {
    if (redisName != null && !"".equals(redisName)) {
        return this.redisBusinessImpl.rpop(redisName, (Date)null, key);
    } else {
        throw new Exception("There is no available redisDBName");
    }
}

public List<String> lrange(String redisDBNameParam, String key, int start, int end) throws Exception {
    if (redisDBNameParam != null && !"".equals(redisDBNameParam)) {
        return this.redisBusinessImpl.lrange(redisDBNameParam, (Date)null, 0, key, start, end);
    } else {
        throw new Exception("There is no available redisDBName");
    }
}

public List<String> lgetAll(String redisDBNameParam, String key) throws Exception {
    if (redisDBNameParam != null && !"".equals(redisDBNameParam)) {
        return this.redisBusinessImpl.lrange(redisDBNameParam, (Date)null, 0, key, 0, -1);
    } else {
        throw new Exception("There is no available redisDBName");
    }
}

hashs 类型

添加商品信息

redis.hmset(‘product:1’, {‘name’: ‘Apple iPhone 12’, ‘price’: ‘7999’, ‘stock’: ‘1000’})

RedisHash批量设置命令 在Redis中,使用HSET命令可以设置一个Hash类型键的一个字段和值。但是当需要设置多个字段和对应的值时,单独使用HSET命令会比较繁琐。为了解决这个问 题,Redis提供了HMSET命令用于批量设置Hash的字段和值。 HMSET命令的语法如下: HMSET key field1 value1 [field2 value2 ...]

hmset Vs hset : 2万数据性能对比: OK耗时 86000:3972

@Override
public void hmset(final String key, final Map<String, String> hash) {
 final Map<byte[], byte[]> bhash = new HashMap<>(hash.size());
 for (final Entry<String, String> entry : hash.entrySet()) {
   bhash.put(SafeEncoder.encode(entry.getKey()), SafeEncoder.encode(entry.getValue()));
 }
 hmset(SafeEncoder.encode(key), bhash);
}

/**
 * 1:缓存员工权限
 *
 * @param responseVo
 * @param entClearingInfoDbVo
 * @param modifyStartTime
 */
private void cacheStaff2redis(CloudResponseVo<String> responseVo, EntClearingParamVo entClearingInfoDbVo, Date modifyStartTime, StringBuilder resultStr) {
    long time1 = System.currentTimeMillis();
    String redisKeyStaff = CacheConstant.STAFF_CACHE_KEY + entClearingInfoDbVo.gettMCNumber() + ":" + entClearingInfoDbVo.getEnterpriseNum();
    try {
        int pageIndex = 1;
        int pageSize = 1000;
        List<StaffSettingInfoBo> entClearingStaffRelaUpdateList = Lists.newArrayList();
        do {
            //分页查询员工数据
            StaffSettingInfoBo staffSettingInfoBo = new StaffSettingInfoBo();
            staffSettingInfoBo.setCompanyNumber(entClearingInfoDbVo.getEnterpriseNum());
            staffSettingInfoBo.setModifyTime(modifyStartTime);
            staffSettingInfoBo.setPageIndex(pageIndex++);
            staffSettingInfoBo.setPageSize(pageSize);
            //分页查询职位信息配置。
            entClearingStaffRelaUpdateList = staffSettingInfoDao.getUsefulStaffSettingInfos(staffSettingInfoBo);
            if (!CollectionUtils.isEmpty(entClearingStaffRelaUpdateList)) {
                resultStr.append("modifyStartTime:").append(modifyStartTime).append(";    ");
                resultStr.append("pageIndex:").append(staffSettingInfoBo.getPageIndex()).append(";    ");
                resultStr.append("***更新员工缓存条数:").append(entClearingStaffRelaUpdateList.size()).append("***;    ");
                try {
                    // 批量设置
                    Map<String, String> hashs = entClearingStaffRelaUpdateList.stream().collect(Collectors.toMap(StaffSettingInfoBo::getStaffID, staffBo -> GsonUtil.gson.toJson(staffBo)));
                    String redisValueStaff = redisHelper.hmset(redisDBName, redisKeyStaff, hashs);
                    //String staff = GsonUtil.gson.toJson(staffSettingInfo);
                    //Long redisValueStaff = redisHelper.hset(redisDBName, redisKeyStaff, staffSettingInfo.getStaffID(), staff);
                    Long l = redisHelper.expire(redisDBName, redisKeyStaff, 3600 * 24 * 15);
                    resultStr.append("redisValueStaff:").append(redisValueStaff);
                } catch (Exception e) {
                    LogUtil.error(e, "cacheStaff2redis");
                    resultStr.append("更新员工缓存异常:").append(e.getMessage());
                    responseVo.setSuccess(false);
                    responseVo.setMessage("exception:" + e.getMessage());
                }
            }
        } while (entClearingStaffRelaUpdateList.size() == pageSize && pageIndex < 200);

        //更新缓存:删除员工的redis
        int pageIndex1 = 1;
        List<StaffSettingInfoBo> entClearingStaffRelaDeleteList = Lists.newArrayList();
        if (modifyStartTime != null) {
            do {
                //分页查询员工数据
                StaffSettingInfoBo staffSettingInfoBo = new StaffSettingInfoBo();
                staffSettingInfoBo.setCompanyNumber(entClearingInfoDbVo.getEnterpriseNum());
                //删除时间必须是连续的,避免漏删除
                staffSettingInfoBo.setModifyTime(modifyStartTime);
                staffSettingInfoBo.setPageIndex(pageIndex1++);
                staffSettingInfoBo.setPageSize(pageSize);
                entClearingStaffRelaDeleteList = staffSettingInfoDao.getUnusefulStaffSettingInfos(staffSettingInfoBo);
                //修改时间为删除的删除缓存
                if (!CollectionUtils.isEmpty(entClearingStaffRelaDeleteList)) {
                    resultStr.append("删除员工缓存条数:").append(entClearingStaffRelaDeleteList.size());
                    Long delStaffCountResult = 0L;
                    for (StaffSettingInfoBo staffSettingInfo : entClearingStaffRelaDeleteList) {
                        Long delStaffCount = redisHelper.hdel(redisDBName, redisKeyStaff, staffSettingInfo.getStaffID());
                        delStaffCountResult += delStaffCount;
                    }
                    resultStr.append("delStaffCountResult:").append(delStaffCountResult);
                }
            } while (entClearingStaffRelaDeleteList.size() == pageSize && pageIndex < 200);
        }

    } catch (Exception e) {
        responseVo.setSuccess(false);
        responseVo.setMessage("exception:" + e.getMessage());
        LogUtil.error(e, "cacheStaff2redis");
        resultStr.append("更新员工缓存异常:").append(e.getMessage());
    } finally {
        LogRecord record = new LogRecord();
        record.setParam(GsonUtils.getGson().toJson(entClearingInfoDbVo));
        record.setKey1(redisKeyStaff);
        record.setTimePeriod(System.currentTimeMillis() - time1);
        record.setReturnValue(GsonUtils.getGson().toJson(responseVo));
        record.setMethod("cacheStaff2redis");
        record.setCompanyID(entClearingInfoDbVo.getEnterpriseNum());
        LogUtil.warn(record);
    }
    responseVo.setSuccess(true);
    responseVo.setMessage("redisValue:" + resultStr);

}

获取商品信息

product_info = redis.hgetall(‘product:1’)



127.0.0.1:6379> hset info name susu  #设置info name值
(integer) 1
127.0.0.1:6379> hgetall info  #获取info中所以的key和value
1) "name"
2) "susu"
127.0.0.1:6379> hget info name   #获取info 中name的值
"susu"
27.0.0.1:6379> hmset info2 k1 1 k2 2   #设置多个key-value
OK
127.0.0.1:6379> hkeys info2  #查看所有的key
1) "k1"
2) "k2"
127.0.0.1:6379> hgetall info2  #查看所有的key-value
1) "k1"
2) "1"
3) "k2"
4) "2"


127.0.0.1:6379> hexists info  EntClearingInfoCashe-20160512144219375328-20180321163431943594
(integer) 0
127.0.0.1:6379> hexists  EntClearingInfoCashe-20160512144219375328-20180321163431943594
(error) ERR wrong number of arguments for 'hexists' command
127.0.0.1:6379> hexists  EntClearingInfoCashe-20160512144219375328-20180321163431943594  zizhen001
(integer) 1
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> hadd  EntClearingInfoCashe-20160512144219375328-20180321163431943594 zizhen003 1111
(error) ERR unknown command 'hadd'
127.0.0.1:6379> hset  EntClearingInfoCashe-20160512144219375328-20180321163431943594 zizhen003 1111
(integer) 1
127.0.0.1:6379> hgetall  EntClearingInfoCashe-20160512144219375328-20180321163431943594
1) "zizhen001"
2) "{\"key\":\"value\"}"
3) "zizhen002"
4) "{\"key\":\"value\"}"
5) "zizhen003"
6) "1111"
127.0.0.1:6379> hdel  EntClearingInfoCashe-20160512144219375328-20180321163431943594 zizhen003
(integer) 1
127.0.0.1:6379> hgetall  EntClearingInfoCashe-20160512144219375328-20180321163431943594
1) "zizhen001"
2) "{\"key\":\"value\"}"
3) "zizhen002"
4) "{\"key\":\"value\"}"
127.0.0.1:6379>

127.0.0.1:6379> hincrby info k1 2
(integer) 3
127.0.0.1:6379> hgetall info
1) "k1"
2) "3"
3) "k2"
4) "2"
127.0.0.1:6379> hincrby info k1 2
(integer) 5
127.0.0.1:6379> hgetall info
1) "k1"
2) "5"
3) "k2"
4) "2"
127.0.0.1:6379> hincrbyfloat info k2 1.1415926
"3.1415926"
127.0.0.1:6379> hgetall info
1) "k1"
2) "5"
3) "k2"
4) "3.1415926"
127.0.0.1:6379>


127.0.0.1:6379> hset  entinfo zizhen001 '{"key":"value"}'
(integer) 1
127.0.0.1:6379> hset  entinfo zizhen002 '{"key":"value"}'
(integer) 1
127.0.0.1:6379> hget entinfo
(error) ERR wrong number of arguments for 'hget' command
127.0.0.1:6379> hgetall entinfo
1) "zizhen001"
2) "{\"key\":\"value\"}"
3) "zizhen002"
4) "{\"key\":\"value\"}"
127.0.0.1:6379> hset  EntClearingInfoCashe-20160512144219375328-20180321163431943594  zizhen001 '{"key":"value"}'
(integer) 1
127.0.0.1:6379> hset  EntClearingInfoCashe-20160512144219375328-20180321163431943594  zizhen002 '{"key":"value"}'
(integer) 1
127.0.0.1:6379> hgetall EntClearingInfoCashe-20160512144219375328-20180321163431943594
1) "zizhen001"
2) "{\"key\":\"value\"}"
3) "zizhen002"
4) "{\"key\":\"value\"}"
127.0.0.1:6379> hgetall EntClearingInfoCashe-20160512144219375328-20180321163431943594 zizhen001
(error) ERR wrong number of arguments for 'hgetall' command
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> hget EntClearingInfoCashe-20160512144219375328-20180321163431943594 zizhen001
"{\"key\":\"value\"}"
127.0.0.1:6379>



List 类型 数据类型应用场景

集合 set 数据类型的应用场景

(1)好友/关注/粉丝/感兴趣的人集合

set 类型唯一的特点使得其适合用于存储好友/关注/粉丝/感兴趣的人集合,集合中的元素数量可能很多,每次全部取出来成本不小,set类型提供了一些很实用的命令用于直接操作这些集合,如

1)sinter命令可以获得A和B两个用户的共同好友

2)sismember命令可以判断A是否是B的好友

3)scard命令可以获取好友数量

4)关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合

(2)随机展示

通常,app首页的展示区域有限,但是又不能总是展示固定的内容,一种做法是先确定一批需要展示的内容,再从中随机获取。如酷狗音乐K歌擂台赛当日的打擂歌曲共29首,首页随机展示5首;昨日打擂金曲共200首,首页随机展示30首。set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。

(3)黑名单/白名单

经常有业务出于安全性方面的考虑,需要设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中。

参考文档

www.php.cn/faq/551689.… zhuanlan.zhihu.com/p/500335153 blog.51cto.com/u_16099335/…

redis key设计技巧 redis key值设计

 转载

话不是这么说的2023-05-25 13:40:35

文章标签redis数据库javaRedis慢查询 文章分类Redis数据库 阅读数 640

一、Redis的键值设计

1.1key的结构

Redis的Key在自定义时,最好遵循以下三个规则:

  • 基本格式:[业务名称]:[数据名]:[id]
  • 长度不超过44字节
  • 不包含特殊字符

如:登录业务,保存用户信息的key 定义为 login:user:10( [业务名称]:[数据名]:[id] )

优点:

  • 可读性强
  • 避免key冲突
  • 方便管理(使用可视化工具显示的是 层级结构 清晰明了)
  • 更节省内存:key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小

1.2 BigKey问题

BigKey通常以key的大小和key中成员的数量来综合判定,如:

  • key本身的数据量过大:一个string类型的key,它的值为5MB
  • key中成员数过多:一个ZSET类型的key,它的成员数量为10000个
  • key中成员的数据量过大:比如一个Hash类型的key,它的成员数量可能不多,但是每个成员的value数据量大

推荐值:

  • 单个key的value小于10KB
  • 对于集合类型的key,建议元素数量小于1000

1.BigKey的危害

  • 网络阻塞
    对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例甚至物理机变慢
  • 数据倾斜
    BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资料达到均衡
  • Redis阻塞
    对元素较多的hash、list、zset等做运算会耗时较久,使主线程被阻塞
  • CPU压力
    对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其他应用

2.查找BigKey

  • redis-cli -a 密码 --bigkeys 利用redis客户端提供的参数,可以遍历分析所有的key,并返回key的整体统计信息与每个数据类型的Top1 的big key
  • redis key设计技巧 redis key值设计_redis
  • (每种数据类型的Top1可能是bigKey也可能不是,因为可能这种数据类型的key使用的数量少,而一些使用频率高的数据类型可能Top2也是BigKey,因此统计的并不完整)
  • scan 自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度
  • redis key设计技巧 redis key值设计_java_02
  • scan 是对所有key分成多个部分进行扫描,避免在千万数量级key下 扫描所有key 影响主线程性能
  • 第三发工具
    利用第三方工具,如Redis-Rdb-Tools分析RDB快照文件,全面分析内存使用情况
  • 网络监控
    自定义工具,监控Redis的网络数据,超出预警值时主动告警 (一般使用云服务中的工具分析如:阿里云)

3.删除BigKey

使用 unlink 异步删除 (Redis 4.0以后)

redis key设计技巧 redis key值设计_数据库_03

4.避免BigKey

例1:  比如在存储一个User对象,有三种存储方式:

redis key设计技巧 redis key值设计_java_04

由图可知,当存储User对象这类数据时,最好选择hash结构进行存储,空间占用小,且可以灵活访问对象的任意字段(hash结构存储注意数据类型转化)例2:  假如hash类型的key,有100万对field和value,这个key有什么问题?如何优化?

redis key设计技巧 redis key值设计_数据库_05

存在的问题:

  1. hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用多
  2. 可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题

redis key设计技巧 redis key值设计_redis_06

string类型占用内存太高,且批量获取数据麻烦,没有hash结构的关联性

redis key设计技巧 redis key值设计_数据库_07

注:  一些短期使用的key或者不经常使用的key设置过期时间,这样就不会出现BigKey问题

二、批处理优化

2.1 Pipeline

1.批处理的优势
单个命令的执行流程

一次命令的响应时间 = 1次往返的网络传输耗时+1次Redis执行命令耗时

redis key设计技巧 redis key值设计_java_08

网络传输耗时: ms毫秒级别 Redis执行命令耗时:us微秒级别
如果执行N条命令,那么N次网络传输往返的耗时将被扩大N倍,而执行命令的耗时可以忽略不记,此时执行N条命令耗时很长,会影响Redis的性能

如果一次网络传输就执行N次命令,那么就可以解决批量数据操作耗时长问题,提高了效率

redis key设计技巧 redis key值设计_redis_09

Redis提供了很多批处理命令,可以实现批量插入数据,例如:

  • mset
  • hmset

注:  不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞

由于mset只能对string类型的数据进行批处理,而hmset只能对hash类型的数据进行操作,如果对复杂数据类型处理可以使用 Pipeline

Pipeline类似于一个管道,将命令放入管道中进行传输。

redis key设计技巧 redis key值设计_redis_10

Pipeline的多个命令之间不具备原子性,可能会出现多线程插队等待耗时问题。

2.2 集群下的批处理

如 mset 或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。

解决方案:

redis key设计技巧 redis key值设计_数据库_11

由于hash_tag容易出现数据倾斜,在集群模式下数据倾斜可能会造成单点问题,因此常用并行slot方式实现集群下批处理。在spring整合的redis包中 stringRedisTemplate 有封装好的用法,底层实现时异步的管道传输模式。

例如:stringRedisTemplate.opsForValue().multiSet();

三、服务端优化

3.1持久化配置

Redis的持久化虽然保证数据安全,但也会带来很多额外的开销,因此持久化一般遵守以下建议:

  1. 用来做缓存的Redis实例尽量不要开启持久化功能 (开多个Redis实例分别复杂不同的业务)
  2. 建议关闭RDB持久化功能,使用AOF持久化
  3. 利用脚本定期在slave节点做RDB,实现数据备份
  4. 设置合理的rewrite阈值,避免频繁的bgrewrite
  5. 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做AOF,避免因AOF引起的阻塞 (可能出现数据丢失)

部署相关建议:

  1. Redis实例的物理机要预留足够内存,应对fork和rewrite
  2. 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
  3. 不要与CPU密集型应用部署在一起 (fork时比较耗CPU)
  4. 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

总结就是 redis最好独占一个服务器 hh

3.2 慢查询

在Redis执行时耗时超过某个阈值的命令,称为慢查询。

由于Redis是单线程执行命令,所以执行一个命令会放入队列中,排队等待执行,此时若一个慢查询耗时较久,队列中等待的命令可能因等待超时出错

redis key设计技巧 redis key值设计_Redis_12

阈值 的配置:

  • slowlog-log-slower-than :慢查询阈值,单位微秒。默认10000,建议1000
    慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:

  • slowlog-max-len: 慢查询日志(本质是一个队列)的长度。默认是128,建议1000

    查看慢查询日志列表:

  • slowlog len : 查询慢查询日志长度

  • slowlog get [n] 读取n条慢查询日志

  • slowlog reset:清空慢查询列表

redis key设计技巧 redis key值设计_慢查询_13

可以使用桌面客户端RESP

redis key设计技巧 redis key值设计_Redis_14

3.3 内存安全和配置

当Redis内存不足时,可能导致key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要警惕,并快速定位到内存被占用的原因。

redis key设计技巧 redis key值设计_Redis_15

查看Redis目前的内存分配状态

  • info memory
  • memory xxx

内存缓冲区配置

  • 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置,默认1mb
  • AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。无法设置容量上限
  • 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置

3.4 集群的问题

1.数据完整性问题

在Redis的默认配置中,如果发现任意一个插槽不可用,整个集群都会对外停止服务

redis key设计技巧 redis key值设计_数据库_16

2.带宽问题
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:

  • 插槽信息
  • 集群状态信息

集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。

解决途径:

  1. 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
  2. 避免在单个物理机中运行太多Redis实例
  3. 配置合适的cluster-node-timeout

3.数据倾斜问题

BigKey或批处理时,使用相同的hash_tag来保证所有key落在同一个插槽slot,会导致某些结点数据量远大于其他节点

4.客户端性能问题
当使用客户端连接Redis集群时,需要进行节点选择、插槽slot判断、读写判断,会影响客户端性能

5.命令的集群兼容性问题

比如批处理mset 在集群模式如果不能落在相同的插槽slot上就会报错。

6.lua和事务问题
多个命令的key必须在同一个slot,如果不在就会报错。因此集群模式下就不能使用lua和事务

单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用性。如果主从能满足业务需求,那么就尽量不搭建Redis集群

blog.51cto.com/u_12929/634…

HyperLogLog

edis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 HyperLogLog 的优点在于它所需的内存并不会因为集合的大小而改变,无论集合包含的元素有多少个,HyperLogLog 进行计算所需的内存总是固定的,并且是非常少的

每个 HyperLogLog 最多只需要花费 12KB 内存,在标准误差 0.81%的前提下,就可以计算 2 的 64 次方个元素的基数。

Redis 实战

HyperLogLog 使用太简单了。PFADD、PFCOUNT、PFMERGE三个指令打天下。

PFADD

将访问页面的每个用户 ID 添加到 HyperLogLog 中。

PFADD Redis主从同步原理:uv userID1 userID 2 useID3

复制

PFCOUNT

利用 PFCOUNT 获取 「Redis 主从同步原理」文章的 UV 值。

PFCOUNT Redis主从同步原理:uv

复制

PFMERGE 使用场景

HyperLogLog 除了上面的 PFADDPFCOIUNT 外,还提供了 PFMERGE
语法
PFMERGE destkey sourcekey [sourcekey ...]

复制

比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。

其中页面的 UV 访问量也需要合并,那这个时候 PFMERGE 就可以派上用场了,也就是同样的用户访问这两个页面则只算做一次

如下所示:Redis、MySQL 两个 HyperLogLog 集合分别保存了两个页面用户访问数据。

PFADD Redis数据 user1 user2 user3
PFADD MySQL数据 user1 user2 user4
PFMERGE 数据库 Redis数据 MySQL数据
PFCOUNT 数据库 // 返回值 = 4

复制

将多个 HyperLogLog 合并(merge)为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合(observed set)的并集。