Redis使用

安装

  1. 进入目标目录下,wget http://download.redis.io/releases/redis-6.0.8.tar.gz

  2. 安装gcc套装

    yum install cpp
    yum install binutils
    yum install glibc
    yum install glibc-kernheaders
    yum install glibc-common
    yum install glibc-devel
    yum install gcc
    yum install make

  3. 升级gcc

    yum -y install centos-release-scl

    yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils

    scl enable devtoolset-9 bash

  4. make test

  5. make install /usr/local/redis6

  6. cp redis.conf /usr/local/redis6

  7. cd /usr/local/redis6

    mkdir log

  8. 修改配置文件redis.conf: daemonize yes //后台启动

    bind 127.0.0.1 改为 bind 0.0.0.0 //127.0.0.1只能本机访问

    0.0.0.0在服务器的环境中,指的就是服务器上所有的ipv4地址

  9. 启动redis服务 ./redis-server …/redis.conf

  10. 启动redis客户端 ./redis-cli

  11. ping - pong

  12. 暴漏端口

    firewall-cmd --list-all

    设置开放的端口号

    firewall-cmd --add-service=http --permanent

    sudo firewall-cmd --add-port=6379/tcp --permanent

    ps: –permanent表示永久生效,不加这个参数的话只会针对本次执行完命令生效,重启后就不管用了

    重启防火墙

    firewall-cmd --reload

Redis数据类型及使用

查命令地址

http://www.redis.cn/commands.html

八大类型

String(字符类型)

Hash(散列类型)

List(列表类型)

Set(集合类型)

SortedSet(有序集合类型,简称zset)

Bitmap(位图)

HyperLogLog(统计)

GEO(地理)

备注

命令不区分大小写,key区分大小写

内置查命令:help @类型名 eg:help @string

String

赋值

set key value

get key

同时设置/获取多个键值

MSET key value [key value …]

MGET key [key …]

数值增减

递增数字 INCR key

增加指定的整数 INCRBY key increment

递减数值 DECR key

减少指定的整数 DECRBY key decrement

使用场景

商品编号,订单号采用INCR命令生成

记录点赞数

Hash

redis hash ===> java Map<String,map<k,v>>

数据结构(java):Map<key,map<field,value>>

一次设置一个字段值 HSET key field value

一次获取一个字段值 HGET key field

一次设置多个字段值 HMSET key field value [field value …]

一次获取多个字段值 HMGET key field [field …]

获取所有字段值 hgetall key

获取某个key内的全部数量 hlen key

删除一个key/field hdel key field [field …]

使用场景

购物车

新增商品1> hset user:01 food1 1
(integer) 1
新增商品2> hset user:01 food2 1
(integer) 1
增加商品2> hincrby user:01 food2 1
(integer) 2
商品数> hlen user:01
(integer) 2
全选商品> hgetall user:01

List

向列表左边添加元素 LPUSH key value [value …]

向列表右边添加元素 RPUSH key value [value …]

查看列表 LRANGE key start stop

获取列表中元素的个数 LLEN key

使用场景

公众号推送消息:lpush wzlike:1001 news sports games

查看消息分页:lrange wzlike:1001 0 10

Set

添加元素 SADD key member [member …]

删除元素 SREM key key member [member …]

获取集合中的所有元素 SMEMBERS key

判断元素是否在集合中 SISMEMBER key member

获取集合中的元素个数 SCARD key

从集合中随机弹出一个元素,元素不删除 SRANDMEMBER key [数字]

从集合中随机弹出一个元素,出一个删一个 SPOP key [数字]

集合运算

差集:SDIFF key [key …]

交集:SINTER key [key …]

并集:SUNION key [key …]

使用场景

抽奖 SPOP SRANDMEMBER

参与抽奖总人数 SCARD

朋友圈点赞

新增点赞 sadd pub:msgid 点赞用户id

取消点赞 srem pub:msgid 点赞用户id

展示所有点赞用户 smembers pub:msgid

点赞用户数统计,就是常见的点赞红色数字 scard pub:msgid

微博共同关注:取交集

qq可能认识的人:sinter-我们都认识的人;sdiff可能认识的人

ZSet

[withscores] 连带着分数一起显示

添加元素 ZADD key score member [score member …]

按照元素分数从小到大的顺序返回索引start到stop之间的所有元素 ZRANGE key start stop [WITHSCORES]

获取元素的分数 zscore key member

删除元素 zrem key member [member …]

获取指定分数范围的元素 zrangebyscore key min max [withscores] [limit offset count]

增加某个元素的分数 zincrby key increment member

获取集合中元素的数量 zcard key

获得指定分数范围内的元素个数 zcount key min max

使用场景

1.商品销量排行榜(sorted set集合),key为goods:sellsort 分数为商品销量

商品编号1001的销量是9,商品编号1002的销量是15 zadd goods:sellsort 9 1001 15 1002

有一个客户又买了2件商品1001,商品编号1001销量增加2 zincrby goods:sellsort 2 1001

求商品销量排行前10名 zrange goods:sellsort 0 10 withscores

2.热搜 zrevrange 倒序-分数高的在前面

Redis事务

Redis的事务是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成。

Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。

Redis将命令集合序列化并确保处于一个事务的命令集合连续且不被打断的执行。

Redis不支持回滚的操作

不保证原子性:redis中的一个事务中如果存在命令执行失败,那么其他命令依然会被执行,没有回滚机制

watch监控,监控的值如果被修改了,事务执行不成功

整合SpringBoot

pom依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.13.4</version>
    </dependency>
</dependencies>

application.properties

server.port=1111
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.247.129
# Redis服务器端口
spring.redis.port=6379
# Redis 服务器密码
spring.redis.password=
# 连接池最大连接数(使用负数表示没有限制)默认8
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负数表示没有限制) 默认-1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.jedis.pool.min-idle=0
spring.redis.timeout=50000

自定义RedisTemplate

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}

controller测试

@RestController
public class GoodsController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisTemplate redisTemplate;
    @Value("${server.port}")
    private String serverPort;
    @GetMapping("/buy_goods")
    public String buyGoods(){
        String key = "goods:001";
        String result = stringRedisTemplate.opsForValue().get(key);
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if(goodsNumber > 0){
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));
            return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
        } else {
            System.out.println("库存不足" + "服务端口:" + serverPort);
        }
        return "库存不足" + "服务端口:" + serverPort;
    }

    @GetMapping("/add_goods/{number}")
    public String buyGoods(@PathVariable Integer number){
        try{
            String key = "goods:001";

            String result = stringRedisTemplate.opsForValue().get(key);
            Integer curNumber = result == null ? 0 : Integer.valueOf(result);
            stringRedisTemplate.opsForValue().set(key,String.valueOf(curNumber + number));

        } catch (Exception e){
            return "添加失败" + "服务端口:" + serverPort;
        }
        return "添加成功" + "服务端口:" + serverPort;
    }
}

分布式锁

单机锁:Lock与Synchronize

private ReentrantLock lock = new ReentrantLock();

@GetMapping("/buy_goods")
public String buyGoods() {
    lock.lock();
    try{
        String key = "goods:001";
        String result = stringRedisTemplate.opsForValue().get(key);
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if(goodsNumber > 0){
            Thread.sleep(10000);
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));
            return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
        } else {
            System.out.println("库存不足" + "服务端口:" + serverPort);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
    return "库存不足" + "服务端口:" + serverPort;
}

分布式锁:

1.setIfAbsent

absent 缺少的,如果没有就添加

对应redis命令:SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX seconds – 设置键key的过期时间,单位时秒
  • PX milliseconds – 设置键key的过期时间,单位时毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);
问题1:为什么设置过期时间?

如果服务器宕机,程序走不到finally块中,为了保证解锁可以加入过期时间,达到自动解锁的效果

赋值与过期时间必须在一行,保证原子性

问题2:误删其他线程的锁

误删其他线程的锁,线程1执行超出key的过期时间,线程2抢到锁,线程1在finally中误删线程2的锁

解决:

删除前进行判断,不要误删其他的锁

private static final String REDIS_LOCK = "redis_lock";
public String buyGoods() throws InterruptedException {
    String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    try{
        // 设置过期时间,到时间自动解锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value,60L, TimeUnit.SECONDS);
        if(!flag){
            return "抢锁失败,服务器忙";
        }
        String key = "goods:001";
        String result = stringRedisTemplate.opsForValue().get(key);
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if(goodsNumber > 0){
            Thread.sleep(10000);
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));

            return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
        } else {
            System.out.println("库存不足" + "服务端口:" + serverPort);
        }
    } finally {
        // 释放redis锁
        if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
            stringRedisTemplate.delete(REDIS_LOCK);
        }
    }
    return "库存不足" + "服务端口:" + serverPort;
}
问题3:finally块的判断+del删除操作不是原子性的

解决:

1.lua脚本 官网推荐

脚本如下:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

RedisUtil

public class RedisUtils {
    private static JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool = new JedisPool(jedisPoolConfig,"192.168.111.147",6379);
    }

    public static Jedis getJedis() throws Exception {
        if(null != jedisPool){
            return jedisPool.getResource();
        }
        throw new Exception("jedispool is not ok");
    }
}

改进后:

 finally {
            Jedis jedis = RedisUtils.getJedis();
            String script = "if redis.call('get',KEYS[1]) == ARGV[1]" +
                    "then" +
                    "    return redis.call('del',KEYS[1])" +
                    "else" +
                    "    return 0" +
                    "end";
            try{
            	// 执行脚本
                Object res = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if("1".equals(res.toString())){
                    System.out.println("----- del redis lock ok");
                } else {
                    System.out.println("----- del redis lock error");
                }
            } finally {
                if(null != jedis){
                    jedis.close();
                }
            }
        }

2.redis自身事务

改进后

finally {

            while (true){
                // 监控key,防止其他人修改
                stringRedisTemplate.watch(REDIS_LOCK);
                // 判断是不是自己的锁
                if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
                    // 开启事务
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    stringRedisTemplate.multi();
                    stringRedisTemplate.delete(REDIS_LOCK);
                    // 删除key,并提交事务
                    List<Object> list = stringRedisTemplate.exec();
                    // 返回空说明事务执行失败,重新尝试
                    if(list == null){
                        continue;
                    }
                }
                // 解除watch锁
                stringRedisTemplate.unwatch();
                break;
            }
        }
问题4:如何确保redisLock过期时间大于业务执行时间, 自动续期
问题5:redis异步赋值造成锁丢失

redis集群master设置key,未同步完成master挂掉,slave上位,导致key在master上有,在slave上没有

解决问题4,5

官网推荐RedLock Redisson

改进后

@Autowired
private Redisson redisson;

@GetMapping("/buy_goods2")
public String buyGoods2() throws Exception {
    RLock redissonLock = redisson.getLock(REDIS_LOCK);
    redissonLock.lock();
    try{
        String key = "goods:001";
        String result = stringRedisTemplate.opsForValue().get(key);
        int goodsNumber = result == null ? 0 : Integer.parseInt(result);
        if(goodsNumber > 0){
            Thread.sleep(10000);
            int realNumber = goodsNumber - 1;
            stringRedisTemplate.opsForValue().set(key,String.valueOf(realNumber));

            return "购买成功,剩余:" + realNumber + "服务端口:" + serverPort;
        } else {
            System.out.println("库存不足" + "服务端口:" + serverPort);
        }
    } finally {
        if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
            redissonLock.unlock();
        }
    }
    return "库存不足" + "服务端口:" + serverPort;
}

Redis内存

查看Redis占用最大内存

配置文件redis.conf 中 maxmemory

如果不设置最大内存大小或者设置最大内存大小为0,在64为操作系统下,不限制内存大小,在32位操作系统下最多使用3GB

生产环境配置多大合适

一般推荐Redis设置内存为最大物理内存的四分之三(HashMap负载因子0.75)

修改方式

1.修改配置文件redis.conf 中 maxmemory

2.命令修改 eg: conf set maxmemory 104857600

查看内存:info memory

内存满了会怎样

报错OOM:OOM command not allowed when used memory > ‘maxmemory’

在这里插入图片描述

Redis内存淘汰策略

过期键删除策略
定时删除

过期后立即删除,数据新鲜度高

对cpu不友好,用处理器性能换取存储空间(时间换空间)

惰性删除

用到时候再删(被访问时候再删),内存中会有不需要的数据

对memory不友好,用存储空间换处理器性能(空间换时间)

定期删除

是前两种策略的折中

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响

周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度

特点1:cpu性能占用设置有峰值,检测频度可自定义设置

特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

举例:redis默认每隔100ms检查,是否有过期的key,有过期key则删除。注意:redis不是每隔100ms将所有的key检查一次(太浪费效率)而是随机抽取进行检查。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

总结:周期性抽查存储空间(随机抽查,重点抽查)

淘汰策略

使用惰性删除和定期删除,难免还有漏网之鱼(既没有用到,也没有被抽取到的key),当存储量接近maxmemory的时候,会触发淘汰策略

noeviction:不会驱逐任何key (出厂默认)

allkeys-lru:对所有key使用LRU算法进行删除

volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除

allkeys-random:对所有key随机删除

volatile-random:对所有设置了过期时间的key随机删除

volatile-ttl:删除马上要过期的key

allkeys-lfu:对所有key使用LFU算法进行删除

volatile-lfu:对所有设置了过期时间的key进行LFU算法进行删除

生产环境推荐用:allkeys-lru

LRU算法

LRU是least Recently used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。淘汰最不常用的数据

LinkedHashMap实现

public class LRUCacheDemo<K,V> extends LinkedHashMap<K,V> {

    private int capacity = 3;

    public LRUCacheDemo(int capacity) {
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size() > capacity;
    }

    public static void main(String[] args) {
        LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
        lruCacheDemo.put(1,"a");
        lruCacheDemo.put(2,"b");
        lruCacheDemo.put(3,"c");
        System.out.println(lruCacheDemo.keySet());

        lruCacheDemo.put(4,"d");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.get(2);
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(5,"e");
        System.out.println(lruCacheDemo.keySet());
        lruCacheDemo.put(6,"f");
        System.out.println(lruCacheDemo.keySet());
    }
}

注意点

1.继承LinkedHashMap<K,V>,重写构造方法,public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) accessOrder设为true,会把最近访问的数据放到末尾,如果是false则不会,就是按顺序添加

2.重写removeEldestEntry,通过覆盖这个方法,加入一定的条件,满足条件返回true。当put进新的值方法返回true时,便移除该map中最老的键和值。

手写实现

public class LRUCacheDemo2<K,V> {

    class Node<K,V> {
        K key;
        V value;
        Node<K,V> next;
        Node<K,V> prev;

        public Node(){
            this.next = null;
            this.prev = null;
        }
        public Node(K key, V value){
            this.key = key;
            this.value = value;
            this.next = null;
            this.prev = null;
        }
    }

    class DoubleLinkedList<K,V>{
        Node<K,V> head;
        Node<K,V> tail;
        public DoubleLinkedList(){
            this.head = new Node();
            this.tail = new Node();
            this.head.next = tail;
            this.tail.prev = head;
        }
        public void addHead(Node node){
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        public void remove(Node node){
            node.prev.next = node.next;
            node.next.prev = node.prev;
            node.next = null;
            node.prev = null;
        }

        public Node getLast(){
            return tail.prev;
        }
    }
    private int cacheSize;
    private Map<Integer,Node<Integer,Integer>> map;
    private DoubleLinkedList<Integer,Integer> doubleLinkedList;

    public LRUCacheDemo2(int cacheSize){
        this.cacheSize = cacheSize;
        map = new HashMap<>();
        doubleLinkedList = new DoubleLinkedList<>();
    }

    public int get(int key){
        if(!map.containsKey(key)){
            return -1;
        }
        Node<Integer, Integer> node = map.get(key);
        doubleLinkedList.remove(node);
        doubleLinkedList.addHead(node);
        return node.value;
    }

    public void put(int key,int value){
        if(map.containsKey(key)){
            // update
            Node<Integer, Integer> node = map.get(key);
            node.value = value;
            map.put(key,node);
            doubleLinkedList.remove(node);
            doubleLinkedList.addHead(node);
        } else {
            // create
            // 判断长度是否足够
            if(map.size() >= cacheSize){
                // 删除最老的元素
                Node lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.remove(lastNode);
            }
            Node node = new Node(key,value);
            map.put(key,node);
            doubleLinkedList.addHead(node);
        }
    }

    public static void main(String[] args) {

        LRUCacheDemo2 lruCacheDemo = new LRUCacheDemo2(3);
        lruCacheDemo.put(1,1);
        lruCacheDemo.put(2,2);
        lruCacheDemo.put(3,3);
        System.out.println(lruCacheDemo.map.keySet());

        lruCacheDemo.put(4,4);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.get(2);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(5,5);
        System.out.println(lruCacheDemo.map.keySet());
        lruCacheDemo.put(6,6);
        System.out.println(lruCacheDemo.map.keySet());
    }
}

总结:

使用双向链表加HashMap 实现

map用来增加查找效率,链表用来做增删操作,

当访问数据后将最新访问的数据移动到链表头,然后删除的时候从链表尾删

如图

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值