Redis

103 阅读14分钟

概述

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):分区容错性
      • 即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性的服务。
      • 分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。

CAP理论

  • CAP理论提出就是针对分布式数据库环境的,所以,P这个属性必须容忍它的存在,而且是必须具备的。
  • 因为P是必须的,那么我们需要选择的就是A和C。
  • 大家知道,在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节点复制到另外的节点时需要时间和要求网络畅通的,所以,当P发生时,也就是无法向某个节点复制数据时,这时候你有两个选择:
    • 选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能保证是同步的了(失去了C属性)。
    • 选择一致性C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了A属性)。
  • 最常见的例子是读写分离,某个节点负责写入数据,然后将数据同步到其它节点,其它节点提供读取的服务,当两个节点出现通信问题时,你就面临着选择A(继续提供服务,但是数据不保证准确),C(用户处于等待状态,一直等到数据同步完成)。

CAP总结

  • 分区是常态,不可避免,三者不可共存
  • 可用性和一致性是一对冤家
    • 一致性高,可用性低
    • 一致性低,可用性高
  • 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
    • CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
    • CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
    • AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

下载与安装

下载

安装

安装在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

五大数据类型

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

<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!";
    }
}

高并发测试

  1. 启动两次工程,端口号分别8001和8002
  2. 使用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
  1. 使用 JMeter 模拟1秒内发出100个http请求,会发现同一个商品会被两台服务器同时抢购!

实现分布式锁的思路

    1. 因为redis是单线程的,所以命令也就具备原子性,使用setnx命令实现锁,保存k-v
    • 如果k不存在,保存(当前线程加锁),执行完成后,删除k表示释放锁
    • 如果k已存在,阻塞线程执行,表示有锁
    1. 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除k(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)!
    • 设置过期时间,例如10秒后,redis自动删除
    1. 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同
    • 第一个线程,执行需要13秒,执行到第10秒时,redis自动过期了k(释放锁)
    • 第二个线程,执行需要7秒,加锁,执行第3秒(锁 被释放了,为什么,是被第一个线程的finally主动deleteKey释放掉了)
    • 。。。连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效
    1. 给每个线程加上唯一的标识UUID随机生成,释放的时候判断是否是当前的标识即可
    1. 问题又来了,过期时间如果设定?
    • 如果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”