文章目录
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进行操作,连接和创建和关闭也不需要我们操心了。
实战操作
相关实战操作请移步gitee:https://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,每次查询都会进入数据库查询
解决方案:
- 缓存空数据 , 需要用过期时间 或 数据库更新后来更新
- 缓存特殊字符串,比如&& , 需要用过期时间 或 数据库更新后来更新
- [布隆过滤器](