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. 避免缓存雪崩
- Redis雪崩
- 失效时间插入一个随机数,这样可以避免集体失效
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
将会替换旧的值
- 设置kv对在N秒后过期
127.0.0.1:6379># set k v ex 100
OK
127.0.0.1:6379># ttl k
(integer) 97
- 设置kv对在N毫秒后过期
127.0.0.1:6379># set k2 v2 px 100000
OK
127.0.0.1:6379># pttl k2
(integer) 92483
- 使用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. 定时删除
创建一个定时器,当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内存上限,当内存中的数据量达到其设置的上限时,就需要采取一定的淘汰策略
noeviction
(默认):新写入操作会报错allkeys-lru
:在键空间中,移除最近最少使用的keyallkeys-random
:在键空间中,随机移除某个keyvolatile-lru
:在设置了过期时间的键空间中,移除最近最少使用的keyvolatile-random
:在设置了过期时间的键空间中,随机移除某个keyvolatile-ttl
:在设置了过期时间的键空间中,有更早过期时间的key优先移除
需要注意,没有设置expire
的key,不满足先决条件,那么volatile-***
和noeviction
基本上一致
- 当部分数据访问频率较高而其余部分访问频率较低,或者数据的使用频率无法预测时,设置
allkeys-lru
比较合适 - 所有数据访问概率大致相等,可以
allkeys-random
- 开发者需要通过设置不同的ttls来确定数据过期的顺序,可以
volatile-ttl
- 一些数据长期保存,而一些数据可以消除,可以
volatile-lru
或volatile-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都可以做
- key:用户id
- field:商品id
- value:商品数量
- +1:
hincr
- -1:
hdecr
- 删除:
hdel
- 全选:
hgetall
- 商品数:
hlen
- +1:
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
用 like:t1001
来维护 t1001 这条微博的所有点赞用户
- 点赞:
sadd like:t1001 u3001
- 取消点赞:
srem like:t1001 u3001
- 是否点赞:
sismember like:t1001 u3001
- 点赞的所有用户:
smembers like:t1001
- 点赞数:
scard like:t1001
13. 商品标签
老规矩,用tags:i5001
来维护商品所有的标签
sadd tags:i5001 画面清晰细腻
sadd tags:i5001 真彩清晰显示屏
sadd tags:i5001 流程至极
14. 商品筛选
# 获取差集
sdiff set1 set2
# 获取交集(intersection)
sinter set1 set2
# 获取并集
sunion set1 set2
- 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
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;
});
}
5. Unlink
@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;
}