概述
Redis入门介绍
- 互联网需求的3高
- 高并发,高可扩,高性能
- Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的NoSql(not only sql)数据库
- NoSQL数据库 和 传统数据库 相比的优势
- NoSQL数据库无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。
- 而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段简直就是一个噩梦
- Redis的常用使用场景
- 缓存,毫无疑问这是Redis当今最为人熟知的使用场景。在提升服务器性能方面非常有效;一些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在redis中,因为redis 是放在内存中的可以很高效的访问
- 排行榜,在使用传统的关系型数据库(mysql oracle 等)来做这个事儿,非常的麻烦,而利用Redis的SortSet(有序集合)数据结构能够简单的搞定;
- 计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
- 好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;
- 简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;
- Session共享,以jsp为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。
Redis/Memcache/MongoDB对比
Redis和Memcache
- Redis和Memcache都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等。
- memcache 数据结构单一kv,redis 更丰富一些,还提供 list,set, hash 等数据结构的存储,有效的减少网络 IO 的次数
- 虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘
- 存储数据安全–memcache挂掉后,数据没了(没有持久化机制);redis可以定期保存到磁盘(持久化)
- 灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过RBD或AOF恢复
Redis和MongoDB
- redis和mongodb并不是竞争关系,更多的是一种协作共存的关系。
- mongodb本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑时仍然要不可避免地进行多次查询。
- 这时就需要redis或Memcache这样的内存数据库来作为中间层进行缓存和加速。
- 比如在某些复杂页面的场景中,整个页面的内容如果都从mongodb中查询,可能要几十个查询语句,耗时很长。如果需求允许,则可以把整个页面的对象缓存至redis中,定期更新。这样mongodb和redis就能很好地协作起来
分布式数据库CAP原理
CAP简介
- 传统的关系型数据库事务具备ACID:
- A:原子性
- C:一致性
- I:独立性
- D:持久性
- 分布式数据库的CAP:
- C(Consistency):强一致性
- “all nodes see the same data at the same time”,即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
- A(Availability):高可用性
- 可用性指“Reads and writes always succeed”,即服务一直可用,而且要是正常的响应时间。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
- P(Partition tolerance):分区容错性
- 即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务。
- 分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
- C(Consistency):强一致性
CAP理论
- CAP理论提出就是针对分布式数据库环境的,所以,P这个属性必须容忍它的存在,而且是必须具备的。
- 因为P是必须的,那么我们需要选择的就是A和C。
- 大家知道,在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当P发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
- 选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了C属性)。
- 选择一致性C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了A属性)。
- 最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,你就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一直等到数据同步完成)。
CAP总结
- 分区是常态,不可避免,三者不可共存
- 可用性和一致性是一对冤家
- 一致性高,可用性低
- 一致性低,可用性高
- 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
- CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
- CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
- AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
下载与安装
下载
- redis:www.redis.net.cn/
- 图形工具:redisdesktop.com/download
安装
安装在linux上
1. 上传tar.gz包,并解压
tar -zxvf redis-5.0.4.tar.gz
2. 安装gcc(必须有网络)
yum -y install gcc
忘记是否安装过,可以使用 gcc -v 命令查看gcc版本,如果没有安装过,会提示命令不存在
3. 进入redis目录,进行编译
make
4. 编译之后,开始安装
make install
安装后的操作
后台运行方式
- redis默认不会使用后台运行,如果你需要,修改配置文件daemonize=yes,当你后台服务启动的时候,会写成一个进程文件运行。
vim /opt/redis-5.0.4/redis.conf
daemonize yes
- 以配置文件的方式启动
cd /usr/local/bin
redis-server /opt/redis-5.0.4/redis.conf
关闭数据库
- 单实例关闭
redis-cli shutdown
- 多实例关闭
redis-cli -p 6379 shutdown
常用操作
- 检测6379端口是否在监听
netstat -lntp | grep 6379
端口为什么是6379?
6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。 MERZ长期以来被antirez(redis作者)及其朋友当作愚蠢的代名词。
- 检测后台进程是否存在
ps -ef|grep redis
连接redis并测试
redis-cli
ping
使用Redis
五大数据类型
- 操作文档:redisdoc.com/
Jedis
java和redis打交道的API客户端
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
连接redis
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.204.141",6379);
String pong = jedis.ping();
System.out.println("pong = " + pong);
}
// 运行前:
// 1.关闭防火墙 systemctl stop firewalld.service
// 2.修改redis.conf [ bind 0.0.0.0 ] 允许任何ip访问,以这个redis.conf启动redis服务(重启redis)
// redis-server /opt/redis5.0.4/redis.conf
常用API
private void testString(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// string
jedis.set("k1","v1");
jedis.set("k2","v2");
jedis.set("k3","v3");
Set<String> set = jedis.keys("*");
Iterator<String> iterator = set.iterator();
for (set.iterator();iterator.hasNext();){
String k = iterator.next();
System.out.println(k+"->"+jedis.get(k));
}
Boolean k2Exists = jedis.exists("k2"); // 查看k2是否存在
System.out.println("k2Exists = " + k2Exists);
System.out.println( jedis.ttl("k1") );// 查看k1的过期时间
jedis.mset("k4","v4","k5","v5");
System.out.println( jedis.mget("k1","k2","k3","k4","k5") );
System.out.println("--------------------------------------------------------");
}
private void testList(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// list
jedis.lpush("list01", "l1","l2","l3","l4","l5");
List<String> list01 = jedis.lrange("list01", 0, -1);
for(String s : list01){
System.out.println(s);
}
System.out.println("--------------------------------------------------------");
}
private void testSet(){
Jedis jedis = new Jedis("192.168.204.141",6379);
// set
jedis.sadd("order","jd001");
jedis.sadd("order","jd002");
jedis.sadd("order","jd003");
Set<String> order = jedis.smembers("order");
Iterator<String> order_iterator = order.iterator();
while(order_iterator.hasNext()){
String s = order_iterator.next();
System.out.println(s);
}
jedis.srem("order", "jd002");
System.out.println( jedis.smembers("order").size() );
}
private void testHash(){
Jedis jedis = new Jedis("192.168.204.141",6379);
jedis.hset("user1", "username","james");
System.out.println( jedis.hget("user1", "username") );
HashMap<String, String> map = new HashMap<String, String>();
map.put("username", "tom");
map.put("gender", "boy");
map.put("address", "beijing");
map.put("phone", "13590875543");
jedis.hmset("user2", map);
List<String> list = jedis.hmget("user2", "username", "phone");
for(String s: list){
System.out.println(s);
}
}
private void testZset(){
Jedis jedis = new Jedis("192.168.204.141",6379);
jedis.zadd("zset01", 60d, "zs1");
jedis.zadd("zset01", 70d, "zs2");
jedis.zadd("zset01", 80d, "zs3");
jedis.zadd("zset01", 90d, "zs4");
Set<String> zset01 = jedis.zrange("zset01", 0, -1);
Iterator<String> iterator = zset01.iterator();
while (iterator.hasNext()){
String s = iterator.next();
System.out.println(s);
}
}
public static void main(String[] args) {
new Test2_API().testZset();
}
事务
- 初始化余额和支出
set yue 100
set zhichu 0
public static void main(String[] args) throws Exception{
Jedis jedis = new Jedis("192.168.204.141",6379);
int yue = Integer.parseInt( jedis.get("yue") );
int zhichu = 10;
jedis.watch("yue"); // 监控余额
Thread.sleep(5000); // 模拟网络延迟
if(yue < zhichu){
jedis.unwatch(); //解除监控
System.out.println("余额不足!");
}else{
Transaction transaction = jedis.multi(); // 开启事务
transaction.decrBy("yue", zhichu); // 余额减少
transaction.incrBy("zhichu", zhichu); // 累计消费增加
transaction.exec();
System.out.println("余额:" + jedis.get("yue"));
System.out.println("累计支出:" + jedis.get("zhichu"));
}
}
- 模拟网络延迟:,10秒内,进入linux修改余额为5,这样,余额<支出,就会进入if
JedisPool
- redis的连接池技术
- 详情:help.aliyun.com/document_de…
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
- 使用单例模式进行优化
package com.lagou;
import jdk.nashorn.internal.scripts.JD;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @BelongsProject: lagou-jedis
* @Author: GuoAn.Sun
* @CreateTime: 2020-08-05 17:46
* @Description: 单例模式优化jedis连接池
*/
public class JedisPoolUtil {
private JedisPoolUtil(){}
private volatile static JedisPool jedisPool = null;
private volatile static Jedis jedis = null;
// 返回一个连接池
private static JedisPool getInstance(){
// 双层检测锁(企业中用的非常频繁)
if(jedisPool == null){ // 第一层:检测体温
synchronized (JedisPoolUtil.class){ // 排队进站
if(jedisPool == null) { //第二层:查看健康码
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1000); // 资源池中的最大连接数
config.setMaxIdle(30); // 资源池允许的最大空闲连接数
config.setMaxWaitMillis(60*1000); // 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
config.setTestOnBorrow(true); //向资源池借用连接时是否做连接有效性检测(业务量很大时候建议设置为false,减少一次ping的开销)
jedisPool = new JedisPool( config, "192.168.204.141",6379 );
}
}
}
return jedisPool;
}
// 返回jedis对象
public static Jedis getJedis(){
if(jedis == null){
jedis = getInstance().getResource();
}
return jedis;
}
}
- 测试类
/**
* @BelongsProject: lagou-jedis
* @Author: GuoAn.Sun
* @CreateTime: 2020-08-05 17:54
* @Description: 测试jedis连接池
*/
public class Test_JedisPool {
public static void main(String[] args) {
Jedis jedis1 = JedisPoolUtil.getJedis();
Jedis jedis2 = JedisPoolUtil.getJedis();
System.out.println(jedis1==jedis2);
}
}
高并发下的分布式锁
- 经典案例:秒杀,抢购优惠券等
搭建工程并测试单线程
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!--实现分布式锁的工具类-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.1</version>
</dependency>
<!--spring操作redis的工具类-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!--redis客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!--json解析工具-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="controller"/>
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="connectionFactory"></property>
</bean>
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="192.168.204.141"></property>
<property name="port" value="6379"/>
</bean>
</beans>
package controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @BelongsProject: lagou-killreids
* @Author: GuoAn.Sun
* @CreateTime: 2020-08-06 11:57
* @Description: 测试秒杀
*/
@Controller
public class TestKill {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("kill")
// 只能解决一个tomcat的并发问题:synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了!
public @ResponseBody synchronized String kill() {
// 1.从redis中获取 手机的库存数量
int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
// 2.判断手机的数量是否够秒杀的
if(phoneCount > 0){
phoneCount--;
// 库存减少后,再将库存的值保存回redis
stringRedisTemplate.opsForValue().set("phone", phoneCount+"");
System.out.println("库存-1,剩余:"+ phoneCount);
}else{
System.out.println("库存不足!");
}
return "over!";
}
}
高并发测试
- 启动两次工程,端口号分别8001和8002
- 使用nginx做负载均衡
upstream sga{
server 192.168.204.1:8001;
server 192.168.204.1:8002;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://sga;
root html;
index index.html index.htm;
}
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
- 使用 JMeter 模拟1秒内发出100个http请求,会发现同一个商品会被两台服务器同时抢购!
实现分布式锁的思路
-
- 因为redis是单线程的,所以命令也就具备原子性,使用setnx命令实现锁,保存k-v
- 如果k不存在,保存(当前线程加锁),执行完成后,删除k表示释放锁
- 如果k已存在,阻塞线程执行,表示有锁
-
- 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除k(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)!
- 设置过期时间,例如10秒后,redis自动删除
-
- 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同
- 第一个线程,执行需要13秒,执行到第10秒时,redis自动过期了k(释放锁)
- 第二个线程,执行需要7秒,加锁,执行第3秒(锁 被释放了,为什么,是被第一个线程的finally主动deleteKey释放掉了)
- 。。。连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效
-
- 给每个线程加上唯一的标识UUID随机生成,释放的时候判断是否是当前的标识即可
-
- 问题又来了,过期时间如果设定?
- 如果10秒太短不够用怎么办?
- 设置60秒,太长又浪费时间
- 可以开启一个定时器线程,当过期时间小于总过期时间的1/3时,增长总过期时间(吃仙丹续命!)
Redisson
- Redis 是最流行的 NoSQL 数据库解决方案之一,而 Java 是世界上最流行(注意,我没有说“最好”)的编程语言之一。
- 虽然两者看起来很自然地在一起“工作”,但是要知道,Redis 其实并没有对 Java 提供原生支持。
- 相反,作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库。
- 而 Redisson 就是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用Redis。
- Redisson 在 java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类。
package controller;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.concurrent.TimeUnit;
/**
* @BelongsProject: lagou-killreids
* @Author: GuoAn.Sun
* @CreateTime: 2020-08-06 11:57
* @Description: 测试秒杀
*/
@Controller
public class TestKill {
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("kill")
// 只能解决一个tomcat的并发问题:synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了!
public @ResponseBody synchronized String kill() {
// 定义商品id
String productKey = "HUAWEI-P40";
// 通过redisson获取锁
RLock rLock = redisson.getLock(productKey); // 底层源码就是集成了setnx,过期时间等操作
// 上锁(过期时间为30秒)
rLock.lock(30, TimeUnit.SECONDS);
try{
// 1.从redis中获取 手机的库存数量
int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
// 2.判断手机的数量是否够秒杀的
if (phoneCount > 0) {
phoneCount--;
// 库存减少后,再将库存的值保存回redis
stringRedisTemplate.opsForValue().set("phone", phoneCount + "");
System.out.println("库存-1,剩余:" + phoneCount);
} else {
System.out.println("库存不足!");
}
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁
rLock.unlock();
}
return "over!";
}
@Bean
public Redisson redisson(){
Config config = new Config();
// 使用单个redis服务器
config.useSingleServer().setAddress("redis://192.168.204.141:6379").setDatabase(0);
// 使用集群redis
//config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://192.168.204.141:6379","redis://192.168.204.142:6379","redis://192.168.204.143:6379");
return (Redisson)Redisson.create(config);
}
}
- 实现分布式锁的方案其实有很多,我们之前用过的zookeeper的特点就是高可靠性,现在我们用的redis特点就是高性能。
- 目前分布式锁,应用最多的仍然是“Redis”