NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
- 不遵循SQL标准。
- 不支持ACID。 原子性、一致性、隔离性、持久性;
- 远超于SQL的性能。
redis是一种典型的nosql数据库
nosql数据库的使用场景:用不着sql的和用了sql也不行的情况,请考虑用NoSql
redis缓存的特征
数据都在内存中,支持持久化,主要用作备份恢复
除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
一般是作为缓存数据库辅助持久化的数据库
redis中的基本数据类型:String List Set Hash zset;
jedis介绍
java中通过jedis来操纵redis数据库,就跟用jdbc来操纵mysql数据库一样。
// jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
利用java操作redis中的五大数据类型
public class JedisDemo {
public static void main(String[] args) {
//创建jedis对象,需要传递两个参数,分别是redis所在的主机ip和redis的端口号
Jedis jedis = new Jedis("192.168.44.168",6379);
// 测试是否连接上这个ip
//需要注意,在redis.conf中注释掉 bind 127.0.0.1 ,然后 protected-mode no ,这一才能远程访问
// 注释完需要重启
String ping = jedis.ping();
}
/**
* 操作字符串String 的常用方法
*/
@Test
public void demo1(){
Jedis jedis = new Jedis("192.168.44.168",6379);
/**
* redis 的五大数据类型 String List set hash zset;
*/
//向redis里面添加键值对
jedis.set("key","value");
// 获取键name对应的值
jedis.get("name");
// 设置多个key--value
jedis.mset("k1","v1","k2","v2");
//获取多个键对应的值,结果是一个列表
List<String> mget = jedis.mget("k1", "k2");
Set<String> keys = jedis.keys("*"); // 得到所有的key
for(String key:keys){
System.out.println(key);
}
}
/**
* 操作list数据
*/
@Test
public void demo2(){
Jedis jedis = new Jedis("192.168.44.168",6379);
// 注意l表示左边,r表示右边
//加入元素 在键key1 中加入 [v1,v2,v3]
jedis.lpush("key1","v1","v2","v3");
// 取值 0表示左边第一个,-1 表示右边第一个
List<String> key1 = jedis.lrange("key1", 0, -1);
}
/**
* 操作set集合
*/
@Test
public void demo3(){
Jedis jedis = new Jedis("192.168.44.168",6379);
jedis.sadd("key1","v1","v2"); //完集合中添加元素
Set<String> key1 = jedis.smembers("key1"); //获取值
jedis.srem("key1"); //删除
}
/**
* hash类型
*/
@Test
public void demo4(){
Jedis jedis = new Jedis("192.168.44.168",6379);
jedis.hset("key1","age","20"); //添加
String hget = jedis.hget("key1", "age");//获取value
}
/**
* zset 排序后的集合
*/
@Test
public void demo5(){
Jedis jedis = new Jedis("192.168.44.168",6379);
jedis.zadd("key",100d,"shanghai"); //第二个参数是排序的时候的权重
Set<String> key = jedis.zrange("key", 0, -1); // 取值
}
}
要求:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
使用random库,并在redis里面设置过期时间,从而达到需求
2、输入验证码,点击验证,返回成功或失败
从redis里面获取验证码,并与输入的比较看是否一直
3、每个手机号每天只能输入3次
利用一个incr指针,每次发送后+1.大于3就提交不能发送
public class PhoneCode {
public static void main(String[] args) {
verifyCode("13600001000");
getRedisCode("13600001000","123456");
}
// 1 生成6位的数字验证码
public static String getCode(){
Random random = new Random();
StringBuilder stringBuilder = new StringBuilder();
for(int i=0;i<6;i++){
int i1 = random.nextInt(10); //随机生成小于10的整数
stringBuilder.append(i1);
}
return stringBuilder.toString();
}
// 2 每个手机每天只能发送三次,验证码放到redis里面,设置过期时间
public static void verifyCode(String phone){
Jedis jedis = new Jedis("192.168.44.168",6379);
//设置两个key,第一个是手机发送次数
String countKey ="VerifyCode" +phone+"count";
// 第二个key是验证码
String codeKey ="VerifyCode" + phone +":code";
//每个手机每天只能发送三次
String count =jedis.get(countKey); //默认是空
if(count == null){
jedis.setex(codeKey,24*60*60,"1"); // 设置发送次数是1并且每天只能发送3次
}else if(Integer.parseInt(count)<=2){ //将字符串转换成整数
jedis.incr(countKey); //次数加一
}else{
System.out.println("今天的次数用完了");
jedis.close();
}
//将生成的验证码放到redis里面
String vcode =getCode();
jedis.setex(codeKey,120,vcode);
jedis.close();
}
// 3、校验验证码
public static void getRedisCode(String phone,String code){
Jedis jedis = new Jedis("192.168.44.168",6379);
String codeKey ="VerifyCode"+phone+":code";
String redisCode =jedis.get(codeKey);
// 判断
if(redisCode.equals(code)){
System.out.println("success");
}else{
System.out.println("fail");}
jedis.close();
}
}
SpringBoot整合redis缓存
首先需要引入两个依赖
<dependency> <!-- springboot整合redis的依赖--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring2.X集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.6.0</version> </dependency>
第二部需要在主配置文件中配置redis
#Redis服务器地址
spring.redis.host=192.168.140.136
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
第三步需要添加redis配置类 (固定的模板)
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean //springboot中提供的操纵redis的类
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
测试验证redis
//测试redis
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
redis中的事务操作
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。(乐观锁)
unwatch取消监视
-
- Redis事务三特性
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
利用秒杀来实现redis中的事务
分为库存和秒杀用户,每一个用户成功,则成功人数+1,同时库存要减少1
public class SecKill {
/**
* 秒杀过程 这个只是单一的过程,并没有并发控制
* @param uid 用户数量
* @param prodid 产品数量
* @return
* @throws IOException
*/
public static boolean doSecKill(String uid,String prodid) throws IOException {
// 1、判断是否为空
if(uid ==null || prodid ==null){return false;}
// 2、连接redis
Jedis jedis = new Jedis("129.168.44.168",6379);
// 3、拼接key
// 库存key
String kcKey ="sk"+prodid+":qt";
// 秒杀成功用户key
String userKey ="sk:"+prodid+":user";
// 4、获取库存,如果库存为null,秒杀还没开始
String s = jedis.get(kcKey);
if(s==null){
System.out.println("秒杀还没开始,请等待");
jedis.close();
return false;
}
// 5、判断用户是否重复秒杀
Boolean sismember = jedis.sismember(userKey, uid); // 判断键userkey里面是否由这个用户 用户存在于set里面
if(sismember){
System.out.println("已经秒杀成功,不能重复秒杀");
jedis.close();
return false;
}
// 6、判断如果商品数量,库存数量小于1,秒杀结束
int i = Integer.parseInt(s); // 获取当前库存
if(i<=0){
System.out.println("秒杀已经结束")
jedis.close();
return false;
}
//7、秒杀过程
// 库存-1
jedis.decr(kcKey);
// 把秒杀成功用户添加到清单里面
jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
}
}
yum install httpd-tools 安装AB测试的工具
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
// 利用双重校验写redis连接池
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true); // ping PONG
jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
利用连接池解决超时的问题
//获取连接的方法可以利用连接池获取jedis对象
JedisPool jedisPool =JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = JedisPool.getResource();
redis中默认使用乐观锁,不使用悲观锁
乐观锁会引起库存遗留
持久化操作RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
另一个持久化层是Aof
主从复制
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
将读写分离;
主从复制的原理:
1、从服务器连接上主服务器之后,从服务器向主服务器发送进行数据同步的消息
2、主服务器接受到从服务器发送过来同步消息,把主服务器数据进行持久化,rdb文件,把rdb文件发送给从服务器,从服务器拿到rdb进行读取,
3、每次主服务器进行写操作后,和从服务器进行数据同步
(注意以上都是主服务器主动更新从服务器)
哨兵模式是主机挂掉之后,从机自动切换成主机,然后提供服务
利用jedis操作redis的集群
redis的集群是无中心化的
public class JedisClusterDemo {
public static void main(String[] args) {
// 创建对象
HostAndPort hostAndPort = new HostAndPort("192.168.44.168",6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
jedisCluster.set("key","value");
jedisCluster.get("bi");
jedisCluster.close();
}
}
redis应用过程中的主要问题
缓存击穿
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案:
设置可访问的名单
采用布隆过滤器
缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:
- 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
- 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
- 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
- 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法
缓存雪崩 很多key集中失效
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕!
解决方案:
- 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
- 使用锁或队列
- 设置过期标志更新缓存
- 将缓存失效时间分散开
setnx 可以设置分布式锁 setnx key 10 对key加锁 del key 删除锁