高并发缓存之缓存介绍、redis基本应用介绍

前言

本篇文章主要介绍缓存得概念、作用、场景、组件;以及redis介绍、应用场景分析常见客户端使用。以及spring缓存注解得介绍。

缓存

为什么需要缓存其实就是为了加强服务器检索效率;也是由于持久化层数据库访问数据慢,才有缓存的概念,高速数据存储层。

什么是缓存

在计算机中,缓存是一个高速数据存储层,其中存储了数据子集,且 通常是短暂性存储,这样日后再次请求此数据时,速度要比访问数据 的主存储位置快。通过缓存,您可以高效地重用之前检索或计算的数据。
至于高速数据存储层,为什么比主存储位置快,也是得益于这个存储层的特性;短暂性存储。

计算机种缓存

这个在分析多线程情况下,数据可见性问题。研究数据缓存,因为需要保证数据查询高效性,有了3级高速缓存。

分为 寄存器 、L1高速缓存、L2高速缓存、L3高速缓存 、主存。 本地二级存储。远程二级存储。

cpu级包含寄存器。cpu高速缓存

内存级包含堆内存、堆外内存、磁盘缓存

为什么要使用缓存

 

  缓存带来的好处:

  • 提升应用程序性能
  • 降低数据库的成本
  • 减少服务端负载
  • 消除数据库热点
  • 可预测的性能
  • 提高读取吞吐量IOPS

这些都是使用redis的好处,从我之前的项目下,也可以用来做用户登录过期,及单用户登录的解决,虽然redis的一般好处不用这个。但在实际开发中,为了方便简单,可以使用redis。

场景

在Java应用中,对于访问频率高,更新少的数据,通常的方案是将这类数据加入缓存
中。相对从数据库中读取来说,读缓存效率会有很大提升。
在集群环境下,常用的分布式缓存有Redis、Memcached等。但在某些业务场景上,可
能不需要去搭建一套复杂的分布式缓存系统,在单机环境下,通常是会希望使用内部
的缓存(LocalCache)。
应用场景也是最常用的就是针对业务场景访问频率高
在本地缓存中:
   比较经典的实现例如guava Ehcache  ,以及手写的concurrenthashmap 都可以用来做缓存。

只能针单台的JVM

分布式的缓存,  redis memcached等

本地缓存

使用guava去实现一个本地缓存,引入guava的maven引用

 <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
            <version>1.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0.1-jre</version>
        </dependency>

代码实现

public class GuavaCacheDemo {
	public static void main(String[] args) throws ExecutionException {
		// 缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
		LoadingCache<String, String> cache = CacheBuilder
				// CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
				.newBuilder()
				// 设置并发级别为8,并发级别是指可以同时写缓存的线程数
				.concurrencyLevel(8)
				// 设置写缓存后8秒钟过期
				.expireAfterAccess(8, TimeUnit.SECONDS)
				// 设置写缓存后1秒钟刷新
				.refreshAfterWrite(1, TimeUnit.SECONDS)
				// 设置缓存容器的初始容量为10
				.initialCapacity(10)
				// 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
				.maximumSize(100)
				// 设置要统计缓存的命中率
				.recordStats()
				// 设置缓存的移除通知
				.removalListener(new RemovalListener<Object, Object>() {
					@Override
					public void onRemoval(RemovalNotification<Object, Object> notification) {
						System.out.println(notification.getKey() + " 被移除了,原因: " + notification.getCause());
					}
				})// build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
				.build(new CacheLoader<String, String>() {
					@Override
					public String load(String key) throws Exception {
						System.out.println("缓存没有时,从数据库加载" + key);
						// TODO jdbc的代码~~忽略掉
						return "cache" + key;
					}
				});
		// 第一次读取
		for (int i = 0; i < 20; i++) {
			System.out.println(cache.get("" + i));
		}
	}
}

得到的结果就是

缓存没有时,从数据库加载0
cache0
缓存没有时,从数据库加载1
cache1
缓存没有时,从数据库加载2
cache2
缓存没有时,从数据库加载3
cache3
缓存没有时,从数据库加载4
cache4
缓存没有时,从数据库加载5
cache5
缓存没有时,从数据库加载6
cache6
缓存没有时,从数据库加载7
cache7
缓存没有时,从数据库加载8
cache8
缓存没有时,从数据库加载9
cache9
缓存没有时,从数据库加载10
cache10
缓存没有时,从数据库加载11
cache11
缓存没有时,从数据库加载12
cache12
缓存没有时,从数据库加载13
cache13
缓存没有时,从数据库加载14
cache14
缓存没有时,从数据库加载15
cache15
缓存没有时,从数据库加载16
cache16
缓存没有时,从数据库加载17
cache17
缓存没有时,从数据库加载18
cache18
缓存没有时,从数据库加载19
cache19

直接使用guava也可以使用本地缓存的。直接使用本地缓存的化,速率是最快的。因此例如在mybatis 也好,都有二级缓存,现在比较流行的是三级缓存。

并且guava缓存给我们提供了许多监听方法,使得这个缓存api更加受用等等。

实现缓存的方案

基于JSR107规范自研,基于concurrnthashmap实现的缓存 ,guava

JSR107规范

在java caching定义了5个核心接口,分别是cachingProvder,CacheManager、Cache、Entry 和expiry

cachingProvder:缓存提供者

CacheManager:缓存管理器

Cache:缓存

Entry:缓存实体类

expiry:过期时间

根据这个规范实现缓存,实现的比较少。主要还是在实际中不好用导致的

基于ConcurrentHashMap实现的数据缓存

 主要需要一个超时机制

ConcurrentHashMap线程安全
SoftReference软引用,在OOM前能够回收内存
CacheObject能够处理过期
利用concurrentHashMap 和 CacheObject 处理过期数据进行更新
缓存对象一定要考虑  软引用 和 虚引用 ,保证jvm正常。
代码实现

public class MapCacheDemo {
	// 使用了 ConcurrentHashMap,线程安全的要求。
	// 使用SoftReference <Object> 作为映射值,因为软引用可以保证在抛出OutOfMemory之前,如果缺少内存,将删除引用的对象。
	// 在构造函数中,我创建了一个守护程序线程,每5秒扫描一次并清理过期的对象。
	private static final int CLEAN_UP_PERIOD_IN_SEC = 5;

	private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();

	public MapCacheDemo() {
		Thread thread = new Thread(new Runnable() {

			@Override
			public void run() {
				while (!Thread.currentThread().isInterrupted()) {
					try {
						Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
						cache.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get)
								.map(CacheObject::isExpired).orElse(false));
					} catch (InterruptedException e) {
						Thread.currentThread().interrupt();
					}
				}
			}
		});

		thread.setDaemon(true);
		thread.start();
	}

	public void add(String key, Object value, long periodInMillis) {
		if (key == null) {
			return;
		}
		if (value == null) {
			cache.remove(key);
		} else {
			long expiryTime = System.currentTimeMillis() + periodInMillis;
			cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
		}
	}

	// 缓存对象value
	public static class CacheObject {
		private Object value;
		private long expiryTime;

		private CacheObject(Object value, long expiryTime) {
			this.value = value;
			this.expiryTime = expiryTime;
		}

		boolean isExpired() {
			return System.currentTimeMillis() > expiryTime;
		}

		public Object getValue() {
			return value;
		}

		public void setValue(Object value) {
			this.value = value;
		}
	}
}

这里代码实现一个简单的本地缓存,当然如果需要监听器缓存去除等功能,就添加接口进行实现就行。这里就不实现了。

Guava Cache

Guava Cache是google guava中的一个内存缓存模块,用于将数据缓存到JVM内存中。 实际项目开发中经常将一些公共或者常用的数据缓存起来方便快速访问。

 

Guava Cache是单个应用运行时的本地缓存 。它不把数据存放到文件或外部服务器。
如果这不符合你的需求,请尝试 Memcached 、Redis这类工具。

Redis概述

Redis是一个开源的C语言编写、支持网络、可基于内存亦可持久化的日志型、key-value数据库,并提供多种语言API,发布订阅的消息通知 mq, 丰富的数据类型。 

本质是客户端-服务端应用软件程序。

特点使用简单 、功能强大、引用场景丰富、性能好等特点。

Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存和消息代理。Redis提供数据结构,如字符串、哈希、列表、集合、带范围查询的排序集合、位图、hyperloglogs、地理空间索引和流。Redis具有内置复制、Lua脚本、LRU逐出、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster的自动分区提供高可用性。

官网地址: https://redis.io
中文社区:http://www.redis.cn

Redis安装和启动

下载、解压、编译Redis

$ wget http://download.redis.io/releases/redis-6.0.6.tar.gz
$ tar xzf redis-6.0.6.tar.gz
$ cd redis-6.0.6
$ make

进入到解压后的 src 目录,通过如下命令启动Redis:

$ src/redis-server

可以使用内置的客户端与Redis进行交互:

$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"

这是来自redis官方的安装流程,其实本地直接下载zip包 ,上传解压就可以使用。

安装过程中 如果遇到make test 问题

 centos 用yum  进行获取就可以了

安装完成过后   注意src 和tests目录

tests目录中包含了 一些工具目录,而src目录中包含了启动 停止脚本,配置等参数

utils目录里面则包含 图像   以及创建集群 相关的工具,初始化 集群  create-cluster

 在src中 主要注意的是redis-cli 以及一些rdb 的数据

 当跑起来过后就会有 

怎么启动redis ,直接使用sudo src/redis-server  conf/redis.conf

 就可以把对应的redis跑起来了

如果客户端怎么连接 server 直接采用 命令客户端,

而默认的 conf 就是采用 redis.conf 

 Redis的使用

通用命令

 del key 删除任何个key   dump  key 序列化key   ttl key  等命令  type key 

直接使用 redis-cli进行 启动客户端

 经常使用得keys  * 查看所有得key .对于少量得key 可以采用这个命令,如果key数据量太大,还是不能采用这个命令。

通过  get  waha  查询  key得值。 以及

ttl 查看 key得生存时间

以及set   ex 过期时间 以秒为单位 

 数据结构 - String

通用得string数据类型 包括下面的 api 

String 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
使用场景 :微博数,粉丝数(常规计数)

 

 直接使用就可以  incr hash  decr hash 

包括 设置 setNX 只有 key值下面数据为空时,就会设置,不为空则不设置

而set   XX 和setNX 是功能相反,存在才赋值

redis客户端

redis 除了自带的redis-cli 自带客户端,还有各种语言构成的客户端

在java中存在的包括 jedis JRedis Redisson这些客户端。

 都是提供给java使用的。

 在java中,我在项目中一般用的是 jedis 。而对比着 redission  redission 对分布式上处理  锁这些,效果上比较好,而我在项目上用分布式锁这些比较少,根据项目的应用来用。

jedis有个好处 就是使用简单和redis-cli 基本一致, 而lettuce的好处在于对于新特性的集成非常全面。

只需要在版本中引用 对应客户端就可以了

    <jedis.version>2.9.2</jedis.version>
    <lettuce.version>5.1.4.RELEASE</lettuce.version>

<dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>${jedis.version}</version>
    </dependency>
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>${lettuce.version}</version>
    </dependency>

需要时直接new jedis就行 ,而且和官方的命令是一致的

public class JedisTests {

	public static void main(String[] args) {
		Jedis jedis = new Jedis("192.168.1.1", 6379);
		jedis.rpush("test", "test");

		jedis.close();
	}
}

如果是在spring中使用redis

引入对应的redis-data

		 <dependency>
         <groupId>org.springframework.data</groupId>
         <artifactId>spring-data-redis</artifactId>
         <version>2.1.5.RELEASE</version>
     </dependency>

直接注解 stringredistemplate  

 <!-- 此处特意混合一下xml+java两种配置方式,理论上xml和java事是可以相互替代的     -->
    <bean id="stringRedisTemplate"
          class="org.springframework.data.redis.core.StringRedisTemplate"
          p:connection-factory-ref="redisConnectionFactory" >
    </bean>

或者 现在流行使用注解的方式  这里 对redistemplate 做了个序列化减少存储大小

@Configuration
@EnableCaching
public class RedisConfig {
	@Bean
	public LettuceConnectionFactory redisConnectionFactory() {
		System.out.println("使用单机版本");
		return new LettuceConnectionFactory(new RedisStandaloneConfiguration("192.168.1.12", 6379));
	}

	@Bean
	public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate redisTemplate = new RedisTemplate();
		redisTemplate.setConnectionFactory(redisConnectionFactory);
		// 可以配置对象的转换规则,比如使用json格式对object进行存储。
		// Object --> 序列化 --> 二进制流 --> redis-server存储
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
		return redisTemplate;
	}
}

在实际开发中还有一个比较常见也简单的客户端 

	@Autowired
	private StringRedisTemplate stringRedisTemplate;

public long addFollowers(String userId) {

		String key = "weibo:followers:" + userId;
		long num = stringRedisTemplate.opsForValue().increment(key);
		return num;
	}

使用方式则采用 opsForValue  完全使用的redis中string数据化结构

而针对的redis中的key,不建议太长,也不要太短。根据内存空间。建议的key 有结构。 hash-ss

带有结构,我在实际应用中会待 时间以及uuid来做

Spring缓存注解

@Cacheable 标记的方法在执行后Spring Cache将缓存其返回结果

@CacheEvict 标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。
@CachePut 标记的方法每次都会执行,执行成功后更新对应的缓存。
相关属性
Key:缓存键
CacheManager:使用的缓存组件,redis、guava
Value:单独的缓存前缀
spring提供 这个缓存注解的意义在于减少代码的写入,对一些简单的缓存代码 直接可以帮我们生成
 */
    // value~单独的缓存前缀
    // key缓存key 可以用springEL表达式 cache-1:123
    @Cacheable(cacheManager = "cacheManager", value = "cache-1", key = "#userId")
    public User findUserById(String userId) throws Exception {
        // 读取数据库
        User user = new User(userId, "张三");
        System.out.println("从数据库中读取到数据:" + user);
        return user;
    }

    @CacheEvict(cacheManager = "cacheManager", value = "cache-1", key = "#userId")
    public void deleteUserById(String userId) throws Exception {
    	// 先数据库删除,成功后,删除Cache
    	// 先判断Cache里面是不是有?有则删除
    	System.out.println("用户从数据库删除成功,请检查缓存是否清除~~" + userId);
    }

    // 如果数据库更新成功,更新redis缓存
    @CachePut(cacheManager = "cacheManager", value = "cache-1", key = "#user.userId", condition = "#result ne null")
    public User updateUser(User user) throws Exception {
    	// 先更新数据库,更成功
    	// 更新缓存
        // 读取数据库
        System.out.println("数据库进行了更新,检查缓存是否一致");
        return user; // 返回最新内容,代表更新成功
    }

这里cacheManger  如果需要使用,需要将cache注册进去 将redis注册


    // 配置Spring Cache注解功能
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
        return cacheManager;
    }

Cacheable:在查询之前 判断redis中是否有数据,有就返回,没有就去查询过后并添加到redis中 ,并返回数据 

CacheEvict :对应着我们的数据删除  先数据库删除,成功后,删除Cache 先判断Cache里面是不是有?有则删除

CachePut :对应着 数据更新       先更新数据库,更成功更新缓存读取数据库

将访问持久层数据库代码和 缓存代码分隔开,减少我们代码量

总结

整篇文章主要对缓存做了大概介绍,以及redis的初步使用,以及在spring中如何进行使用的。本篇文章介绍比较基础简单。后面我会继续更新redis 的使用,以及分析redis的核心思想。集群的使用。以及常见问题处理。

Guess you like

Origin blog.csdn.net/qq_33373609/article/details/120600813