Redis之实现使用及解决方案

1、客户端

客户端根Redis之间使用一种特殊的编码格式(在AOF文件里面我们可以看到的),交做Redis Serialization Protocol (Redis 序列化协议)。特点:容易实现、解析快、可读性强。

a、手写客户端

客户端和服务端通过TCP连接进行数据交互,服务器默认的端口号为6379.

客户端和服务器发送的数据一律以\r\n(CRLF回车+换行结尾。

package org.example.myclient;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author kylin
 * @date 2020/4/18
 * <p>
 * 基于Redis序列化协议,本次采用wireshark 对jedis抓包获得set和get的数据,然后进行模拟
 * <p>
 * 1、建立Socket连接
 * 2、OutputStream 写入数据(发送到服务器)
 * 3、InputStream读取数据(从服务器接口)
 */
public class MyClient {
    private Socket socket;
    private OutputStream write;
    private InputStream read;

    public MyClient(String host, int port) throws IOException {
        socket = new Socket(host, port);
        write = socket.getOutputStream();
        read = socket.getInputStream();
    }

    /**
     * 通过 wireshark抓包可以获得set 的数据包:*3\r\n$3\r\nSET\r\n$8\r\nqingshan\r\n$4\r\n2673\r\n
     *
     * @param key 键
     * @param val 值
     * @throws IOException io
     */
    public void set(String key, String val) throws IOException {
        StringBuffer sb = new StringBuffer();
        // 代表3个参数
        sb.append("*3").append("\r\n");
        // 第一个参数(get)的长度
        sb.append("$3").append("\r\n");
        // 第一个参数的内容
        sb.append("SET").append("\r\n");

        // 第二个参数key的长度
        sb.append("$").append(key.getBytes().length).append("\r\n");
        // 第二个参数key的内容
        sb.append(key).append("\r\n");
        // 第三个参数value的长度
        sb.append("$").append(val.getBytes().length).append("\r\n");
        // 第三个参数value的内容
        sb.append(val).append("\r\n");

        write.write(sb.toString().getBytes());
        byte[] bytes = new byte[1024];
        read.read(bytes);
        System.out.println("-------------set-------------");
        System.out.println(new String(bytes));
    }

    /**
     * 通过 wireshark抓包可以获得get 的数据包:*2\r\n$3\r\nGET\r\n$8\r\nqingshan\r\n
     *
     * @param key 键
     * @throws IOException io
     */
    public void get(String key) throws IOException {
        StringBuffer sb = new StringBuffer();
        // 代表2个参数
        sb.append("*2").append("\r\n");
        // 第一个参数(get)的长度
        sb.append("$3").append("\r\n");
        // 第一个参数的内容
        sb.append("GET").append("\r\n");

        // 第二个参数长度
        sb.append("$").append(key.getBytes().length).append("\r\n");
        // 第二个参数内容
        sb.append(key).append("\r\n");

        write.write(sb.toString().getBytes());
        byte[] bytes = new byte[1024];
        read.read(bytes);
        System.out.println("-------------get-------------");
        System.out.println(new String(bytes));
    }
    
	//test
    public static void main(String[] args) throws IOException {
        MyClient client = new MyClient("192.168.154.9", 6379);
        client.set("qilou", "2673");
        client.get("qilou");
    }
}
b、Jedis

最熟悉和常用的一种客户端。轻量,简洁,便于集成和改造。

有四种工作模式:单机、分片、哨兵、集群

三种请求模式:Client、pipeline、事务

  • jedis多个线程连接一个链接的时候不安全

    可以使用连接池,为每个请求创建不同的链接,

    redis.clients.util.Pool 有三种实现JedisPool、JedisSentinelPool、ShardedJedisPool

    public static void main(String[] args) {
        JedisPool poo = new JediPool("127.0.0.1","6379");
        Jedis jedis = pool.getResource();
    }
    
单机模式连接

不用刚说了。就是标准的Jedis连接

分片模式连接

采用客户端自己的分片模式

package org.example.shard;

import redis.clients.jedis.*;

import java.util.Arrays;
import java.util.List;

/**
 * @author kylin
 * @date 2020/4/18
 */
public class ShardingTest {
    public static void main(String[] args) {
        JedisPoolConfig poolConfig = new JedisPoolConfig();

        // Redis服务器 随便两台独立的
        JedisShardInfo shardInfo1 = new JedisShardInfo("192.168.154.9", 6378);
        JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.154.9", 6379);

        // 连接池
        List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2);
        ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);

        try (ShardedJedis jedis = jedisPool.getResource()) {
            for (int i = 0; i < 100; i++) {
                jedis.set("k" + i, "" + i);
            }
            for (int i = 0; i < 100; i++) {
                Client client = jedis.getShard("k" + i).getClient();
                System.out.println("取到值:" + jedis.get("k" + i) + "," + "当前key位于:" + client.getHost() + ":" + client.getPort());
            }

        }
    }
}

哨兵模式连接

连接sentinel操作数据

package org.example.sentinel;

import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

/**
 * @author kylin
 * @date 2020/4/18
 */
public class JedisSentinelTest {
    private static JedisSentinelPool pool;

    private static JedisSentinelPool createJedisPool() {
        // master的名字是sentinel.conf配置文件里面的名称
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add("192.168.154.6:26379");
        sentinels.add("192.168.154.7:26379");
        sentinels.add("192.168.154.8:26379");
        pool = new JedisSentinelPool(masterName, sentinels);
        return pool;
    }

    public static void main(String[] args) {
        JedisSentinelPool pool = createJedisPool();
        pool.getResource().set("qilou", "qq"+System.currentTimeMillis());
        System.out.println(pool.getResource().get("qilou"));
    }
}

集群模式连接

连接redis-cluster

package org.example.cluster;

import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * @author kylin
 * @date 2020/4/18
 */
public class ClusterTest {
    public static void main(String[] args) throws IOException {
        // 不管是连主备,还是连几台机器都是一样的效果
       /* HostAndPort hp1 = new HostAndPort("192.168.165.7",7291);
        HostAndPort hp2 = new HostAndPort("192.168.165.7",7292);
        HostAndPort hp3 = new HostAndPort("192.168.165.7",7293);*/
        HostAndPort hp4 = new HostAndPort("192.168.165.7", 7294);
        HostAndPort hp5 = new HostAndPort("192.168.165.7", 7295);
        HostAndPort hp6 = new HostAndPort("192.168.165.7", 7296);

        Set<HostAndPort> nodes = new HashSet<>();
        /*nodes.add(hp1);
        nodes.add(hp2);
        nodes.add(hp3);*/
        nodes.add(hp4);
        nodes.add(hp5);
        nodes.add(hp6);

        JedisCluster cluster = new JedisCluster(nodes);
        cluster.set("kylin:cluster", "lilei999");
        System.out.println(cluster.get("kylin:cluster"));
        ;
        cluster.close();
    }
}


pipeline操作

这种模式就是一次性发送多个命令,最后一次取回所有的返回结果,这种模式通过减少网络的往返时间和io读写次数,大幅度提高通信性能。

100万数据只需要1秒多

  • set一百万数据测试

    package org.example.pipeline;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.Pipeline;
    
    /**
     * @author kylin
     * @date 2020/4/18
     * <p>
     * pipeline 批量写操作
     * @see PipelineGet 测试读
     */
    public class PipelineSet {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("192.168.165.7", 6379);
            Pipeline pipelined = jedis.pipelined();
            long t1 = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                pipelined.set("batch" + i, "" + i);
            }
            pipelined.syncAndReturnAll();
            long t2 = System.currentTimeMillis();
            System.out.println("耗时:" + (t2 - t1) + "ms");
            // 耗时:1964ms 垃圾虚拟机测试
        }
    }
    
    
    
  • get100万数据测试

    package org.example.pipeline;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.Pipeline;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Set;
    
    /**
     * @author kylin
     * @date 2020/4/18
     * <p>
     * pipeline 批量读操作
     * @see PipelineSet 数据录入
     */
    public class PipelineGet {
        public static void main(String[] args) {
            new Thread(() -> {
                Jedis jedis = new Jedis("192.168.165.7", 6379);
    
                Set<String> keys = jedis.keys("batch*");
                Pipeline pipelined = jedis.pipelined();
                long t1 = System.currentTimeMillis();
                for (String key : keys) {
                    pipelined.get(key);
                }
    //            List<Object> result = pipelined.syncAndReturnAll();
    //            for (Object src : result) {
    //                System.out.println(src);
    //            }
                System.out.println("Pipeline " + pipelined.syncAndReturnAll().size() + "数据  get耗时:" + (System.currentTimeMillis() - t1) + "ms");
                // 耗时:2178ms 垃圾虚拟机测试
            }).start();
            new Thread(() -> {
                Jedis jedis = new Jedis("192.168.165.7", 6379);
                Set<String> keys = jedis.keys("batch*");
                Pipeline pipelined = jedis.pipelined();
                long t1 = System.currentTimeMillis();
                for (String key : keys) {
                    pipelined.get(key);
                }
    //            List<Object> result  = pipelined.syncAndReturnAll();
    //            for (Object src : result) {
    //                System.out.println(src);
    //            }
                System.out.println("Pipeline " + pipelined.syncAndReturnAll().size() + "数据  get耗时:" + (System.currentTimeMillis() - t1) + "ms");
                // 耗时:2207ms 垃圾虚拟机测试
            }).start();
        }
    }
    
    
jedis实现分布式锁

使用的jedis里面的

package org.example.distlock;

import redis.clients.jedis.Jedis;

import java.util.Collections;

/**
 * @author kylin
 * @date 2020/4/18
 * @desc
 */
public class DistLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁名称
     * @param requestId  请求标识 是客户端的ID(设置成value),如果我们要保证只有加锁的客户端才能释放锁,就必须获得客户端的id
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        return LOCK_SUCCESS.equals(result);
    }

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁
     * @param requestId 请求标识 是客户端的ID(设置成value),如果我们要保证只有加锁的客户端才能释放锁,就必须获得客户端的id
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        return RELEASE_SUCCESS.equals(result);
    }
}
jedis操作事务
package org.example.transaction;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
 * @author kylin
 * @date 2020/4/18
 * <p>
 * 事务的四大命令MULTI EXEC DISCARD WATCH
 */
public class TestTransaction {
    public static void main(String[] args) {
        // 监视trxkey,然后等待线程2执行了再支持,那么线程1 会报错
        new Thread() {
            public void run() {
                Jedis jedis = new Jedis("192.168.165.7", 6379);
                String watch = jedis.watch("trxkey");
                System.out.println("method1线程[" + Thread.currentThread().getName() + "]watch结果:" + watch);
                Transaction multi = jedis.multi();
                multi.set("trxkey", "2673-thread1");
                // 让Thread2先执行完
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                List<Object> exec = multi.exec();
                System.out.println("method1执行结果:" + exec);
                jedis.unwatch();
            }
        }.start();

        // 和上面一样,也是监视,直接执行,会成功
        new Thread() {
            public void run() {
                Jedis jedis = new Jedis("192.168.165.7", 6379);
                String watch = jedis.watch("trxkey");
                System.out.println("method2线程[" + Thread.currentThread().getName() + "]watch结果:" + watch);
                Transaction multi = jedis.multi();
                multi.set("trxkey", "2673-thread2");
                List<Object> exec = multi.exec();
                System.out.println("method2执行结果:" + exec);
            }
        }.start();

    }

}
jedis操作monitor

可以监听所有的操作,redis自身提供

package org.example.monitor;


import com.google.common.util.concurrent.AtomicLongMap;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisMonitor;

import java.util.List;

/**
 * @author kylin
 * @date 2020/4/18
 * @desc 监听所有的操作
 */
public class MonitorTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.165.7", 6379);
         
        jedis.monitor(new JedisMonitor() {
            @Override
            public void onCommand(String command) {
                System.out.println("#monitor: " + command);
                AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create();
                // ATOMIC_LONG_MAP.incrementAndGet(command);
            }
        });

    }
}

jedis操作发布/订阅

也支自定义的的发布订阅增加的处理,JedisPubSub(个人建议去看看了解下)

  • 订阅

    package org.example.pubsub;
    
    import redis.clients.jedis.Jedis;
    
    /**
     * @author kylin
     * @date 2020/4/18
     */
    public class ListenTest {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("192.168.165.7", 6379);
            final MyListener listener = new MyListener();
            // 使用模式匹配的方式设置频道
            // 会阻塞
            jedis.psubscribe(listener, "qilou-*");
        }
    }
    
    
  • 发布

    package org.example.pubsub;
    
    import redis.clients.jedis.Jedis;
    
    /**
     * @author kylin
     * @date 2020/4/18
     */
    public class PublishTest {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("192.168.165.7", 6379);
            jedis.publish("qilou-123", "666");
            jedis.publish("qilou-abc", "zhangsnazhiliu");
        }
    }
    
jedis用lua实现限流
package org.example.lua;

import org.example.util.ResourceUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.Arrays;
import java.util.Collections;

/**
 * @author kylin
 * @date 2020/4/18
 */
public class LuaTest {
    public static void main(String[] args) {
        Jedis jedis = getJedisUtil();
        jedis.eval("return redis.call('set',KEYS[1],ARGV[1])", 1, "test:lua:key", "qilou2673lua");
        System.out.println(jedis.get("test:lua:key"));
        for (int i = 0; i < 10; i++) {
            limit();
        }
    }


    /**
     * 10秒内限制访问5次
     */
    public static void limit() {
        Jedis jedis = getJedisUtil();
        // 只在第一次对key设置过期时间
        String lua = "local num = redis.call('incr', KEYS[1])\n" +
                "if tonumber(num) == 1 then\n" +
                "\tredis.call('expire', KEYS[1], ARGV[1])\n" +
                "\treturn 1\n" +
                "elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
                "\treturn 0\n" +
                "else \n" +
                "\treturn 1\n" +
                "end\n";
        Object result = jedis.evalsha(jedis.scriptLoad(lua), Collections.singletonList("localhost"), Arrays.asList("10", "5"));
        System.out.println(result);
    }

    private static Jedis getJedisUtil() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        JedisPool pool = new JedisPool(jedisPoolConfig, "192.168.165.7", 6379, 10000);
        return pool.getResource();
    }

}

c、Luttece

它和Jedis相比,完全克服了其线程不安全的缺点,是一个可伸缩的线程安全的Redis 客户端,支持同步、异步和响应式模式(Reactive)。多线程可以共享一个连接实例,而不必担心多线程并发问题。

支持redis的高级功能,如Pipeline、发布订阅、事务、Sentinel、集群、支持连接池

他是SpringBoot 2.x 默认的客户端,替换了Jedis。所以可以直接使用RedisTemplate进行操作,连接和创建和关闭也不需要我们操心了。

实战操作

相关实战操作请移步giteehttps://gitee.com/kylin1991_admin/redis-jedis-drill/tree/master/springboot-redis

d、Redisson

​ 是一个在Redis的基础上实现的Java驻内存数据网络(In-Memory Data Grid),提供了分布式和可扩展的Java数据结构。

​ 由上得出,Redisson 和Jedis 定位不同,他不是一个单纯的Redis客户端,而是基础Redis实现的分布式的服务,如果有需要用到一些分布式的数据结构,比如我们还可以基于Redisson的分布式队列实现分布式事务,就可以引入Redisson 的依赖实现。

wiki 地址:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

  • 特点

    • 基于Netty 实现,采用非阻塞IO,性能高
    • 支持异步请求
    • 支持连接池、pipeline、LUA Scripting、Redis Sentinel、Redis Cluster
    • 不支持事务,官方建议以LUA Scripting 代替事务
    • 主从、哨兵、集群都支持。Spring 也可以配置和注入RedissonClient
  • 实现分布式锁

    package lock;
    
    import org.redisson.Redisson;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author kylin
     * @date 2020/4/19
     * @desc  简易分布式锁
     */
    public class LockTest {
        private static RedissonClient redissonClient;
    
        static {
            Config config=new Config();
            config.useSingleServer().setAddress("redis://192.168.165.7:6379");
            redissonClient= Redisson.create(config);
        }
    
        public static void main(String[] args) throws InterruptedException {
            RLock rLock=redissonClient.getLock("updateAccount");
            // 最多等待100秒、上锁10s以后自动解锁
            // 源码:tryLock()——tryAcquire()——tryAcquireAsync()——tryLockInnerAsync()  可以看到使用过的脚本
            if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
                System.out.println("获取锁成功");
            }
    //        Thread.sleep(20000);
            rLock.unlock();
    
            redissonClient.shutdown();
        }
    }
    
    
连接操作

参考gitee:https://gitee.com/kylin1991_admin/redis-jedis-drill/tree/master/gupao-redisson

2、数据一致性方案

针对读多写少的场景,我们可以使用缓存来提升查询速度

那么必然涉及到数据的一致性问题,例如缓存和数据库的数据不一致怎么解决呢?

先操作redis的数据再操作数据库的数据?

先操作数据库的数据再操作redis的数据?

先删缓存还是先删数据库?

先删缓存 会出现数据库更新前,其他线程把老的数据更新到缓存

先删数据库 再删缓存,那如果删除缓存失败呢?失败放入消息队列,一次删,也是可以的

所以延迟双删策略(综合版)会更理想了

1、删除缓存;

2、更新数据库

3、休眠500ms(这个时间,依据读取数据的耗时而定)

4、再次删除缓存

3、高并发问题

a、热点数据发现

在redis存储的数据中心,有一部分是被频繁访问的。

有两种情况可能会导致热点问题的产生

  • 用户几种访问的数据,比如抢购商品,明星结婚 和 明星出轨的微博
  • 数据进行分片情况下,负载不均衡,超过了单个服务器的承受能力。

热点会导致缓存服务器不可用,最终造成压力堆积到数据库。

如何找出访问频率高的key呢?或者我们可以在哪里记录key被访问的情况呢?

  • 计数:

    在所有get set的地方记录;

    但是会有如下问题:

    • 不知道要存多少key,可能发生内存泄漏的问题
    • 会对客户端的代码造成入侵
    • 只能统计当前客户端的热点key
  • 代理层:

    代理端实现,比如TwemProxy 或者 Codis。

    • 但是不是所有的项目都使用了代理的架构
  • 服务端:

    Redis 有一个monitor的命令,可以监控到所有Redis的执行命令

    • Jedis代码实现monitor地址
    • monitor命令在高并发的场景下,会影响性能,所以不适合长时间使用。
    • 只能统计一个Redis节点的热点key
b、缓存雪崩

缓存雪崩就是Redis的大量热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候Redis 请求的并发量又很大,就会导致所有的请求落到数据库

解决方案:

  • 加互斥锁或者使用队列,针对同一个key只允许一个线程到数据库查询
  • 缓存定时预先更新,避免同时失效
  • 通过加随机数,使key 在不同的时间过期
  • 缓存永不过期
c、缓存穿透

如果数据库和缓存中都不存在key,每次查询都会进入数据库查询

解决方案:

  • 缓存空数据 , 需要用过期时间 或 数据库更新后来更新
  • 缓存特殊字符串,比如&& , 需要用过期时间 或 数据库更新后来更新
  • [布隆过滤器](
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值