分布式存储-Redis实战&常见问题解决
前面讲了一些Redis的使用场景和数据类型,本章会聊到:
Redis抽奖实现
整体流程:
- 缓存奖品信息:我们会使用这个活动id去查询数据库,并把查询到的数据缓存在redis中(这个步骤是异步的,我们用一个CountDownLatch去对他进行控制,缓存完成后,给count-1,后续需要redis中数据的流程就可以继续处理)
- 开始抽奖:这是一个简单的区间算法,在lottery_item中有对于每个奖品的概率比。从redis中拿到所有的奖项,如果没有则从数据库中获取(因为上面缓存的那一步骤是异步的,可能这个时候还有缓存成功)。我们根据随机数去落到我们设置好的概率区间中(区间越大,抽到某个奖品的概率越大)
- 发放奖品:我们的奖品类型不同 (lottery_prize#prize_type)根据不同奖品的类型,走不同的逻辑,比如我们有的奖品要发送短信,有的奖品不要,我们就定一个模板方法,然后不同类型的奖品走不同类型的发送逻辑。
SpringBoot中使用方法(Lettuce)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>redis: port: 6379 host: ip lettuce: pool: max-active: -1 max-idle: 2000 max-wait: -1 min-idle: 1 time-between-eviction-runs: 5000@Autowired Redistemplate<String,String> redistemplate;
Redis的客户端
常见的:Jedis、Redission、Lettuce(上面给的pom)
我们发送一个命令(set n v)到redis上的时候,使用抓包工具发现
这里的$*表示key的长度 ,比如:$3表示set的长度是3 *3 表示我们传递了三个参数给redis
那解析下来的命令就是:*3\r\n$3\r\nSET\r\n$1\r\nn\r\n$\r\nv,** 那是不是证明只要我们符合这样的编码协议就可以和redis交流了呢
定义get set 命令
public class CommandConstant { public static final String START="*"; public static final String LENGTH="$"; public static final String LINE="\r\n"; //这里提供两个命令 public enum CommandEnum{ SET, GET } }View Code封装api
public class CustomerRedisClient { private CustomerRedisClientSocket customerRedisClientSocket; //连接的redis地址和端口 public CustomerRedisClient(String host,int port) { customerRedisClientSocket=new CustomerRedisClientSocket(host,port); } //封装一个api 发送指令 public String set(String key,String value){ //传递给redis,同时格式化成redis认识的数据 customerRedisClientSocket.send(convertToCommand(CommandConstant.CommandEnum.SET,key.getBytes(),value.getBytes())); return customerRedisClientSocket.read(); //在等待返回结果的时候,是阻塞的 } //获取指令 public String get(String key){ customerRedisClientSocket.send(convertToCommand(CommandConstant.CommandEnum.GET,key.getBytes())); return customerRedisClientSocket.read(); } //这里按照redis的要求格式化 就是前面抓包后拿到的格式 *3\r\n$3\r\nSET\r\n$1\r\nn\r\n$\r\nv public static String convertToCommand(CommandConstant.CommandEnum commandEnum,byte[]... bytes){ StringBuilder stringBuilder=new StringBuilder(); stringBuilder.append(CommandConstant.START).append(bytes.length+1).append(CommandConstant.LINE); stringBuilder.append(CommandConstant.LENGTH).append(commandEnum.toString().length()).append(CommandConstant.LINE); stringBuilder.append(commandEnum.toString()).append(CommandConstant.LINE); for (byte[] by:bytes){ stringBuilder.append(CommandConstant.LENGTH).append(by.length).append(CommandConstant.LINE); stringBuilder.append(new String(by)).append(CommandConstant.LINE); } return stringBuilder.toString(); } }View Code连接redis
public class CustomerRedisClientSocket { //这里可以使用nio private Socket socket; private InputStream inputStream; private OutputStream outputStream; public CustomerRedisClientSocket(String ip,int port){ try { socket=new Socket(ip,port); inputStream=socket.getInputStream(); outputStream=socket.getoutputStream(); } catch (IOException e) { e.printstacktrace(); } } //发送指令 public void send(String cmd){ try { outputStream.write(cmd.getBytes()); } catch (IOException e) { e.printstacktrace(); } } //读取数据 public String read(){ byte[] bytes=new byte[1024]; int count=0; try { count=inputStream.read(bytes); } catch (IOException e) { e.printstacktrace(); } return new String(bytes,0,count); } }View Code测试
public class MainClient { public static void main(String[] args) { CustomerRedisClient customerRedisClient=new CustomerRedisClient("ip",6379); System.out.println(customerRedisClient.set("customer","Define")); System.out.println(customerRedisClient.get("customer")); } }View Code结果 +ok(redis返回的成功报文) $6(返回的value是6的长度)
根据上面我们自己实现的中间件,我们可以悟出,从这些层面选择中间件:
- 通信层面的优化:当我们获取返回结果的时候是阻塞的(我们自己实现的中间件)
- 是否采用异步通信:多线程(效率高)
- 针对key和value的优化 :传递的报文越小,传递的速度肯定更快
- 连接的优化(连接池)
使用redis中遇见的问题
【数据库和redis的数据一致性问题】:实际上很难解决强一致性问题,常见有两种操作
【缓存雪崩】
- 【原因】大量的热点数据同时失效,因为设置了相同的过期时间,刚好这个时候请求量又很大,那这个时候压力就到了数据库上,从而就导致了雪崩。
【缓存穿透】:
- 【原因】:Redis和数据库中都不存在查询的数据,那这就是一次无效查询,如果有人伪造了很多请求,那可能会引发数据库宕机,因为redis中没有数据,请求肯定就请求到数据库,这就叫缓存穿透
- 【方案】:使用布隆过滤器
- 【流程】:
- 【实现】:
- 使用guava
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency>View Code- 程序初始的时候加载数据到布隆过滤器中
@Slf4j @Component public class BloomFilterDataLoadApplicationRunner implements ApplicationRunner { @Autowired ICityService cityService; @Override public void run(ApplicationArguments args) { List<City> cities=cityService.list(); //第一个参数指的是要存储的数据,第二个数据是允许的容错率 BloomFilter<String> bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000000,0.03); cities.parallelStream().forEach(city-> bloomFilter.put(RedisKeyConstants.CITY_KEY+":"+city.getId())); BloomFilterCache.cityBloom=bloomFilter; } }View Code- 客户端访问时增加验证
@GetMapping("/bloom/{id}") public String filter(@PathVariable("id")Integer id){ String key=RedisKeyConstants.CITY_KEY+":"+id; if(BloomFilterCache.cityBloom.mightContain(key)){ return redistemplate.opsForValue().get(key).toString(); } return "数据不存在"; } public class BloomFilterCache { public static BloomFilter<String> cityBloom; }View Code- 剖析布隆过滤器:布隆过滤器是一种空间利用率极高的数据结构,他的底层实际上并不存储我们缓存的元素的内容,而是存储缓存元素的标记 都是0/1。比如:一个int类型是32位4个字节,32位意味着我们可以存储32个0或者1,那我现在如果要存储32个条数据,只需要一个int类型,到底这是怎么做到的?底层用到了位图
- 一个例子解释位图:
- 现在有32位 【0000 0000 0000 0000 0000 0000 0000 0000】
- 比如存储5这个数字 ->5 的二进制是101 【0000 0000 0000 0000 0000 0000 0010 1000】
- 第二个数字是9 ->9的二进制是 1001 【0000 0000 0000 0000 0000 0010 0110 1000】
- 布隆过滤器引入了多个函数去生成hash数值,我们传入的数据通过这些函数计算,则落入了这32位中。比如 有x y z 三个函数,我们传入了一个的数据,通过这三个函数进行hash换算,落到了 32位中的5 6 9 ,那就把这5 6 9 这几个地方变为1,当我们要查询时候我们之前传递的数据是否存在的时候,再次用这些函数进行换算,如果相关位置是1 则说明数据存在,否则数据则不存在。
- 解析布隆过滤器的参数:传递的 100000000是我们要构建多少个int类型(一个int类型可以存储32位),0.03是误判率(误判率指的就是对我们传递的数据进行hash换算的函数)
BloomFilter<String> bloomFilter=BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000000,0.03);- 很多地方都运用了这种思想,比如
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。