98-Hands-on

1. redisTemplate

1. expire

1. 设置

  • 不设置有效期,过期时间为: -1
stringRedisTemplate.expire(key, 60L, TimeUnit.SECONDS);

1. 获取

  • 返回值为-1时,此键值没有设置过期日期
  • 返回值为-2时,不存在此键
Long expire = stringRedisTemplate.getExpire(localKey);
if (expire != null && expire != -2) {
    throw new ResultException(40023, "短信已经发送," + smsCodeTime / 60 + "分钟有效,请勿重复发送", expire);
}

2. 避免缓存雪崩

import cn.hutool.core.util.RandomUtil;

long expireTime = 100 + RandomUtil.randomInt(10, 100);
this.stringRedisTemplate.expire(key, expireTime, TimeUnit.MINUTES);

3. 多级存取

/**
 * userByLoginName
 *
 * @param appId      appId
 * @param identifier loginName
 * @param isRefresh  isRefreshCache
 * @return User
 */
@Override
public User userByIdentifier(String appId, String identifier, Boolean isRefresh) {
    String localKey = RedisKeyUtils.userInfoKey(appId, identifier);
    if (!isRefresh) {
        Object o = redisTemplateObj.opsForValue().get(localKey);
        if (o != null) {
            return (User) o;
        }
    }
    User param = new User();
    param.setAppId(appId);
    if (identifier.matches(Constant.mobileRegex)) {
        param.setMobile(identifier);
    } else {
        param.setLoginName(identifier);
    }
    List<User> users = userExtMapper.select(param);
    if (users.size() > 0) {
        User one = users.get(0);
        redisTemplateObj.opsForValue().set(localKey, one, Duration.ofDays(RandomUtil.randomInt(7, 14)));
        return one;
    }
    return null;
}












 














 




4. increment()

// 1-key存在,直接自增, 2-key不存在,从0创建再自增。expire默认为-1
stringRedisTemplate.opsForValue().increment(key);
// 设置超时时间
stringRedisTemplate.expire(key, 30L, TimeUnit.DAYS);

2. redis过期key、内存淘汰

1. 设置过期

  • expire key seconds:key在N秒后过期
  • pexpire key milliseconds:key在n毫秒后过期
  • expireat key timestamp:key在某个时间戳后过期(精确到秒)
  • pexpireat key millisecondstimestamp:key在一个时间戳后过期(精确到毫秒)

1. expire

  • N秒后过期
  • 命令TTL全称(time to live)
127.0.0.1:6379># set key value
OK
127.0.0.1:6379># expire key 100
(integer) 1
127.0.0.1:6379># ttl key
(integer) 93

2. pexpire

  • N毫秒后过期
127.0.0.1:6379># set key2 value2
OK
127.0.0.1:6379># pexpire key2 100000
(integer) 1
127.0.0.1:6379># pttl key2
(integer) 94524

3. expireat

  • 在某个时间戳过期(精确到秒)
127.0.0.1:6379># set key3 value3
OK
127.0.0.1:6379># expireat key3 1630644399
(integer) 1
127.0.0.1:6379># ttl key3
(integer) 67
  • expired Key3 1630644399:(精确到秒)之后过期。使用TTL查询,可以发现Key3会在67s后过期
  • 在 Redis 中,可以使用 time 命令查询当前时间的时间戳(精确到秒)
127.0.0.1:6379># time
1"1630644526"
2"239640"

4. pexpireat

  • 在某个时间戳过期(精确到毫秒)
127.0.0.1:6379># set key4 value4
OK
127.0.0.1:6379># pexpireat key4 1630644499740
(integer) 1
127.0.0.1:6379># pttl key4
(integer) 3522

5. set()

直接操作value为string的过期时间有几种方法:

  • set key value ex seconds:N秒后过期
  • set key value ex milliseconds:设置key在n毫秒后过期
  • setex key seconds value:为指定的key设置值及其过期时间,如果key已经存在,SETEX将会替换旧的值
  1. 设置kv对在N秒后过期
127.0.0.1:6379># set k v ex 100
OK
127.0.0.1:6379># ttl k
(integer) 97
  1. 设置kv对在N毫秒后过期
127.0.0.1:6379># set k2 v2 px 100000
OK
127.0.0.1:6379># pttl k2
(integer) 92483
  1. 使用setex来设置
127.0.0.1:6379># setex k3 100 v3
OK
127.0.0.1:6379># ttl k3
(integer) 91

2. 取消过期

  • persist key去除key值的过期时间
  • set key value重新设置value
127.0.0.1:6379># ttl k3
(integer) 97
127.0.0.1:6379># persist k3
(integer) 1
127.0.0.1:6379># ttl k3
(integer) -1

3. 过期策略

  • Redis对过期key的删除策略
    1. 定时删除
    2. 定期删除
    3. 惰性删除

1. 定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务执行对key的删除操作

  • 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
  • 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量

2. 定期删除

Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除

  • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存
  • 缺点:难以确定删除操作执行的时长和频率。如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,业务不能忍受的错误

3. 惰性删除

过期key,停留在内存里。查一下key,才会被Redis给删除掉。这就是所谓的惰性删除。expireIfNeeded()检查数据是否过期,执行get的时候调用

  • 空间换时间
    • 优点:节约CPU性能,发现必须删除的时候才删除
    • 缺点:内存压力很大,出现长期占用内存的数据

edis采取了折中的删除策略,定期删除 + 惰性删除策略

4. 内存淘汰策略

  • redis.conf设置 100MB 的内存限制
  • CONFIG SET命令动态修改
maxmemory 100mb
  • 设置了Redis内存上限,当内存中的数据量达到其设置的上限时,就需要采取一定的淘汰策略
image-20230911145000509
  • noeviction(默认):新写入操作会报错
  • allkeys-lru:在键空间中,移除最近最少使用的key
  • allkeys-random:在键空间中,随机移除某个key
  • volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key
  • volatile-random:在设置了过期时间的键空间中,随机移除某个key
  • volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除

需要注意,没有设置expire的key,不满足先决条件,那么volatile-***noeviction基本上一致

  • 当部分数据访问频率较高而其余部分访问频率较低,或者数据的使用频率无法预测时,设置allkeys-lru比较合适
  • 所有数据访问概率大致相等,可以allkeys-random
  • 开发者需要通过设置不同的ttls来确定数据过期的顺序,可以volatile-ttl
  • 一些数据长期保存,而一些数据可以消除,可以volatile-lruvolatile-random
  • 设置expire会消耗额外内存,可以选择allkeys-lru。不再设置过期时间,高效利用内存

5. 经验之谈

  • 对具有时效性的key设置过期时间,通过Redis自身的过期key清理策略来降低内存占用
  • 单Key不要过大,这种key网络传输延迟会比较大,需要分配的输出缓冲区也较大,定期清理时也容易造成较高延迟。最好能通过业务拆分,数据压缩等方式避免这种过大key产生
  • 不同业务,最好使用不同的逻辑db分开。将key分布在不同的db有助于过期Key及时清理。另外不同业务使用不同db也有助于问题排查和无用数据及时下线

3. 应用场景

1. 缓存

  • String类型
    • eg:热点数据缓存、对象缓存、全页缓存、可以提升热点数据的访问速度

2. 数据共享分布式

  • String类型。Redis是分布式的独立服务,可以在多个应用之间共享
    • eg:分布式Session
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

3. 分布式锁

  • String类型。setnx方法,只有不存在时才能添加成功,返回true
public static boolean getLock(String key) {
    Long flag = jedis.setnx(key, "1");
    if (flag == 1) {
        jedis.expire(key, 10);
    }
    return flag == 1;
}

public static void releaseLock(String key) {
    jedis.del(key);
}

4. 全局ID

  • int类型。incrby,利用原子性
    • incrby userid 1000
  • 分库分表的场景,一次性拿一段

5. 计数器

  • int类型。incr
    • eg:文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库

6. 限流

  • int类型。incr
    • 以访问者的ip和其他信息作为key,访问一次增加一次计数,超过次数则返回false

7. 位统计

  • String类型。bitcount(1.6.6的bitmap数据结构介绍)
  • 字符是以8位二进制存储的
set k1 a
setbit k1 6 1
setbit k1 7 0
get k1

# 6 7 代表的a的二进制位的修改
#  a ASCII码是97,二进制 01100001
#  b ASCII码是98,二进制 01100010

# bit非常节省空间(1MB=8388608 bit),可以做大数据量的统计
  • 在线用户统计,留存用户统计
setbit onlineusers 01
setbit onlineusers 11
setbit onlineusers 20
  • 支持按位与、按位或等等操作
BITOP AND destkey [key...]   # 对一个或多个 key 求逻辑并,结果保存到 destkey
BITOP OR destkey [key...]    # 对一个或多个 key 求逻辑或,结果保存到 destkey
BITOPX OR destkey [key...]   # 对一个或多个 key 求逻辑异或,结果保存到 destkey
BITOP NOT destkey key        # 对给定 key 求逻辑非,结果保存到 destkey
  • 计算出7天都在线的用户
BITOP AND "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ...  "day_7_online_users"

8. 购物车

  • String、hash。String可以做的hash都可以做
image-20230911145124774
  • key:用户id
  • field:商品id
  • value:商品数量
    • +1:hincr
    • -1:hdecr
    • 删除:hdel
    • 全选:hgetall
    • 商品数:hlen

9. 用户消息时间线

  • list,双向链表。直接作为timeline就好。插入有序

10. 消息队列

List提供了两个阻塞的弹出操作,可以设置超时时间

  • blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果没有元素会阻塞列表直到等待超时或发现可弹出元素为止
  • brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果没有元素会阻塞列表直到等待超时或发现可弹出元素为止

  • 队列:先进先除:rpush, blpop,左头右尾,右边进入队列,左边出队列
  • 栈:先进后出:rpush, brpop

11. 抽奖

  • 自带一个随机获得值
spop myset

12. 点赞、签到、打卡

  • 微博ID是t1001,用户ID是u3001
image-20230911145144161

like:t1001 来维护 t1001 这条微博的所有点赞用户

  • 点赞:sadd like:t1001 u3001
  • 取消点赞:srem like:t1001 u3001
  • 是否点赞:sismember like:t1001 u3001
  • 点赞的所有用户:smembers like:t1001
  • 点赞数:scard like:t1001

13. 商品标签

image-20230911145158297

老规矩,用tags:i5001来维护商品所有的标签

  • sadd tags:i5001 画面清晰细腻
  • sadd tags:i5001 真彩清晰显示屏
  • sadd tags:i5001 流程至极

14. 商品筛选

# 获取差集
sdiff set1 set2
# 获取交集(intersection)
sinter set1 set2
# 获取并集
sunion set1 set2
image-20230911145211346
  • eg:iPhone11 上市了
sadd brand:apple iPhone11

sadd brand:ios iPhone11

sadd screensize:6.0-6.24 iPhone11

sadd screentype:lcd iPhone11
  • 筛选商品(交集)。苹果的、ios的、屏幕在6.0-6.24之间的,屏幕材质是LCD屏幕
sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd

15. 用户关注、推荐模型

# follow-关注, fans-粉丝
# 相互关注
sadd 1:follow 2
sadd 2:fans 1
sadd 1:fans 2
sadd 2:follow 1

# 我关注的人也关注了他(取交集)
sinter 1:follow 2:fans

# 可能认识的人:
# 用户1可能认识的人(差集):
sdiff 2:follow 1:follow
# 用户2可能认识的人
sdiff 1:follow 2:follow

16. 排行榜

# id 为6001 的新闻点击数加1
zincrby hotNews:20190926 1 n6001

# 获取今天点击最多的15条
zrevrange hotNews:20190926 0 15 withscores
image-20230911145231085

4. Scan

/**
 * scan
 *
 * @param pattern pattern
 * @return String
 */
@Override
public Set<String> scan(String pattern) {
    return redisTemplateObj.execute((RedisCallback<Set<String>>) connection -> {
        Set<String> keysTmp = new HashSet<>();
        Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder()
                .match(pattern)
                .count(10_000).build());

        while (cursor.hasNext()) {
            keysTmp.add(new String(cursor.next(), StandardCharsets.UTF_8));
        }
        return keysTmp;
    });
}










 
 
 







@Override
public Long unlink(Set<String> keys) {
    return redisTemplateObj.unlink(keys);
}


 

/**
 * delByKey
 *
 * @param pattern pattern
 * @return Boolean
 */
@RequestMapping("/delByKeys")
public ResultUtil<Long> delByKeys(String pattern) {
    Set<String> scan = redisService.scan(pattern);
    return ResultUtil.success(redisService.unlink(scan));
}








 
 

99. good-code

  • 方哥给的代码,简单学习下思想
@PostConstruct
public void init() {
    for (OoxxEnableEnum m : OoxxEnableEnum.values()) {
        redisService.addByKeyFieldValue(m.getCode(), "url", m.getUrl());
        redisService.addByKeyFieldValue(m.getCode(), "enabled", m.isEnabled());
    }
}

@RequestMapping("/updateOoxxData")
public void updateOoxxData(@RequestParam("key") String key, @RequestParam("field") String field
        , @RequestParam("value") Object value) {
    redisService.addByKeyFieldValue(key, field, value);
}

@RequestMapping("/getOoxxData")
public Object getOoxxData(@RequestParam("key") String key, @RequestParam("field") String field) {
    return redisService.getValueByKeyAndField(key, field);
}


 
 
 
 












@Override
public void addByKeyFieldValue(String key, String field, Object value) {
    redisTemplateObj.opsForHash().put(RedisKeyUtils.getInvokeOoxxKey(key), field, value);
}

@Override
public Object getValueByKeyAndField(String key, String field) {
    return redisTemplateObj.opsForHash().get(RedisKeyUtils.getInvokeOoxxKey(key), field);
}

@Override
public String getInvokeUrl(String OoxxId) {
    if (!this.invokeEnabled(OoxxId)) {
        return null;
    }
    Object obj = this.getValueByKeyAndField(OoxxId, "url");
    if (obj != null) {
        return obj.toString();
    }
    return null;
}

@Override
public boolean invokeEnabled(String OoxxId) {
    Object obj = this.getValueByKeyAndField(OoxxId, "enabled");
    if (obj != null) {
        return (boolean) obj;
    }
    return false;
}