1.为什么要用缓存服务器
在单体架构的项目中当客户端发送请求给tomcat,tomcat做请求的转发,连接数据库,数据库操作硬盘读写的操作,导致数据造成的压力大部分都在数据库这边。
如果查一个数据,假设数据库要1秒返回结果,在加上网络传输的时间,那么整个响应的时间就要1秒以上。在这1秒的时间之内,tomcat有一个内存区域用来应对请求的处理,这个时候必然有一个线程是阻塞在这个请求之上的,只要操作数据库的线程请求没有解决掉,之后的线程是没法做别的事情的,当这个有大量的数据请求过来之后,tomcat内存区域就要开更多的线程资源来处理这些请求。为了保证tomcat不会挂掉这个时候我们可以采用集群。因为tomcat的并发量是于单位时间内越快的请求处理就能处理的愈多的请求,所以在单tomcat请求的速度和我们当前请求的响应时间是很有关系的。但是本身来讲我们追求高并发一定是从某一个维度去思考,我们在保证集群的情况下还能尽可能的提高每一个的tomcat的速度,那么在同等规模的集群下能处理的请求更多,所以我们要考虑单台机器最优的情况才考虑集群要不然会浪费大量的资源。
2.怎么用缓存服务器
首先客户端发送一个请求来获取某个数据,这个时候tomcat先去redis检查有没有数据,如果有tomcat直接从redis拿数据。这个时候和数据库就没有关系了,如果redis没有数据,那么tomcat再请求数据库,请求数据库之后并不会马上返回,而是把数据库的数据放到redis缓存重建,缓存完之后再返回到tomcat。但是缓存是不可能所有数据全部缓存到redis里面去,本身来说redis和数据库的量集都不一样,数据库的数据是放在硬盘上的,redis是放在内存上的。如果redis在这个时候使用集群的目的是累加存储,成本就太高了。所以我们要构建多级的缓存尽可能的从上级缓存依次往下找,如果都没有才去硬盘找。redis是多级缓存中最重要的一级,因为他是一个独立的服务,他能够做的事情最多。而且和所有的缓存都不冲突。
2.1.@Cacheable
- 通常用于查询业务,先去缓存服务器查询,如果有结果直接返回,如果没有,再调用目标方法执行业务,并且将目标方法的返回值重建到缓存中 -查询所有
2.1.1-查询单个,这个key就不能固定了要不然会覆盖 ,还可以在添加一个条件判断condition,这个条件判断的属性类参数里一定要有。因为这个注解是在执行方法执行的
2.2 -@CachePut
- 标记了该注解的方法,一定会被执行,将方法的返回值重建到缓存中,多用于添加的业务。条件判断condition,这个条件判断的属性可以用返回值
@CacheEvict- 表示删除某个缓存,多用于新增、修改、删除的业务 因为线程安全问题修改数据库,删除缓存 - 相对来说出问题的概率较小
2.3@Caching
- 以上3个注解的数组集合体
因为不仅要删除学生缓存还要删除学生列表缓存
3.应对数据高速读写的业务
假设一个下单的操作,对于tomcat来讲,这是一个订单服务,那就要有几个步骤1.查询数据库库存 2. 判断库存 3. 足够–修改数据库的库存 4. 生成订单
这个时候不仅要用redis缓存数据来减少数据库读写的压力,而且还要保证线程的安全。如果对整个业务加把锁,性能就太差了。在订单业务刚开始的时候,mysql先把数据库的库存先写到redis里面去,当订单服务接收请求的时候请求的读写全部到redis操作,当请求完之后再把数据返回到数据库。
redis的内存化操作是异步的,因为他的持久化是异步的。我们所谓的数据安全并不是当前操作一定成功而是操作失败客户一定要知道,怕就怕客户收到的消息的成功的,操作是失败的。这个时候也不能加锁,
4.作为分布式锁使用
在应对数据高速读写的业务的时候,客户端发送一个请求过来,怎么保证业务的线程安全。对这个业务加把锁对于分布式的环境下来说是不可靠的。如果tomcat是一个集群这个锁就没有意义了,因为tomcat里面的锁,锁不住其他的tomcat的业务,如果锁的住其他tomcat假如果有人恶意的给一个tomcat加了一把锁,那全世界都要等这把锁释放,那全世界不是乱套了嘛。所以业务加锁一般都是在redis加锁,因为redis是单线程的他可以保证你只有一个业务能加锁成功。他的实现是代码写在程序里执行在redis里。
作为一个工具类
@Component
public class LockUtil {
@Autowired
private StringRedisTemplate redisTemplate;
//加锁的lua脚本
private String lockLua = "--锁的名称\n" +
"local lockName=KEYS[1]\n" +
"--锁的value\n" +
"local lockValue=ARGV[1]\n" +
"--过期时间 秒\n" +
"local timeout=tonumber(ARGV[2])\n" +
"--尝试进行加锁\n" +
"local flag=redis.call('setnx', lockName, lockValue)\n" +
"--判断是否获得锁\n" +
"if flag==1 then\n" +
"--获得分布式锁,设置过期时间\n" +
"redis.call('expire', lockName, timeout)\n" +
"end\n" +
"--返回标识\n" +
"return flag ";
//解锁的lua脚本
private String unLockLua = "--锁的名称\n" +
"local lockName=KEYS[1]\n" +
"--锁的value\n" +
"local lockValue=ARGV[1]\n" +
"--判断锁是否存在,以及锁的内容是否为自己加的\n" +
"local value=redis.call('get', lockName)\n" +
"--判断是否相同\n" +
"if value == lockValue then\n" +
" redis.call('del', lockName)\n" +
" return 1\n" +
"end\n" +
"return 0";
private ThreadLocal<String> tokens = new ThreadLocal<>();
/**
* 加锁
* @return
*/
public void lock(String lockName){
lock(lockName, 30);
}
public void lock(String lockName, Integer timeout){
String token = UUID.randomUUID().toString();
//设置给threadLocal
tokens.set(token);
//分布式锁 - 加锁,锁的value不写在参数里
Long flag = (Long) redisTemplate.execute(new DefaultRedisScript(lockLua, Long.class),
//快速获得list的做法
Collections.singletonList(lockName),
token, timeout + ""
);
System.out.println("获得锁的结果:" + flag);
//设置锁的自旋
if (flag == 0) {
//未获得锁
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock(lockName, timeout);
}
}
/**
* 解锁
* @return
*/
public boolean unlock(String lockName){
//获得ThreadLocal
String token = tokens.get();
//解锁
Long result = (Long) redisTemplate.execute(new DefaultRedisScript(unLockLua, Long.class),
Collections.singletonList(lockName),
token);//token要获得加锁的但是我们不能把token设置成全局的,这样所有的锁都叫token,那我要加锁的时候
//有特有的token,因为加锁和解锁都是在同一个线程执行,同一个线程传东西可以用ThreadLocal
System.out.println("删除锁的结果:" + result);
return result == 1;
}
}
5.redis集群
在应对数据高速读写的业务的时候,读写都在redis对当redis来说压力也大,但是可以用集群来解决问题。当redis集群的时候,互联网项目又适合用读写分离,那么一个redis服务用来读,列一个redis服务是用来写。如果他们之间没有联系就会读不到数据,这个时候redis有一个主从复制,主叫maser这个redis主要用来写当然他可以读,从的redis叫slave他只能用来读,而且读的压力大于写,因为一个订单业务读的比较多。写只有一台master的redis,这样就形成读就算全挂了业务也可以进行下去,因为master可以读和写。但是master挂了,哨兵就会选一台slave的redis的服务当master,哨兵是依靠心跳机制来确认redis有没有挂,而且哨兵在redis服务里面是一个进程。搭建好了主从复制,还要实现读写分离,也就是说现在程序不会去连redis连的是哨兵把命令发给哨兵,于稍兵决定把命令发给哪个redis。但是这个时候哨兵挂了怎么办,这个时候就可以哨兵集群,哨兵集群有一个特点,哨兵两两之间会相互监控然后同时每个稍兵还会去监控master和slave,理论上只要还有一个哨兵存活我当前的集群就还是可以用的。
但是这样又有一个问题,我一个稍兵得配置多少地址不仅要监控所有redis还要监控其他的稍兵,维护成本太高了,我们可以把所有的稍兵和所有的slave只配置master的地址这个时候master就有点类型于注册中心的意思了,当master挂了假如有一台哨兵监测到了,他并不会马上去主从切换而是要等其他的的哨兵监测到mater挂了,然后他们在主从切换,他们之间内部是会需要一个算法投票的把那台slave切换成主机。
Redis集群结构
13.1 主从集群
主从复制:
读写分离:
哨兵:主从故障切换、请求读写分离
使用docker搭建Redis主从复制集群(6个容器、3个redis、3个哨兵)
1)准备配置文件和文件夹的路径
2)编写docker-compose.yml
version: "3.1"
services:
master:
image: redis:5
container_name: master
restart: always
network_mode: host
volumes:
- ./redis_master/conf/redis.conf:/etc/redis/redis.conf
- ./redis_master/data:/data
command:
['redis-server', '/etc/redis/redis.conf']
slave1:
image: redis:5
container_name: slave1
restart: always
network_mode: host
volumes:
- ./redis_slave1/conf/redis.conf:/etc/redis/redis.conf
- ./redis_slave1/data:/data
command:
['redis-server', '/etc/redis/redis.conf']
slave2:
image: redis:5
container_name: slave2
restart: always
network_mode: host
volumes:
- ./redis_slave2/conf/redis.conf:/etc/redis/redis.conf
- ./redis_slave2/data:/data
command:
['redis-server', '/etc/redis/redis.conf']
3)配置slave从机的redis.conf
replicaof 192.168.195.188 6379
主从复制搭建完成
4)搭建哨兵模式(docker-compose.yml)
sentinel1:
image: redis:5
container_name: sentinel1
restart: always
network_mode: host
volumes:
- ./redis_sentinel1/conf/sentinel.conf:/etc/redis/sentinel.conf
- ./redis_sentinel1/data:/data
command:
['redis-sentinel', '/etc/redis/sentinel.conf']
sentinel2:
image: redis:5
container_name: sentinel2
restart: always
network_mode: host
volumes:
- ./redis_sentinel2/conf/sentinel.conf:/etc/redis/sentinel.conf
- ./redis_sentinel2/data:/data
command:
['redis-sentinel', '/etc/redis/sentinel.conf']
sentinel3:
image: redis:5
container_name: sentinel3
restart: always
network_mode: host
volumes:
- ./redis_sentinel3/conf/sentinel.conf:/etc/redis/sentinel.conf
- ./redis_sentinel3/data:/data
command:
['redis-sentinel', '/etc/redis/sentinel.conf']
5)配置哨兵的sentinel.conf
sentinel monitor mymaster 192.168.195.188 6379 2
sentinel down-after-milliseconds mymaster 10000 #多久未收到master的心跳,认为master下线
6)SpringBoot连接哨兵,进行读写分离
Spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.195.188:26379
- 192.168.195.188:26380
- 192.168.195.188:26381