文章目录
- redis大全总结篇
-
- redis简介
- redis五种数据类型
- redis应用场景实例
-
- string:文章阅读数
- list例子
- hash存储user表
- set共同好友
- redis数据淘汰策略
- redis resp协议
-
- resp:
- resp优点:
- 请求-响应模型:
- resp支持数据类型
- redis管道
- redis 事务
- 事务,管道,脚本语义区别
- redis缓存一致性
- redis缓存穿透
- redis缓存雪崩
- redis分布式锁
- 参考资料
redis大全总结篇
redis简介
redis是一个基于内存的key-value数据库,每秒读写近10万次,是目前为止最优秀的key-value DB,redis支持丰富的数据类型并提供了丰富的api操作他,并且AOF,RDB支持持久化数据。但是这些都不是redis给我印象最深刻的地方,我学习redis最深刻的感悟就是,粒度小的api能够有无穷的潜力,例如list的lpush和rpush就可以组装成栈和队列。
强烈建议初学redis一定要把菜鸟教程上的redis命令全部敲一遍,这样会有一点印象,如果直接使用java api操作可能不理解很多东西
菜鸟教程
redis五种数据类型
redis支持string,list,set,zset,hash五种数据类型,这五种数据类型各自有其不同的作用,就不介绍命令了,需要可以去菜鸟教程看。
redis应用场景实例
string:文章阅读数
假如文章id=101
set article:readcount:101 1 get article:readcount:101 incr article:readcount:101
list例子
栈:先入后出,lpush+lpop
lpush list 1 lpush list 2 lpush list 3 lpop list 输出3 lpop list 输出2 lpop list 输出1
队列:先入先出 lpush+rpop
lpush list 1 lpush list 2 lpush list 3 rpop list 输出1 rpop list 输出2 rpop list 输出3
阻塞队列:先入先出,lpush+brpop
lpush list 1 lpush list 2 lpush list 3 brpop list 0 输出1 brpop list 0 输出2 brpop list 0 输出3 brpop list 0 这次将一直阻塞
微博订阅消息
我的id=530
胡歌发布微博,胡歌的关注者id列表[1,2,3,4…,530,…]
古月哥欠发布微博,古月哥欠的关注者id列表[1,3,…,530,…]
胡歌发了一条微博消息存到数据库,消息id为123,胡歌遍历关注者列表,关注者多的话,后台耗时长
lpush msg:1 123 lpush msg:2 123 .... lpush msg:530 123
古月哥欠发了一条微博消息存到数据库,消息id为124,古月哥欠遍历关注者列表
lpush msg:1 124 lpush msg:3 124 .... lpush msg:530 124
我来查看这两位发布的信息
lrange msg:530 0 -1 输出124,123
hash存储user表
id name age 1 lisi 20 2 zs 23 hmset user name:1 lisi age:1 20 hmset user name:2 zs age:2 23 hmget user name:1 age:1
set共同好友
A:[B,C,D.E]
B:[A,C,D,F]
很容易看出来AB共同好友为CD
sadd A B C D E sadd B A C D F sinter A B 输出CD
redis数据淘汰策略
请参考数据淘汰策略
这个人写的真的很好,我写不出来,要写也是复制别人写的
redis resp协议
resp:
Redis Serialization Protocol (Redis序列化协议),该协议用于客户端与redis服务端通信,应用场景是redis-cli通过pipe与服务器建立联系,实例就是将数据库的数据批量导入redis
resp优点:
实现简单,快速解析,人类可读
请求-响应模型:
redis接收到客户端的命令后,对其进行处理,然后响应,这是最简单的模型,但是有两个例外
1:Redis支持流水线操作,因此,客户端可以一次发送多个命令,并等待稍后的回复。
2:当Redis客户端订阅Pub / Sub通道时,协议会更改语义并成为推送协议,也就是说,客户端不再需要发送命令,因为服务器会自动向客户端发送新消息
排除上述两个例外,Redis协议是一个简单的请求 - 响应协议。
resp支持数据类型
SImple Strings ,回复的第一个字节是‘+’
eg:“+OK\r\n”
错误,回复的第一个字节是“ - ”
eg: “-Error unknow command ‘kk’\r\n”
整数,回复的第一个字节是“:”
eg: “:10\r\n”
Bulk Strings,回复的第一个字节是“$”
eg: "$5\r\nvalue\r\n" 其中字符串为 value,而5就是value的字符长度 "$0\r\n\r\n" 空字符串 "$-1\r\n" null
数组,回复的第一个字节是“ *”
*2\r\n$2\r\nmy\r\n$5\r\nredis\r\n *2 $2 my $5 redis
可以打开AOF文件看一下,他采取的就是resp协议
一段测试代码
public class RedisResp { private Socket socket; public RedisResp() { try { socket = new Socket("192.168.235.100", 6379); } catch (IOException e) { e.printStackTrace(); System.out.println("连接失败" + e.getMessage()); } } /** * 设置值 * @param key * @param value * @return * @throws IOException */ public String set(String key, String value) throws IOException { StringBuilder sb = new StringBuilder(); sb.append("*3").append("\r\n"); sb.append("$").append(3).append("\r\n"); sb.append("set").append("\r\n"); // 注意中文汉字。一个汉字两个字节,长度为2,所以不可以直接length sb.append("$").append(key.getBytes().length).append("\r\n"); sb.append(key).append("\r\n"); sb.append("$").append(value.getBytes().length).append("\r\n"); sb.append(value).append("\r\n"); System.out.println(sb.toString()); socket.getOutputStream().write(sb.toString().getBytes()); byte[] b = new byte[2048]; socket.getInputStream().read(b); return new String(b); } /** * 获取值 * @param key * @return * @throws Exception */ public String get(String key) throws Exception { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("*2").append("\r\n"); stringBuffer.append("$").append(3).append("\r\n"); stringBuffer.append("get").append("\r\n"); stringBuffer.append("$").append(key.getBytes().length).append("\r\n"); stringBuffer.append(key).append("\r\n"); socket.getOutputStream().write(stringBuffer.toString().getBytes()); byte[] b = new byte[2048]; socket.getInputStream().read(b); return new String(b); } public static void main(String[] args) throws Exception { RedisResp resp = new RedisResp(); System.out.println(resp.set("key" ,"value")); System.out.println(resp.get("key")); } }
redis管道
redis采用请求响应模型,客户端发送请求到接收服务器的响应需要消耗一下四个时间
1:客户端到服务器的时间
2:命令排队时间
3:命令实际处理时间
4:服务器到客户端的时间
其中第1 4步加起来就是RTT往返时延。这个RTT如果较于总时间比值较大的话那么这次redis处理是低效的,基于这一点,redis提供管道技术,客户端可以一次发多个命令,不用每次都等服务器响应,最后服务器一次全部响应这些数据,这样一来RTT只需要一次即可,有点http长连接的感觉,管道一方面减少了RTT往返时延过长带来的影响,另一方面命令打包有助于redis更快执行命令,因为网络传输需要时间,可能这次执行完命令还要等下一条命令,但是命令打包也不是越多越好,redis会在执行完打包命令前缓存所有命令的执行结果,会消耗内存,所以命令打包也要适量。
另外管道可以在事务中执行也可以不在,管道表达的语义绝不是所有命令要么都执行成功,要么都不成功,管道中某些命令执行失败也会返回错误给客户端,管道表达的语义是执行所有的命令
下面程序管道只花了40ms,不使用将近800ms
Jedis jedis = RedisPool.getJedis(); Pipeline pipelined = jedis.pipelined(); long begin = System.currentTimeMillis(); for (int i = 1; i < 10000; i++) { pipelined.get("stock"); } pipelined.sync(); jedis.close(); long end = System.currentTimeMillis(); System.out.println("pipeline time:" + (end - begin));
redis 事务
本质: redis事务的本质是一组命令集合,事务中的命令会被串行化执行,其他客户端提交的命令不能插入到本事务中
特点:串行化,排他性
redis事务不同于关系型数据库,他没有隔离级别的概念,没有回滚,不保证事务的原子性,在我们的关系型数据库中,事务中一条指令执行失败会导致事务回滚,但是在redis事务中一条指令执行失败,其他指令仍会执行。
redis事务执行阶段
一个事务从开始到执行会经历以下三个阶段:
开始事务。
命令入队。
执行事务。
redis事务相关指令
DISCARD:取消事务,放弃执行事务块内的所有命令。 EXEC:执行所有事务块内的命令。 MULTI:标记一个事务块的开始。 UNWATCH:取消 WATCH 命令对所有 key 的监视。 WATCH key [key ...]:监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
事务正常执行
multi set k1 v1 set k2 v2 exec
放弃事务
multi set k1 v1 set k2 v2 discard //get k1 返回nil
事务队列中有命令性错误,执行EXEC时所有命令都不会执行
multi set k1 v1 sets k2 v2 exec //get k1 返回nil
事务队列中有执行错误时,执行EXEC时其他命令被正常执行
multi set k1 v1 incr k1 exec //get k1 返回v1
watch
client1: set balance 80 client2: set balance 100 watch balance //-》监视balance,余额必须为100,否则事务执行失败,这一步执行完去执行client1的set balance 80 multi get balance exec //返回nil
一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。
故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作。
Jedis jedis = RedisPool.getJedis(); jedis.watch("balance");//监视余额为100,改动了事务执行失败 jedis.set("balance","80");//改动了将导致执行时返回null Transaction transaction = jedis.multi(); transaction.get("balance");//入队 List<Object>result = transaction.exec(); System.out.println(result); jedis.close();
事务,管道,脚本语义区别
-
事务和脚本从原子性上来说都能满足原子性的要求,其区别在于脚本可借助Lua语言可在服务器端存储的便利性定制和简化操作,但脚本无法处理长耗时的操作。redis确保这条script脚本执行期间,其它任何脚本或者命令都无法执行。正是由于这种原子性,script才可以替代MULTI/EXEC作为事务使用。当然,官方文档也说了,正是由于script执行的原子性,所以我们不要在script中执行过长开销的程序,否则会验证影响其它请求的执行。
-
管道是无状态操作集合,使用管道可能在效率上比使用script要好,但是有的情况下只能使用script。因为在执行后面的命令时,无法得到前面命令的结果,就像事务一样,如果你要保证命令的原子性或者后面的命令依赖前面命令的执行结果,则只能使用script或者事务+watch。
redis缓存一致性
读操作:先读缓存,读不到再读数据库,回写缓存,设置过期时间
写操作:先更新数据库,再删除缓存
方案一:先更新缓存,在更新数据库 (1)线程A更新了缓存 (2)线程B更新了缓存 (3)线程B更新了数据库 (4)线程A更新了数据库 数据库理论应该保存B的数据,可是却保存了A的数据
方案二:先更新数据库,再更新缓存 (1)线程A更新了数据库 (2)线程B更新了数据库 (3)线程B更新了缓存 (4)线程A更新了缓存 缓存中本应该是B的数据可是却是A的数据
方案三: 先删除缓存,在更新数据库 该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形: (1)请求A进行写操作,删除缓存 (2)请求B查询发现缓存不存在 (3)请求B去数据库查询得到旧值 (4)请求B将旧值写入缓存 (5)请求A将新值写入数据库 上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
方案四:先更新数据库,再删除缓存 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。 命中:应用程序从cache中取数据,取到后返回。 更新:先把数据存到数据库中,成功后,再让缓存失效。 (1)缓存刚好失效 (2)请求A查询数据库,得一个旧值 (3)请求B将新值写入数据库 (4)请求B删除缓存 (5)请求A将查到的旧值写入缓存 这种情况发生的前提是2比3耗时长,显然这个前提不成立,这种方案最优
redis缓存穿透
大量请求访问一个一定不在数据库中的值,所有请求全部落在db,导致db宕机。采取布隆过滤器或给缓存设置一个空值
//缓存穿透/缓存雪崩 @RequestMapping("/queryUser") @ResponseBody public User queryUser(int userId){ //先从缓存找 User user = (User)redisTemplate.opsForValue().get("user:"+userId); if(user!=null) return user; //再查数据库 System.out.println("查询数据库,id=" + userId); user = userService.getUser(userId); if(user!=null){ Random random = new Random();//热门商品过期时间设置长一点 long time = 3600+random.nextInt(3600);//一个小时到两个小时,解决缓存雪崩 redisTemplate.opsForValue().set("user:"+userId,user,time, TimeUnit.SECONDS); }else{//解决缓存穿透 redisTemplate.opsForValue().set("user:"+userId,null,60, TimeUnit.SECONDS); } return user; }
redis缓存雪崩
redis缓存雪崩:缓存数据在某个集中时间失效,导致大量请求落在db,导致宕机。采取分散设置缓存失效时间
产生雪崩的原因之一,比如双十一零点,会迎来一波又一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。
这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。
redis分布式锁
分布式锁一般有
数据库乐观锁、
基于Redis的分布式锁
基于ZooKeeper的分布式锁三种实现方式
首先,想要保证分布式锁可以使用,下面这四个条件是必须要满足的:
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
jedis实现分布式锁
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.10.0-m1</version> </dependency>
public class RedisLock { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX";//NX 不存在才会set private static final String SET_WITH_EXPIRE_TIME = "PX";//EX:s PX:ms /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识,用这个来防止客户端自己把别人加的锁给解了 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { // String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @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)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }
spring redisTemplate实现分布式锁,spring-data-redis2.1之前setIfAbsent没有过期时间参数
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.4.RELEASE</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
@RequestMapping("/subStock") @ResponseBody public int subStock(){ String requestId = UUID.randomUUID().toString(); String key = "stockkey"; int stock=0; try{ //事务加锁设置expire stringRedisTemplate.setEnableTransactionSupport(true); stringRedisTemplate.multi(); stringRedisTemplate.opsForValue().setIfAbsent(key,requestId);//setnx stringRedisTemplate.expire(key,30, TimeUnit.SECONDS); List<Object> result = stringRedisTemplate.exec(); //再安全一点的做法是开子线程给这把锁续命,每隔十秒执行stringRedisTemplate.expire(key,30, TimeUnit.SECONDS); //如果没到十秒本请求执行结束,释放锁,子线程自己消亡 if(result.get(0).equals(true)&&result.get(1).equals(true)){ stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock<=0){ System.out.println("库存不足"); return stock; } stock-=1; System.out.println("剩余库存:"+stock); stringRedisTemplate.opsForValue().increment("stock",-1); } }finally { //只有自己才能释放自己的锁 if(requestId.equals(stringRedisTemplate.opsForValue().get(key))){ stringRedisTemplate.delete(key); } } return stock; }
spring redisTemplate实现分布式锁 2.1之后,setIfAbsent有过期参数,可以不用事务了
redisson实现分布式锁
参考资料
数据淘汰策略
菜鸟教程
redis官网resp
redis resp
redis管道
redis事务