Redis
原创笔记
nosql基本介绍:
作为关系数据库的补充
应用海量用户和海量数据前提下的数据处理问题
nosql应用方案:
redis简介:
本地缓存:
java里面使用map作为本地缓存,存在分布式的数据不一致
如果是单体应用,效率很高.但是不能使用分布式场景,所以推出了redis
特征:
redis是单线程的
数据储存在内存中
qps 10W ,跨网络肯定还拿不到
主要存储热数据,不会存储所有数据
最忌讳key对应的value太大
原则上redis只做数据的存储与提供
所有的配置都是在redis.conf文件中
应用:
热点数据加速场景
时效信息(验证码,投票)
分布式数据共享(session控制)
使用这类缓存中间件主要就两点依据
1.不要求强一致性,即时性
2.访问量大,但是更新频率不高
简单使用总结
data=cache.load(id)
if(data==null)
{
data=db.load(id);
cache.put(id,data);
}
从这些redis应用,经过不断总结,推出了redis现在的数据类型
其实所有数据类型都是字符串的拓展
redis操作成功时,以int(1)展示,操作失败时,展示int(0)
如果数据量比较大,推荐使用mset,mget
redis主要文件
redis-server
redis-cli
命名方式:
使用冒号进行分隔
表名:主键名:主键值:字段名
如果使用hash,那么如下表示
表名:主键名:主键值
例如:
user:id:00789:fans
对象存储方式:
数据库表中的一条记录,redis如何表示
单值存储
便于修改每个数据
set user:id:789:fans 1234
set uesr:id:789:blogs 53
或者
mset user:id:789:fans 1234 uesr:id:789:blogs 53
json存储
对象如果频繁更新显示笨重
set user:id:789 {id:1234, blogs:53}
这两种没有优略,根据使用场景选择
map存储
hset user:id:789 {id:1234, blogs:53}
string(json)与hash分析
json一个整体,需要整体更新,以读为主
hash可以用field隔离,更新比较灵活
命令行基本操作:
key通用操作:
del key 删除key
exists key 判断key是否存在
type key 获取key对应的value类型
expire key seconds 设置key的过期时间
pexpire key mills 毫秒
ttl 查看key的有效期 (有效时长,-2 key不存在, -1key存在)
pttl 查看有效期毫秒
persist key 将key转为永久性
keys pattern(* 匹配所有符号任意数量, ? 匹配任意一个符号)
keys user:? 查询所有user: 开头的
rename key newkey 重命名
内部数据库:
key重复问题,随着业务增加,key及其容易冲突
redis为每个服务提供了16个数据库,编号为0-15,每个数据库之间相互独立
select index 切换数据库
ping 测试连接性
move key db 移动key到某个数据库
dbsize 查看数据总量
flushdb 删除改数据库
flushall 清除所有数据
string:
如果字符串以整数形式展示,可以作为数字操作使用
set key value
get key
mset 批量操作
strlen 获取数据的字符长度
append key value 追加数据到现在数据后面(如果不存在,就创建)
setex key seconds value 设置数据的过期时间
psetex key milliseonds value 设置毫秒
(注意:setex后,再次set过期时间会被覆盖)
setnx 分布式锁 set if not exist
如果key已经存在,则不做任何动作
原子操作
注意:如果值不能转为整数,或者超出long类型,会报错
incr key 数值+1
decr key 减1
incrby key v 数值 + v(v可以设置负数)
decrby key v 数字 -v
秒杀操作的话,只有一条命令能够执行成功
例如:
setnx product:10001 true
setnx proudct:10001 false 取锁失败
//执行业务操作
del proudct:10001 执行完毕,释放锁
应用场景:
web 集群session共享
文章访问计数器
主键控制:
数据库分表后,可以使用string维护主键
批量获取id,放在内存里面(redis天然单线程)
适用于任何类型数据库,支持数据库集群
或者使用雪花算法.
hash:
相当于双层map
key-field-value
注意:这里面的value不能嵌套,只能存储字符串
不要滥用hash存储对象
操作就是string的操作加一个h
hset key field value
hget key field
hgetall 获取所有field和value(如果field非常多,会成为性能瓶颈)
hdel key field 删除field
hmset
hmget
hlen key 获得field的数量
hexists key field 判断field是否存在
hkeys key 获得所有字段名
kvals key 获得所有值(有可能重复)
原子操作:
注意事项同string
hash没有其他的hincr,hdecr等操作
hincrby key field v 值增加指定数量
hincrbyfloat key field v 值增加小数
典型场景:
购物车:
仅讨论购物车的redis模型,不讨论持久化等其他因素
以用户id为key
商品id为field
商品数量为value
添加购物车
hset cart:id:1001 g10088 5
增加商品
hincrby cart:id:1001 g10088 1
商品个数
hlen cart:id:1001
删除商品:
hdel cart:id:1001 g10088
查看购物车
hgetall cart:id:1001
但是存在缺陷,不知道商品的名称
所以把商品的描述信息存为另一个field
把命名格式改为商品id:nums ,商品id:info
hmset cart:id:1001 g10088:nums 5 g10088:info {...}
但是这样还是存在商品名称和描述的大量重复,可以把描述抽取成另外一个string
hset cart:id:1001 g10088:nums 5
hset goods:id:g10088 info {...}
限购秒杀:
hash可以用于限购,限量发放优惠券,秒杀等等场景.
list:
可以实现栈,队列,阻塞队列,底部使用双向链表实现
可以解决分布式数据一致性
能够存储大量数据
使用就是string的命令加上l
注意:push和pop的位置关系
list有索引的概念,但是通常以push和pop为主
lpush 插入最左边,可以同时插入多个value
rpush 插入最右边,可以同时插入多个value
lpop
rpop
blpop key1 key2 timeout(秒) 左边弹出一个元素,若没有元素,阻塞等待一段时间
brpop
任务队列基础
可以从多个队列取出,结果会告诉你从哪个队列取出
lrem key count value(注意这里是元素的值) 移除元素值为value
llen key 返回数据个数
lindex key index 返回第index项数据
lrange key start stop 返回[start,stop]的数据
例如:
lrange msg 0 -1 返回全部的数据
使用场景:
1.微博,微信等消息流
一般粉丝数量可以这样子,给每个粉丝推送,实现一条命令
lpush msg:用户id 消息id
lpush msg:用户id 消息id
lrange msg:用户id 0 4
2.朋友圈点赞
rpush friend:id:xx
3.使用队列模型解决多路信息合并(比如说:日志合并)
最新消息:
4.使用栈模型解决最新消息
set:
新的存储结构,便于查询(list的遍历速度太慢)
从hash变化而来,与hash结构完全相同,不过仅储存键
sadd key value value2 批量添加
srem key value value2 批量删除
srembers key 查看所有元素
scard key 获取数据个数
sismember key value 查看是否包含元素
sinter key1 key2 交集
sunion key1 key2 并集
sdiff key1 key2 差集
sinterstore des key1 key2 求交集并保存
sunionstore des key1 key2
sdiffstore des key1 key2
smove source des member 移动元素到另一个集合
srandmember key count 随机获取指定数量的数据(集合元素不变)
spop key count 随机弹出指定数量的元素(集合改变)
应用场景:
随机推荐:
set可以应用于随机推荐类信息检索,例如热点歌单推荐,热点新闻推荐,热点旅游线路,大V等等
抽奖小程序
sadd key usrid
smembers key 查看所有参与的用户
srandmember key 2 抽奖数量 (但是这种方式不能实现多等奖)
微信收藏,点赞
sadd like 用户id
srem like 用户id
sismember lilke 用户id 用户是否点赞
smembers like 获取点赞列表(不行的,每个用户点赞列表不一样)
只有发消息的人能够看见
关注模型:
set可以还用于同类信息的关联搜索,二度关联,深度关联搜索
例如:显示共同关注(一度),共同好友(一度)
由A,获取B的购物清单,游戏充值列表(二度)
突然你想要买的东西给你推荐了.
后端基于大数据给你的推荐,其中一种就是基于关注模型
使用交集运算查看共同关注的人
使用ismember查看某人的关注列表是否包含
使用差集运算 可能认识的人
访问统计:
set还可以用于统计访问量, PV(访问量),UV(独立访客),IP(独立IP)
PV:访问次数,可以刷新增加
UV:统计cookie
IP:统计ip
zset:
需要一种能够排序的数据类型
在set的基础上添加了score
有序集合,每个元素带有分值
score也可以是一个小数,可能会丢失精度
zadd key score member score2 member2
zrem key member1 member2
zrange key start stop [withscores] (是否显示分数) 从小到大显示members
zrevrange key start stop 倒序获取元素start 到stop [start,stop]
zcard key 查询数据总数
zincrby key inc member 为member的分数+inc
zrank key member 查看具体排名(索引,从0开始)
zrevrank key member 从大到小
zscore key member 获取score
zincrby key incr member score增加
区间查询
zrangebyscore key min max [withscores] [limit x x]
zcount key min max
区间删除
zremrangebyscore key min max 按值区间删除
zremrangebyrank key start stop 按索引删除
集合操作
交集,并集
合并时,会对score进行一些操作(sum,min,max等等)
应用场景:
点击新闻
zincrby hotnews:2019 1 守护香港
排行榜:
sorted_sset主要作用就是用来排序
日榜前十
zrevrange hostnew:2019 0 9 wtihscores
两年榜单
zunion host2:all 2 hostnew:2019 hostnew:2020
定时任务:
比如网站会员,游戏会员,定时投票等等
sorder_set把每一个时间戳作为score,不在set里面不是vip
按照时间进行排序,每次移除下一个要处理的时间
分布式锁:
下面以一个秒杀案例,来引入锁
这里必须要加锁,要保证只有一个人能够先抢到
不加锁就会存在混乱
所以这里使用了synchronized为代表的本地锁
本地锁:
@RestController
public class ProController
{
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/user")
public String deductStock()
{
synchronized (this)
{
int stock =Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock >0)
{
int realStock=stock-1;
redisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功"+realStock);
}
else{
System.out.println("库存不足");
}
}
return "end";
}
}
还是存在问题的,本地锁无法控制多台服务器的线程
就是说我开启了两个端口程序,每个程序的synchronized只能控制自己的多线程.
那么还是会取到同一个数据
这个案例用mysql可以实现,redis也可以实现
入门分布式锁:
setnx ("lockkey",true)
在spring里面setIfAbsent
@RestController
public class ProController
{
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/user")
public String deductStock()
{
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock_key", "true");
if(!absent)
{
return "error_code";
}
int stock =Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock >0)
{
int realStock=stock-1;
redisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功"+realStock);
}
else{
System.out.println("库存不足");
}
redisTemplate.delete("lock_key");
return "end";
}
}
问题:如果存在断电,宕机等情况,不能及时删除锁,就会发生死锁问题
改进版分布式锁:
需要设置锁的过期时间,而且保证为原子操作
setnxes
spring里面为setIfAbsent("lock_key", "true",10, TimeUnit.SECONDS);
而且需要捕获异常(避免出现异常而不删除锁)
@RestController
public class ProController
{
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/user")
public String deductStock()
{
try
{
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock_key", "true",10, TimeUnit.SECONDS);
if(!absent)
{
return "error_code";
}
int stock =Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock >0)
{
int realStock=stock-1;
redisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功"+realStock);
}
else{
System.out.println("库存不足");
}
}
finally
{
redisTemplate.delete("lock_key");
}
return "end";
}
}
问题:如果锁已经过期了,别人设置了锁,那么我们就删除了别人的锁
高级版分布式锁:
每个人使用自己的UUID作为标识,这样就不会删除别人的锁
@RestController
public class ProController
{
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/user")
public String deductStock()
{
String uuid =UUID.randomUUID().toString();
try
{
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock_key", uuid,10, TimeUnit.SECONDS);
if(!absent)
{
return "error_code";
}
int stock =Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock >0)
{
int realStock=stock-1;
redisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功"+realStock);
}
else{
System.out.println("库存不足");
}
}
finally
{
if(uuid.equals(redisTemplate.opsForValue().get("lock_key")))
{
redisTemplate.delete("lock_key");
}
}
return "end";
}
}
问题:uuid正确,准备删除锁时,锁过期被别人设置,这样又删除了别人的锁
核心问题: 加锁设置过期时间必须为原子操作,获取锁,删除锁也必须为原子操作
Lua版分布式锁:
使用lua脚本对删除锁进行原子化
@RestController
public class ProController
{
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/user")
public String deductStock()
{
String uuid =UUID.randomUUID().toString();
try
{
Boolean absent = redisTemplate.opsForValue().setIfAbsent("lock_key", uuid,10, TimeUnit.SECONDS);
if(!absent)
{
return "error_code";
}
int stock =Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock >0)
{
int realStock=stock-1;
redisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功"+realStock);
}
else{
System.out.println("库存不足");
}
}
finally
{
String script ="if redis.call('get',KEYS[1]==ARGV[1] then return redis.call('del',KEYS[1])) else return 0 end";
Long lock1=redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class ),Arrays.asList("lock_key"),uuid);
}
return "end";
}
}
问题:
不设置销毁时间,程序异常终止时,这个锁一直存在,所有程序都无法运行了
可是无论设置多少时间,还会出现问题
1.程序运行太长,锁自动销毁后,其他线程又拿到锁,不断重复,相当于没有锁
2.锁的时间太短,业务还没结束就被删除了
Redisson:
由于锁的时间难以确定,所以出现了另外一个线程来动态监控锁.
如果锁30s,另外一个线程每隔10s查看锁定状态(10s就是1/3的过期时间)
这样子,就能实现动态删除锁
但是代码实现比较复杂,所以有些框架就是这个原理
简介:
redission就是一个redis的java实现框架
比较安全,可以放心使用
redisson牛逼的地方在于实现了java的Lock接口,所以如果学习过javaLock,可以无缝衔接上redisson
而且他把java里面本地锁全部改成了分布式锁,可以超级高效使用
简单示例,具体见spring Data
@RestController
public class ProController
{
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping("/user")
public String deductStock()
{
Config config=new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
Redisson redisson= (Redisson) Redisson.create(config);
String lockKey="Product001";
RLock lock=redisson.getLock(lockKey);
try
{
lock.lock();
int stock =Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stock >0)
{
int realStock=stock-1;
redisTemplate.opsForValue().set("stock",realStock+"");
System.out.println("扣减库存成功"+realStock);
}
else{
System.out.println("库存不足");
}
}
finally
{
lock.unlock();
}
return "end";
}
}
底层实现:
源码:
redis底层的lua实现具备原子性
lua会把一大串代码当成原子结构执行
所以redisson底层大量使用lua来解决分布式一致性的问题
lock的具体实现方法
然后循环调用tryAcquire方法
tryAcqurie实际上又调用这个方法
下面是核心代码
执行设置异步锁的lua代码
如果不传参数,就是-1,然后会使用默认30s
还会多执行一个监听器来续锁的持续时间
注意:如果使用lock.lock(10,TimeUnit.SECONDS); 设置时间,就不会自动续期
其他锁:
红锁:
超过半数redis节点加锁成功才算加锁成功(没有主从节点)
性能大大降低(原本redis只需要给主节点加锁)
较少使用
读写锁:
写锁是一个排他锁,读锁是共享锁
写者优先,有线程在写时,其他线程必须等待
读的时候写,写也必须等待
保证了一定能够获取最新的数据
信号量:
和操作系统的信号量是一个东西
有初始值为几, 然后+1 -1
=0 就不能在获取了
可以用于控制流量
闭锁:
设置一个初始值,每次操作初始值-1.只有等初始值为0后, 才可以进行操作
缓存数据一致性:
所谓缓存数据一致性,就是数据库与redis之间的数据是否一致
一般有两种解决方案
1.双写模式
每次更新数据库之后,同时更新redis
问题示例:
2.失效模式
每次更新数据库之后,删除缓存.这样下一个业务进行,就会重新设置缓存
问题示例:
由于网络,主机性能等原因,读数据库与更新缓存之间被另一个线程删除了缓存
上面两个问题都是由于网络因素,主机性能因素等各种原因,导致各个线程之间的前后顺序发生变化,出现数据不一致问题
无论那种解决方案,都还会出现数据不一致问题
• 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加 上过期时间,每隔一段时间触发读的主动更新即可
• 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
• 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
• 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略);
总结:
我们不应该过度设计,增加系统的复杂性.遇到实时性、一致性要求高的数据,就应该查数据库。实时性、一致性要求不高的数据,并且读多写少,就放在缓存并设置过期时间.如果对脏数据容忍度低,就加上读写锁
持久化:
redis服务器关闭后,数据就会丢失,需要持久化机制
持久化操作默认不开启,需要手动开启
两种持久化方式可以同时使用
两种工作方式进行抉择,如果对数据敏感性高,建议使用aof方案
如果可以容忍数据部分丢失,但是追求数据恢复速度快,建议使用rdb
也可以同时开启两种策略作为双保险
持久化需要更具业务需求判断:
1.主键控制,不持久化
2.购物车 ,不持久化
3.限购秒杀,持久化
4.关系模型,不持久化
5.最新消息,持久化
AOF:
append only file
将数据的操作过程保存,日志形式,储存操作过程.储存格式复杂.
解决了一些rdb的弊端,改为对所有操作记录
目前已经解决了实时性,已经是主流使用方式
服务器接受指令时,不会马上执行,而是先放到aof的缓冲区,过一段时间后写入aof文件
配置
appendonly yes # 默认不开启
appendfsync everysec #写策略
aof的三种写策略
1.always
每次操作都同步到aof文件中,数据零误差,性能较低
不建议使用
2.everysec
每秒将缓冲区写入aof.准确率较高,性能较高
建议使用,也是默认配置
最大丢失1s数据
3.no
由操作系统控制,整体过程不可控
aof重写:
随着程序运行,aof不断加大.
后面的命令很有可能覆盖前面的操作,因此redis提出了重写
和bgsave非常像,也是两种重写方式,通过调用fork创建子进程
两种重写方式:
手动bgrewriteaof
自动
auto-aof-rewrite-min-size size
auto-aof-rewirte-percentage percent
RDB:
Redis DataBase
将当前数据状态保存,快照形式,储存数据结果,存储格式简单
默认不保存数据
使用下面三个方式,都会生成dump.rdb
由于rdb使用全量复制方式,比较消耗时间,在此期间的写操作可能丢失数据
优点:存储效率高.恢复速度快;可以适用于灾难恢复
缺点:
1.无法实时持久化,有大可能丢失数据;
2.需要fork创建子进程,消耗cpu性能
3.备份数据量大,IO性能下降
配置:
save:
save 手动保存一次数据
save保存在前台执行,也是一条指令,会阻塞服务器,所以基本不用
bgsave:
后台保存
调用fork函数创建一个后台保存的子进程
由于也是手动操作很少使用
save配置:
save配置也是基于bgsave的原理,不过由redis自动调用
save second changes
示例:
前大后小
每900s 1个值变化就会自动保存(增删改)
save 900 1
save 300 10
save 60 10000
save配置需要根据实际情况进行配置,频度过高和过低都会出现性能问题
second和changes的配置通常具有互补关系,不能设置为不含关系
事务:
类似与mysql中的事务,但是好像有点不一样,说不出来
就是把一批操作作为一个整体使用,全部成功才会提交,否则回滚
这里的事务也是在某个客户端范围有效,其他客户端无效
multi开启事务
exec 结束事务
如果出错,discard终止事务
开启事务后,所有操作会被加入队列,此时不会执行指令
只有等到exec指令才会执行
实际上使用不多
低级事务执行流程
乐观锁:
watch 对象
大家监控某个对象,如果对象不发生改变(其他客户端改变,事务异常退出),后面的事务才能执行成功
必须在开启前watch,仅对紧跟在后面的事务有效,其他事务没有效果
这里的乐观锁和其他语言里的锁是不一样的,其他语言里获取锁,其他人不能操作
而这里获取锁,其他人操作,会导致自己操作失败
分布式锁:
就是排他锁的原理
最简单方案,使用setnx设置分布式锁
setnx key true
del key
没有拿到锁不能操作
但是这个方案,可能存在死锁问题
就像上面讲过的,使用redisson进行分布式事务控制
基本配置:
daemonize yes # 后台启动改成yes
#bind 127.0.0.1 # 注释掉(不限制IP)
protected-mode no # 改成no
logfile "6379.log" #日志文件名称
dir "" #设置日志文件,持久化文件位置
缓存失效问题
缓存穿透:
查询一个一定不存在的数据,缓存没有,数据库也没有.
由于没有将null写入缓存,因此每次查询都需要到数据库,此时就失去了缓存的意义
就好像没有缓存一样,所以称为穿透
解决:设置布隆过滤器
缓存雪崩:
设置相同的过期时间,导入大量缓存在某一个时刻同时失效
请求全部转发到db,db压力过大
解决:给原有的失效时间加上一个随机值,比如1~5分钟
缓存击穿:
热点数据过期时,刚好有大量请求涌入.
所有请求全部转发db
解决: 排他锁
Spring Data
1.导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
2.配置
本地环境可以不配置
spring:
redis:
host: localhost
port: 6379
redisson客户端配置
@Configuration
public class RedissonConfig
{
@Bean
public RedissonClient redissonClient()
{
Config config=new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3.使用
redis
redisTemplate.opsForValue().set("key","value"); //opsForValue操作字符串
redisTemplate.opsForValue().get("key");
redisTemplate.opsForHash()/opsForList()... //操作其他数据结构,都是大同小异
redisson
RLock lock=redisson.getLock("lock"); //获取锁
RLock lock =redisson.getFairyLock("xx") //公平锁
RReadWriteLock lock =redisson.getReadWriteLock("xx") //redisson可以获取不同的锁,但是下面的使用都是相同的
lock.lock(); //阻塞式等待,默认持有锁30s,会自动续期
lock.lock(10,TimeUnit.SECONDS); //阻塞式等待,手动设置时间,锁不会自动续期
lock.unlock()解锁
推荐使用不会自动续期的,手动设置时间
加锁时需要注意粒度问题,锁的名称用来控制粒度.
比如product 和 proudct:11:category 就有本质区别