分布式缓存架构-Redis(一)
什么是Redis
Redis是由意大利人Salvatore Sanfilippo(网名:antirez)开发的一款内存高速缓存数据库。
Redis全称为:Remote Dictionary Server(远程数据服务),该软件使用C语言编写,Redis是一个key-value存储系统,它支持丰富的数据类型,如:string、list、set、zset(sorted set)、hash。
Redis采用的是基于内存的采用的是单进程***单线程模型***的KV数据库,由C语言编写。官方提供的数据是可以达到100000+的qps
Redis单线程好处
代码更清晰,处理逻辑更简单
不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
不存在多进程或者多线程导致的切换而消耗CPU
所以redis线程是安全的
单进程单线程弊端
无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
Redis应用场景
1. 令牌(Token)生成
2. 短信验证码
3. 发布订阅
相当于消息系统,ActiveMQ,RocketMQ、Kafka等工具类似,但是个人觉得简单用一下还行,如果对于数据一致性要求高的话还是用Kafka等专业系统。
由于redis把数据添加到队列是返回添加元素在队列的第几位,所以可以做判断用户是第几个访问这种业务
队列不仅可以把并发请求变成串行,并且还可以做队列或者栈使用
4. 分布式锁
验证前端的重复请求(可以自由扩展类似情况),可以通过redis进行过滤:每次请求将request Ip、参数、接口等hash作为key存储redis(幂等性请求),设置多长时间有效期,然后下次请求过来的时候先在redis中检索有没有这个key,进而验证是不是一定时间内过来的重复提交
秒杀系统,基于redis是单线程特征,防止出现数据库“爆破”
全局增量ID生成,类似“秒杀”
5. 计数器
诸如统计点击数等应用。由于单线程,可以避免并发问题,保证不会出错,而且100%毫秒级性能!
6. 缓存(热点数据)
热点数据(经常会被查询,但是不经常被修改或者删除的数据),首选是使用redis缓存,毕竟强大到冒泡的QPS和极强的稳定性不是所有类似工具都有的,而且相比于memcached还提供了丰富的数据类型可以使用,另外,内存中的数据也提供了AOF和RDB等持久化机制可以选择,要冷、热的还是忽冷忽热的都可选。
结合具体应用需要注意一下:很多人用spring的AOP来构建redis缓存的自动生产和清除,过程可能如下:
Select 数据库前查询redis,有的话使用redis数据,放弃select 数据库,没有的话,select 数据库,然后将数据插入redis
update或者delete数据库钱,查询redis是否存在该数据,存在的话先删除redis中数据,然后再update或者delete数据库中的数据
上面这种操作,如果并发量很小的情况下基本没问题,但是高并发的情况请注意下面场景:
为了update先删掉了redis中的该数据,这时候另一个线程执行查询,发现redis中没有,瞬间执行了查询SQL,并且插入到redis中一条数据,回到刚才那个update语句,这个悲催的线程压根不知道刚才那个该死的select线程犯了一个弥天大错!于是这个redis中的错误数据就永远的存在了下去,直到下一个update或者delete。
安装Redis
1 windows安装
http://wiki.xuetang9.com/?p=7483
2 linux安装
环境准备
vm虚拟机或linux服务器,下面是虚拟机安装教程
虚拟机安装:http://wiki.xuetang9.com/?p=7640
centos安装:http://wiki.xuetang9.com/?p=7649
下载jdk1.8:https://www.injdk.cn/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dq3pecLg-1607954878040)(img\image-20200710052102891.png)]
安装jdk
将安装包上传到linux服务器
rpm -qa|grep jdk
如果已经存在,那么可以执行卸载命令
yum -y remove jdk1.8-1.8.0_251-fcs.x86_64
然后执行rpm安装命令
rpm -ivh jdk-8u251-linux-x64.rpm
安装完成查看jdk版本
java -version
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlw0LLbx-1607954878042)(img\image-20200710053310675.png)]
配置环境变量
vi /etc/proflie
在文件末尾追加下面的的配置
# JDK 1.8
export JAVA_HOME=/usr/java/jdk1.8.0_251-amd64
export PATH=$PATH:$JAVA_HOME/bin
安装redis
下载最新版redis:https://redis.io/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvEl844Q-1607954878044)(img\image-20200710055012958.png)]
将安装包上传到linux服务器
安装gcc编译器,centos7 默认的 gcc 版本为:4.8.5 < 5.3 无法编译
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
# 临时有效,退出 shell 或重启会恢复原 gcc 版本
scl enable devtoolset-9 bash
# 长期有效
# echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile
解压redis安装包
tar -zxvf redis-6.0.5.tar.gz
编译
cd redis-6.0.5
make
编译成功后
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQotOGCu-1607954878048)(img\image-20200710055929871.png)]
运行redis服务器
./src/redis-server
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzCpOkEG-1607954878053)(img\image-20200710060124580.png)]
修改redis.conf文件
vi redis.conf
修改关键点
#设置后端启动
daemonize yes
#运行远程连接需要调整
#bind 127.0.0.1
#关闭密码验证
protected-mode no
如果想要远程访问,还需要开启防火墙
#查看是否开放端口
firewall-cmd --list-port
firewall-cmd --add-port=6379/tcp --permanent
firewall-cmd --reload
设置开机启动
vi /etc/systemd/system/redis-server.service
输入内容
[Unit]
Description=redis-server
After=network.tartget
[Service]
Type=forking
ExecStart=/usr/local/redis/redis-6.0.5/src/redis-server /usr/local/redis/redis-6.0.5/redis.conf
PrivateTmp=true
[Install]
WantedBy=multi-user.target
启动服务器与开机启动
# 使服务自动运行
systemctl daemon-reload
systemctl enable redis-server
# 启动服务
systemctl start redis-server
systemctl status redis-server
# 停止与重启
systemctl stop redis-server
systemctl restart redis-server
基本数据类型
Redis目前支持6种数据类型,分别是:
String(字符串)
List(列表)
Hash(字典)
Set(集合)
Sorted Set(有序集合)
Steam(日志数据结构)Redis 5加入的数据结构
String(字符串)
String是简单的 key-value 键值对,value 不仅可以是 String,也可以是数字。String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。
# 新增或修改指定key的值
set key val
# 获取指定key的值
get key
# 删除指定key的值
del key
应用场景:
String是最常用的一种数据类型,普通的key/value存储都可以归为此类,这里就不所做解释了。
List(列表)
Redis列表是简单的字符串列表,可以类比到C++中的std::list,简单的说就是一个链表或者说是一个队列。可以从头部或尾部向Redis列表添加元素。列表的最大长度为2^32 - 1,也即每个列表支持超过40亿个元素。
Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
# 从头部开始添加至
lpush key v1 v2 ....
# 从尾部添加
rpush key v1 v2 ...
# 查看列表
lrange key start end (start从0开始,负数表示到链表尾部的位置,-1链表尾部,-2尾部倒数第二)
# 删除链表第一个元素
lpop key
# 删除链表倒数第一个元素
rpop key
# 获取链表元素个数
llen key
# 扩展命令
lpushx key value
rpushx key value
# 仅当参数中指定的key存在时,向关联的list的头部或尾部插入value。如果不存在,将不进行插入。
应用场景
Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表、粉丝列表等都可以用Redis的list结构来实现,再比如有的应用使用Redis的list类型实现一个简单的轻量级消息队列,生产者push,消费者pop/bpop。
Hash(字典)
Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
# 添加或修改指定key
hset key field val
# 设置多个key
hmset key f1 v1 f2 v2
# 获取值
hget key field
# 获取多个值
hmget key f1 f2
# 删除指定值
hdel key f1 f2
# 删除整个列表
del key
Set(集合)
Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
# 添加删除元素
sadd key values[v1,v2,...]
srem key member[m1,m2,...]
# 获取set中所有成员
smembers key
# 求差集合(A、B两个集合,获取属于A但是B中没有的元素)
sdiff A B
# 求交集(A、B两个集合,AB两个集合都有的元素)
sinter A B
# 求并集
sunion A B
Sorted Set(有序集合)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
#添加元素
zadd key score member score2 member2.....
#获得元素,
#获取指定成员对应的分数
zscore key member
#获取集合元素个数
zcard key
#删除元素
zrem key mem....
#范围查询
#获取集合中脚标为start-end的成员,[withscores]参数表明返回的成员包含其分数。
zrange key start stop [withscores]
#照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
zrevrange key start stop [withscores]
#按照排名范围删除元素
zremrangebyrank key start stop
#按照分数范围删除元素
zremrangebyscore key min max
Stream(日志结构数据类型)
适用于消息队列和时间序列存储
# 追加消息
XADD key ID field string [field string ...]
# ID可以自己指定,例如0-1, 0-2等等,*表示使用时间戳做ID
# 消息长度
XLEN key
# 则是范围查找的命令
XRANGE key start end [COUNT count]
XREVRANGE key end start [COUNT count]
# 可选项COUNT是用来做达到多少数量消息之后就停止查找的
# 关于start和end
# `-`和`+` 分别代表最小和最大,或者说最老和最新的消息
# - start和end可以使用时间戳,而不加上后面的自增ID部分来查询,例如 `XRANGE mystream 1518951480106 1518951480107`
# 删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
xdel key ID [ID ...]
# 删除整个stream
del key
# 读取消息
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
# BLOCK是阻塞读,如果填0,则是一直到有消息,否则都是阻塞。
# STREAMS后原本写ID的地方,如果使用 $ 则是代表最新的消息的ID,设想,如果你加入一个群聊,不看历史消息,但是从你加入之后的消息都能读到,该怎么做?这种时候就可以用这个了。
更多命令
官方文档:https://redis.io/commands
中文参考:http://www.redis.cn/commands.html
归类整理参考:http://redisdoc.com/string/index.html
SrpingBoot使用Redis
Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件
#配置redis连接
#选择数据库,默认值为0
spring.redis.database=0
#主机地址,默认localhost
spring.redis.host=localhost
#端口号,默认6379
spring.redis.port=6379
#密码
spring.redis.password=
#请求超时时间
spring.redis.timeout=0
#配置jedis连接池
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
代码中使用
RedisTemplate
SpringBoot内置实现的RedisAPI
用于操作对象的key-value
使用方式:直接注入,然后调用opsForXXX方法
@Autowired
RedisTemplate redisTemplate;
@Test
void testAddObject() {
User u = new User();
u.setId(1);
u.setName("Naaman");
u.setZip("65000");
u.setAddress("成都锦江");
redisTemplate.opsForValue().set("u1", u);
}
@Test
void testQueryObjec(){
System.out.println(redisTemplate.opsForValue().get("u1"));
}
StringRedisTemplate
继承自RedisTemplate,重新实现了序列化策略,使用StringRedisSerialier类来序列化key-value,包括List、Hash、Set等数据结构。
操作String类型数据(不是说只能操作redis中的string类型,而是存储数据是string,key也为string的数据)
使用方式:直接注入,然后调用opsForXXX方法
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void testAddHash(){
String key = "users";
User u = new User();
u.setId(1);
u.setName("Naaman");
u.setZip("65000");
u.setAddress("成都锦江");
stringRedisTemplate.opsForHash().put(key,"u1",u.toString());
stringRedisTemplate.opsForHash().put(key,"u2","u2");
}
@Test
void testQueryHash(){
String key = "users";
System.out.println(stringRedisTemplate.opsForHash().get(key,"u1"));
}
Cache
参考地址
https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
从3.1开始,Spring引入了对Cache的支持。其使用方法和原理都类似于Spring对事务管理的支持。
其核心思想是这样的:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。
所以在使用Spring Cache的时候我们要保证我们缓存的方法对于相同的方法参数要有相同的返回结果。
和Spring对事务管理的支持一样,Spring对Cache的支持也有基于注解和基于XML配置两种方式。这里我们会简单介绍注解方式使用。
@EnableCaching
在Application类注入,表示启用缓存支持
@Cacheable
可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。
可以指定三个属性,value、key和condition
- value:属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的。
- key:属性是用来指定Spring缓存方法的返回结果时对应的key的。
- condition:属性指定发生的条件。condition属性默认为空,表示将缓存所有的调用情形。
@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
@CachePut
也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。
与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中
@CachePut(value={"users"}, key="#user.id", condition="#user.id%2==0")
@CacheEvict
用来标注在需要清除缓存元素的方法或类上的。
当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
可以指定的属性有value、key、condition、allEntries和beforeInvocation。
其中value、key和condition的语义与@Cacheable对应的属性类似。
- allEntries:是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。
- beforeInvocation:可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。
@CacheEvict(value=“users”, beforeInvocation=true, allEntries=true)
@Caching
@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。
其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。
@Caching(
cacheable = @Cacheable("users"),
evict = {
@CacheEvict("cache2"),
@CacheEvict(value = "cache3", allEntries = true)
})
@CacheConfig
缓存配置注解,注解到类上面,表示本类的全局配置
@Service
@CacheConfig(cacheNames = "user")
public class UserServiceImpl implements UserService {
@Override
@Cacheable(key = "#id")
public User findUserById(int id) {
return userMapper.findUserById(id);
}
@Override
@CachePut(key = "#user.id", condition = "#result != null ")
public User updateUser(User user) {
if (userMapper.updateUser(user) == 1) {
return user;
}
return null;
}
@Override
@CacheEvict(key = "#id",condition = "#result")
public boolean deleteById(int id) {
return userMapper.deleteById(id) == 1;
}
}