一,redis6.0安装相关

安装redis6.0.8-linux

因为6.0.6有bug,官网声明,linux版本是因为epoll()仅仅支持linux系统,linux才能发挥redis的最佳性能。

外网

查看redis版本

linux系统内查看 redis-server -v redis客户端内查看 info

二,五种常用基本数据类型的应用场景

redis命令不区分大小写,但是key区分大小写

help @string 查看string相关的所有命令

1.string

同时设置多个键值对 
mset k1 v1 k2 v1 k3 v3 
mget k1 k2 k3

数值增减

	递增数字 incr key

	增加指定的整数 incrby key increment

	递减数值 decr key

	减少指定的整数 decrby key decrement

获取字符串长度 strlen key

分布式锁 
	setnx key value
	set key value [EX seconds] [PX milliseconds] [NX|XX]
	EX:key在多少秒之后过期
	PX:key在多少毫秒之后过期
	NX:当key不存在的时候才创建key
	XX:当key存在的时候,覆盖key

应用场景

商品编号,订单号的生成

点赞

2.hash

redis的hash对应着java中的Map<String,Map<Object,Object>>

一次设置一个字段 hset key field value
一次获取一个字段 hget key field
一次设置多个字段 hmset key field value field value
一次获取多个字段 hmget key field field
获取所有字段 hgetall key
获取某个key内的全部数量 hlen
删除一个key hdel key

应用场景

购物车(中小厂) userId:{skuId:num,skuId:num}

3.list

向列表左边添加元素 lpush key value ...
向列表右边添加元素 rpush key value ...
查看列表 lrange key start stop
获取列表中元素的个数 llen key

应用场景

微信文章订阅公众号 userId:[articleId,articleId,articleId]

4.set

添加元素 sadd key member ....
删除元素 srem key member ....
获取集合中的所有元素 smembers key
判断元素是否在集合中 sismember key member
获取集合中的元素个数 scard key
从集合中随机弹出一个元素,元素不删除 srandmember key [个数]
从集合中随机弹出一个元素,出一个删一个 spop [个数] 
集合运算 
	集合的差集运算
		SDIFF key
	集合的交集运算
		SINTER key
	集合的并集运算 
		SUNION key

应用场景

微信抽奖小程序
微信朋友圈点赞
微信好友关注社交关系
QQ内推可能认识的人

5.zset

向有序集合中加入一个元素和该元素的分数

添加元素 zadd key socre member score member....
按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素 zrange key start stop 
获取元素的分数 zscore key member
删除元素 zrem key member ....
获取指定分数范围的元素 zrangebyscore key min max 
增加某个元素的分数 zincrby key increment member
获取集合中元素的数量 zcard key
获得指定范围内的元素的个数 zcount key min max
按照排名范围删除元素 zremrangebyrank key start stop
获取元素的排名
	从小到大 zrank key member
	从大到小 zrevrank key member

应用场景

根据商品销售对商品进行排序显示

抖音热搜

三,redis分布式锁

1.jvm层面的加锁

2.分布式锁:分布式微服务架构,拆分后各个微服务之间为了避免冲突和数据故障而加入的一种锁

之所以产生分布式锁的原因就是集群的概念引入,本地JVM锁没法锁住集群。

分布式锁的几种实现方式

1.mysql
2.zookeeper
3.redis

环境搭建

多个微服务之间+保证同一时刻,同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)

synchronized和lock的区别:不见不,过时不候

依赖

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>

配置文件

server.port=8888

spring.redis.database=0
spring.redis.host=121.199.31.160
spring.redis.port=6379
spring.redis.password=
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

配置类

@SpringBootConfiguration
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;
    }

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://121.199.31.160:6379").setDatabase(0);
        return Redisson.create(config);
    }
}

redis版本

/**
 * @author yhd
 * @createtime 2020/12/19 15:06
 *
 * redis 分布式锁如何续期
 * redis的异步复制造成锁丢失,比如:主节点没来得及把刚刚set进来的这条数据给从节点,就挂了。
 * zookeeper cp 先同步数据在返回信号
 * redis ap 先返回信号在同步数据
 * 理论上,zookeeper保证一致性,但是不能保证高可用。
 * 实际上,选择redis。
 * redission+aop
 *
 */
@Slf4j
@RestController
@RequestMapping("good")
public class GoodController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String SERVER_PORT;

    private final String REDIS_KEY = "goods:001";

    private final String REDIS_LOCK = "redis:lock";

    @GetMapping("buy")
    public String buyGoods() {

        String value = UUID.randomUUID().toString().replace("-", "") + Thread.currentThread().getName();
        try {
            /**
             * 尝试加锁,但是考虑到机器宕机,程序根本走不到finally、
             * 所以我们需要给锁设置一个过期时间,到期自动释放。
             * 并且我们需要保证原子性。
             *
             */
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
            /**
             * 没有抢到锁
             */
            if (!flag) {
                return "竞争锁失败!";
            }
            //查看库存数
            String result = stringRedisTemplate.opsForValue().get(REDIS_KEY);
            Integer goodNums = result == null ? 0 : Integer.parseInt(result);

            if (goodNums > 0) {

                int realNumber = goodNums - 1;
                stringRedisTemplate.opsForValue().set(REDIS_KEY, String.valueOf(realNumber));
                log.info("success to buy goods ,The stock is :" + realNumber + "!" + "The cluster port is " + SERVER_PORT);
                return "success to buy goods ,The stock is :" + realNumber + "!" + "The cluster port is " + SERVER_PORT;
            }
        } finally {
            /**
             * 考虑程序可能会发生异常,
             * 但是无论如何,最终一定要释放锁。
             * 考虑到过期时间程序并没有执行完,所以需要防止删除别的线程加的锁。
             * !!!原子性
             * 1.撸啊 脚本 TODO
             * 2.使用redis的事物操作
             *  开启   提交  取消    监控   取消监控
             *  MULTI EXEC DISCARD WATCH UNWATCH
             */
            while (true) {
                //监视这个key
                stringRedisTemplate.watch(REDIS_LOCK);
                //如果要删除的key和当前的key相同
                if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){
                    //开启redis事物支持
                    stringRedisTemplate.setEnableTransactionSupport(true);
                    //开启事物
                    stringRedisTemplate.multi();
                    //删除key
                    stringRedisTemplate.delete(REDIS_LOCK);
                    //提交事物
                    List<Object> list = stringRedisTemplate.exec();
                    //如果啥也没删掉,也就是在进入if之后,另一个线程进来了,跳出
                    if (CollectionUtils.isEmpty(list)){
                        continue;
                    }
                }
                //事物成功完成,取消key的监视
                stringRedisTemplate.unwatch();
                //跳出循环
                break;
            }
        }
        log.info("500 ERROR");
        return "500 ERROR";
    }
}

redisson版本

/**
 * @author yhd
 * @createtime 2020/12/19 19:05
 * redisson 两个机制
 * 红锁 redlock redislock的简写 ,实际上就是setnx命令保证原子性。
 * 看门狗
 */
@RestController
@RequestMapping("order")
public class OrderController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    @Value("${server.port}")
    private String SERVER_PORT;

    private final String REDIS_KEY = "goods:001";

    private final String REDIS_LOCK = "redis:lock";

    @SneakyThrows
    @GetMapping("save")
    public String saveOrder() {
        return lock();
    }

    @SneakyThrows
    private String lock() {
        RLock lock = redissonClient.getLock(REDIS_LOCK);
        //尝试获取锁成功
        if (lock.tryLock(10, TimeUnit.SECONDS)) {
            try {
                String num = stringRedisTemplate.opsForValue().get(REDIS_KEY);
                Integer number = num == null ? 0 : Integer.parseInt(num);
                if (number > 0) {
                    stringRedisTemplate.opsForValue().set(REDIS_KEY, String.valueOf(number - 1));
                    return String.valueOf(number - 1);
                }
            } finally {
                //保证最终一定要释放锁,释放锁之前,确保现在确实加锁了,还是当前线程加的锁
                if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        } else {
            //没获取到锁就自旋
            TimeUnit.SECONDS.sleep(2);
            return this.lock();
        }
        return null;
    }
}

四,redis缓存过期淘汰策略

查看redis最大占用内存

配置文件->maxmemory

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

maxmemory是字节类型,注意转换。

一般推荐redis设置内存为最大物理内存的四分之三。

如何修改redis默认内存大小?
1.配置文件
2.命令行 config get maxmemory
config set maxmemory 1*(单位字节)
3.查看redis内存使用情况 info memory
redis内存打满会怎么样?再次存数据会被报OOM

redis不可能实时轮询所有被设置了过期时间的key,来检测数据是否已经到达过期时间,然后对他进行删除。
立即删除能保证内存数据的最大新鲜度,因为他保证过期键值对在过期之后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu最不友好。因为删除操作会占用cpu时间,如果刚好碰上了cpu很忙的时候。比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力。这会产生大量的性能消耗,同时也会影响数据的读取操作。

定时删除相当于拿时间换空间

惰性删除
数据到达过期时间,不作任何处理,等到下次访问数据的时候,如果没有过期返回数据,如果过期了,删除,返回不存在。
惰性删除的最大缺点是,他对内存最不友好。如果一个键已经过期了,而这个键有保留在DB,那么只要这个过期键不被删除,他所占用的内存就不会被释放。在使用惰性删除策略时,如果DB中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么他们也许永远也不会被删除,甚至可以看成是内存泄漏,容易将redis内存打满。

惰性删除相当于拿空间换时间

定期删除(redis使用的)

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

周期行轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频率
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间

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

极端情况:
1.定期删除,从来没有被抽查到
2.惰性删除,从来没有被使用过

内存淘汰策略

noeviction:不会驱逐任何key
volatile-ttl:删除马上要过期的key
allkeys-lru:对所有key使用LRU算法进行删除(*)
volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
allkeys-random:对所有key随机删除
volatile-random:对所有设置了过期时间的key随机删除
allkeys-lfu:对所有key使用LFU算法进行删除
volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

LRU:最近最少使用LFU:频率最少

LRU算法

LRU是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。

LRU算法核心是哈希链表

解法一

/**
 * @author yhd
 * @createtime 2020/12/21 17:46
 */
public class Lru01 extends LinkedHashMap {
    private static final long serialVersionUID = -7867328098766411260L;

    private Integer size;

    private Float loadFactory;

    private Boolean flag;

    /**
     *
     * @param size 容器的容量大小
     * @param loadFactory 加载因子,扩容相关
     * @param flag 容器中元素的排序相关
     *             当flag设置为true的时候,最近使用的会往后移动
     *             当设置为false的时候,并不会进行移位,一直保持插入的顺序
     */
    public Lru01(Integer size, Float loadFactory, Boolean flag) {
        super(size, loadFactory, flag);
        this.size = size;
        this.loadFactory = loadFactory;
        this.flag = flag;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return super.size() > size;
    }
}
class TestA{
    public static void main(String[] args) {
        Lru01 lru01 = new Lru01(3, 0.75F, true);
        lru01.put(1,1);
        lru01.put(2,2);
        lru01.put(3,3);
        System.out.println("lru01 = " + lru01);
        lru01.put(4,4);
        System.out.println("lru01 = " + lru01);
    }
}

解法二

/**
 * @author yhd
 * @createtime 2020/12/21 18:59
 * Map 负责查找,构建一个虚拟的双向链表,他里面安装的就是一个个Node节点,作为数据载体。
 */
public class Lru02 {

    private Integer cacheSize;

    public Map<Integer, Node<Integer, Integer>> map;

    public DoubleLinkedList<Integer, Integer> doubleLinkedList;

    public Lru02(Integer cacheSize) {
        this.cacheSize = cacheSize;
        //查找
        map = new HashMap<Integer, Node<Integer, Integer>>();
        //操作
        doubleLinkedList = new DoubleLinkedList<Integer, Integer>();
    }

    /**
     * 获取节点的时候,判断如果map里面没有这个节点,我们就直接返回-1
     * 从链表删除这个节点,然后重新头插进去
     * 返回map获取到的value。
     * @param key
     * @return
     */
    public Integer get(Integer key){
        if (!map.containsKey(key)){
            return -1;
        }
        Node<Integer,Integer> node=map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);
        return node.value;
    }

    public void set(Integer key,Integer value){
        //说明这是修改操作
        if (map.containsKey(key)){
            Node<Integer, Integer> node = map.get(key);
            node.value=value;
            map.put(key,node);

            doubleLinkedList.removeNode(node);
            doubleLinkedList.addHead(node);
        }else{
            //说明容量已经达到上限
            if (map.size()==cacheSize){
                Node tail = doubleLinkedList.getTail();
                map.remove(tail.key);
                doubleLinkedList.removeNode(tail);
            }
            //此时才算是新增
            Node<Integer, Integer> newNode = new Node<Integer, Integer>(key, value);
            map.put(key,newNode);
            doubleLinkedList.addHead(newNode);
        }
    }


    /**
     * 1.构造一个Node结点 作为数据载体
     */
    class Node<K, V> {
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

        public Node() {
            this.prev = this.next = null;
        }

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

    class DoubleLinkedList<K, V> {
        Node<K, V> head;
        Node<K, V> tail;

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

        /**
         * 头插法
         *
         * @param node
         */
        public void addHead(Node<K, V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        /**
         * 删除节点
         *
         * @param node
         */
        public void removeNode(Node<K, V> node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            //help GC
            node.prev = null;
            node.next = null;
        }

        /**
         * 获取最后一个有效节点
         *
         * @return
         */
        public Node getTail() {
            return tail.prev;
        }
    }


    public static void main(String[] args) {
        //TODO TEST
    }
}