Redis介绍
特征
-
键值对类型,值支持多种类型
-
单线程,命令具备原子性,线程安全
-
低延迟速度快(基于内存、IO多路复用、良好编码)
-
支持数据持久化
-
支持主从集群、分片集群
启动
-
默认启动
-
redis-server
-
-
指定配置启动
-
开机自启
-
vim /etc/systemd/system/redis.service
-
[Unit] Description=Advanced key-value store After=network.target Documentation=http://redis.io/documentation, man:redis-server(1) [Service] Type=forking ExecStart=/usr/bin/redis-server /etc/redis/redis.conf ExecStop=/bin/kill -s TERM $MAINPID PIDFile=/run/redis/redis-server.pid TimeoutStopSec=0 Restart=always User=redis Group=redis RuntimeDirectory=redis RuntimeDirectoryMode=2755 UMask=007 PrivateTmp=yes LimitNOFILE=65535 PrivateDevices=yes ProtectHome=yes ReadOnlyDirectories=/ ReadWriteDirectories=-/var/lib/redis ReadWriteDirectories=-/var/log/redis ReadWriteDirectories=-/var/run/redis NoNewPrivileges=true CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_SYS_RESOURCE MemoryDenyWriteExecute=true ProtectKernelModules=true ProtectKernelTunables=true ProtectControlGroups=true RestrictRealtime=true RestrictNamespaces=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # redis-server can write to its own config file when in cluster mode so we # permit writing there by default. If you are not using this feature, it is # recommended that you replace the following lines with "ProtectSystem=full". ProtectSystem=true ReadWriteDirectories=-/etc/redis [Install] WantedBy=multi-user.target Alias=redis.service
-
数据结构
key-value的数据库,key一般为String类型,value可以是以下形式
-
String
-
Hash
-
List
-
Set
-
SortedSet
-
GEO
-
BitMap
-
HyperLog
基本操作
通用命令
查看符合的key(模糊查询) 不建议在生产环境使用,造成线程阻塞
KEYS 参数
keys * 获取所有keys
keys n* 获取所有n开头keys
删除指定的key
del 键名(支持多个键名)
del k1 k2 k3
判断key是否存在
exists 键名(支持多个键名)
给一个key设置有效期,有效期到期key会被自动删除
expire 键名 秒
查看一个key的剩余有效期
ttl 键名
返回结果为-1代表永久有效
返回结果为-2代表被移除
Key的层级结构
Redis的key允许有多个单词形成层级结构,多个单词之间用':'隔开
例如我们的项目名称叫 heima,有user和product两种不同类型的数据,我们可以这样定义key:
-
user相关的key:heima:user:1
-
product相关的key:heima:product:1
如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:
KEY | VALUE |
---|---|
heima:user:1 | {"id":1, "name": "Jack", "age": 21} |
heima:product:1 | {"id":1, "name": "小米11", "price": 4999} |
一旦我们向redis采用这样的方式存储,那么在可视化界面中,redis会以层级结构来进行存储,形成类似于这样的结构,更加方便Redis获取数据
String类型
string,int,float
常用命令
-
SET:添加或者修改已经存在的一个String类型的键值对
-
GET:根据key获取String类型的value
-
MSET:批量添加多个String类型的键值对
-
MGET:根据多个key获取多个String类型的value
-
INCR:让一个整型的key自增1
-
INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
-
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
-
SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
-
SETEX:添加一个String类型的键值对,并且指定有效期
Hash类型
value是一个无序列表
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD:
常用命令
-
HSET key field value:添加或者修改hash类型key的field的值
-
HGET key field:获取一个hash类型key的field的值
-
HMSET:批量添加多个hash类型key的field的值
-
HMGET:批量获取多个hash类型key的field的值
-
HGETALL:获取一个hash类型的key中的所有的field和value
-
HKEYS:获取一个hash类型的key中的所有的field
-
HINCRBY:让一个hash类型key的字段值自增并指定步长
-
HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
List类型
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
特征也与LinkedList类似:
-
有序
-
元素可以重复
-
插入和删除快
-
查询速度一般
常用来存储一个有序数据,例如:朋友圈点赞先后顺序列表,评论列表等。
常用命令
-
LPUSH key element ... :向列表左侧插入一个或多个元素
-
LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil
-
RPUSH key element ... :向列表右侧插入一个或多个元素
-
RPOP key:移除并返回列表右侧的第一个元素
-
LRANGE key star end:返回一段角标范围内的所有元素
-
BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
Set类型
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:
-
无序
-
元素不可重复
-
查找快
-
支持交集.并集.差集等功能
常用命令
-
SADD key member ... :向set中添加一个或多个元素
-
SREM key member ... : 移除set中的指定元素
-
SCARD key: 返回set中元素的个数
-
SISMEMBER key member:判断一个元素是否存在于set中
-
SMEMBERS:获取set中的所有元素
-
SINTER key1 key2 ... :求key1与key2的交集
-
SDIFF key1 key2 ... :求key1与key2的差集
-
SUNION key1 key2 ..:求key1和key2的并集
SortedSet类型
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。
SortedSet具备下列特性:
-
可排序
-
元素不重复
-
查询速度快
因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。
常用命令
-
ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值
-
ZREM key member:删除sorted set中的一个指定元素
-
ZSCORE key member : 获取sorted set中的指定元素的score值
-
ZRANK key member:获取sorted set 中的指定元素的排名
-
ZCARD key:获取sorted set中的元素个数
-
ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
-
ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值
-
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
-
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
-
ZDIFF.ZINTER.ZUNION:求差集.交集.并集
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:
-
升序获取sorted set 中的指定元素的排名:ZRANK key member
-
降序获取sorted set 中的指定元素的排名:ZREVRANK key memeber
Jedis
导入依赖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.0</version>
</dependency>
建立连接
void setUp(){
// 建立连接
jedis = new Jedis("192.168.31.160", 6379);
// 密码
jedis.auth("123456");
// 选择库
jedis.select(0);
}
redis操作
@Test
void testString(){
// 插入数据
String result = jedis.set("name", "sb");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
@Test
void testHash() {
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("name", "test");
hashMap.put("age", "21");
hashMap.put("sex", "man");
jedis.hset("user:1", "name", "sb");
jedis.hset("user:1", "age", "22");
jedis.hset("user:1", "sex", "man");
jedis.hset("user:2", hashMap);
Map<String, String> map = jedis.hgetAll("user:2");
System.out.println(map);
}
释放连接
void testDown() {
if (jedis != null) {
jedis.close();
}
}
Jedis连接池
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此使用Jedis连接池代替Jedis的直连方式
有关池化思想,并不仅仅是这里会使用,很多地方都有,比如说我们的数据库连接池,比如我们tomcat中的线程池,这些都是池化思想的体现。
public class JedisConnectionFacotry {
private static final JedisPool jedisPool;
static {
//配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setMaxWaitMillis(1000);
//创建连接池对象
jedisPool = new JedisPool(poolConfig,
"192.168.31.160",6379,1000,"123456");
}
public static Jedis getJedis(){
return jedisPool.getResource();
}
}
代码说明:
-
1) JedisConnectionFacotry:工厂设计模式是实际开发中非常常用的一种设计模式,我们可以使用工厂,去降低代的耦合,比如Spring中的Bean的创建,就用到了工厂设计模式
-
2)静态代码块:随着类的加载而加载,确保只能执行一次,我们在加载当前工厂类的时候,就可以执行static的操作完成对 连接池的初始化
-
3)最后提供返回连接池中连接的方法.
SpringDataRedis
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:Spring Data Redis
-
提供了对不同Redis客户端的整合(Lettuce和Jedis)
-
提供了RedisTemplate统一API来操作Redis
-
支持Redis的发布订阅模型
-
支持Redis哨兵和Redis集群
-
支持基于Lettuce的响应式编程
-
支持基于JDK.JSON.字符串.Spring对象的数据序列化及反序列化
-
支持基于Redis的JDKCollection实现
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
Redis实战
起步
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.yml配置
server:
port: 8081
spring:
application:
name: hmdp
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC
username: root
password: 123456
redis:
host: 192.168.31.160
port: 6379
password: 123456
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
jackson:
default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
level:
com.hmdp: debug
---
server:
servlet:
encoding:
force-request: true
force-response: true
context-path: /api
缓存问题及解决方案
缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案
-
在服务器端,接收参数时业务接口中过滤不合法的值,null,负值,和空值进行检测和空值。
-
bloom filter(布隆过滤):类似于哈希表的一种算法,用所有可能的查询条件生成一个bitmap,在进行数据库查询之前会使用这个bitmap进行过滤,如果不在其中则直接过滤,从而减轻数据库层面的压力。
-
空值缓存:一种比较简单的解决办法,在第一次查询完不存在的数据后,将该key与对应的空值也放入缓存中,只不过设定为较短的失效时间,例如几分钟,这样则可以应对短时间的大量的该key攻击,设置为较短的失效时间是因为该值可能业务无关,存在意义不大,且该次的查询也未必是攻击者发起,无过久存储的必要,故可以早点失效。
缓存雪崩
因为缓存服务挂掉或者热点缓存失效,所有请求都去查数据库,导致数据库连接不够或者数据库处理不过来,从而导致整个系统不可用。
解决方案
-
在原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。
-
使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
-
提高数据库的容灾能力,可以使用分库分表,读写分离的策略。
-
使用redis集群
缓存击穿
缓存击穿实际上是缓存雪崩的一个特例,缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。击穿与雪崩的区别即在于击穿是对于某一特定的热点数据来说,而雪崩是全部数据。
解决方案
-
使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。
-
设置逻辑过期:针对与热点缓存数据缓存到期。为防止缓存时间到期数据清除,同时大量请求需要查询该数据,导致大量请求进入数据库。可将热点数据添加逻辑过期时间,不使用redis自带的过期时间,可避免字段在大量访问的时间段内缓存过期。当请求查询该字段时,首先检查该字段的逻辑过期时间,判断是否过期,如果没有过期则直接返回结果;如果数据过期,则生成一个新的线程用于查询数据库并更新redis,同时对查询数据库的方法加锁,查询结束后更新redis释放锁。在数据逻辑时间过期到redis更新数据完成之间,其他请求都会返回redis中的旧数据,不会降低系统的qps。缺点为无法保证数据的一致性,在redis更新期间其他线程使用的仍然是旧数据。
使用
注入StringRedisTemplate CacheClient
StringRedisTemplate stringRedisTemplate;
RedisClient redisClient;
public ShopServiceImpl(StringRedisTemplate stringRedisTemplate, RedisClient redisClient) {
this.stringRedisTemplate = stringRedisTemplate;
this.redisClient = redisClient;
}
RedisClient工具类
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* Redis操作工具类
* 包含将任意类型数据序列化为String类型存储方法
* 根据id获取String存储反序列化为任意类型的方法
* 存储并设置逻辑过期时间方法
* 防止缓存穿透的查询方法
* 使用逻辑过期防止缓存穿透查询方法
*/
@Slf4j
@Component
public class RedisClient {
private static final Long CACHE_NULL_TTL = 2L;
private static final Long LOCK_TTL = 2L;
private static final String LOCK_KEY = "lock:";
StringRedisTemplate stringRedisTemplate;
public RedisClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 以字符串形式存入redis,入参任意类型的值,序列化为string类型存入redis
*
* @param key 键名
* @param value 值(任意类型)
* @param time ttl
* @param unit 时间单位
*/
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 以字符串形式存入redis,入参任意类型的值,序列化为string类型存入redis,
* 同时执行传入的数据库方法
*
* @param key 键名
* @param value 值(任意类型)
* @param dbFallback 数据库方法 如"id2 -> getById(id2)"
* @param time ttl
* @param unit 时间单位
*/
@Transactional(rollbackFor = Exception.class)
public void set(String key, Object value, Function<Object, Object> dbFallback, Long time, TimeUnit unit) {
dbFallback.apply(value);
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
/**
* 存入redis,设置逻辑过期时间
*
* @param key 键名
* @param value 值(任意类型)
* @param time ttl
* @param unit 时间单位
*/
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 根据id查询redis中的数据,并将查询到的数据反序列化为任意类型返回
*
* @param keyPrefix 查询id的前缀 如"login:code:"
* @param id 需要查询的id
* @param type 返回值类型 如"Shop.class"
* @param <R> 返回值类型 如"Shop.class"
* @param <ID> id的类型
* @return 指定类型的返回值
*/
public <R, ID> R queryByKey(String keyPrefix, ID id, Class<R> type) {
String jsonStr = stringRedisTemplate.opsForValue().get(keyPrefix + id);
return JSONUtil.toBean(jsonStr, type);
}
/**
* 使用redis查询防止缓存穿透方法
* 首先在缓存中查询,如果id已存在redis则直接返回查询结果,若命中保存的null,则返回null。
* 如果redis未存储此id,调用用户写入的查询逻辑去做数据库查询,如果命中则写入redis并返回结果,
* 若未命中则将null以查询id为键名写入redis,防止缓存穿透。
*
* @param keyPrefix 查询id的前缀 如"login:code:"
* @param id 需要查询的id
* @param type 返回值类型 如"Shop.class"
* @param <R> 返回值类型 如"Shop.class"
* @param <ID> id的类型
* @param dbFallback 数据库查询函数 如"id2 -> getById(id2)"
* @param time ttl
* @param unit 时间单位
*/
public <R, ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (StrUtil.isNotBlank(json)) {
// 存在则返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否为空值
if (json != null) {
// 返回错误信息
return null;
}
// 不存在,去数据库查询
R r = dbFallback.apply(id);
// 不存在,返回错误
if (r == null) {
// 将空值写入redis,防止缓存穿透
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 存在,写入redis
this.set(key, r, time, unit);
return r;
}
// 创建线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 使用逻辑过期时间防止缓存穿透
*
* @param keyPrefix 查询id的前缀 如"login:code:"
* @param id 需要查询的id
* @param type 返回值类型 如"Shop.class"
* @param <R> 返回值类型 如"Shop.class"
* @param <ID> id的类型
* @param dbFallback 数据库查询函数 如"id2 -> getById(id2)"
* @param time ttl
* @param unit 时间单位
*/
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 从redis获取缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if (StrUtil.isBlank(json)) {
// 不存在直接返回
return null;
}
// redis命中,把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 判断是否过期
if (expireTime.isAfter(LocalDateTime.now())) {
// 未过期,返回信息
return r;
}
// 已过期,缓存重建
// 获取互斥锁
String lockKey = LOCK_KEY + id;
boolean isLock = tryLock(lockKey);
// 判断是否获取成功
if (isLock) {
// 成功获取,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R r1 = dbFallback.apply(id);
// 写入redis
this.setWithLogicalExpire(key, r1, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
return r;
}
// 获取锁
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
// 释放锁
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}
RedisData实体类
该实体类用于实现redis字段逻辑过期,使用逻辑过期方法防止缓存击穿
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
// 逻辑过期
private LocalDateTime expireTime;
private Object data;
}
使用方法
// 将数据存入redis,十分钟后过期
@Test
void cacheClientSave() {
User user = new User();
user.setPhone("12345678911");
user.setNickName("sb");
user.setPassword("123456");
redisClient.set("test:1", user, 10L, TimeUnit.MINUTES);
}
// 将数据存入数据库后同时存入redis
@Test
void cacheClientSave1() {
User user = new User();
user.setId("1")
user.setPhone("18376711111");
user.setNickName("sb");
user.setPassword("123456");
redisClient.set("test:1", user, (a)->userMapper.insert(user), 10L, TimeUnit.MINUTES);
}
// 根据key查询redis数据
@Test
void cacheClientQuery() {
User user = redisClient.queryByKey("test:", "1", User.class);
System.out.println(user);
}
// 查询redis同时解决缓存穿透
Shop shop = redisClient.queryWithPassThrough(
CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 逻辑过期解决缓存击穿
Shop shop = redisClient.queryWithLogicalExpire(
CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);