认识Redis
Redis是一门非关系性数据库(NoSql)
特征:
键值(key-value)型,value支持多种不同数据结构,功能丰富
单线程,每个命令具备原子性
低延迟,速度快(基于内存、IO多路复用、良好的编码)。
支持数据持久化
支持主从集群、分片集群
支持多语言客户端
存储方式:
关系型数据库基于磁盘进行存储,会有大量的磁盘IO,对性能有一定影响
非关系型数据库,他们的操作更多的是依赖于内存来操作,内存的读写速度会非常快,性能自然会好一些
扩展性
关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦
Redis的官方网站地址:https://redis.io/
安装的话我就不教大家了相信大家都会,去liunx装一个然后把包放进去用远程连接一下,完事。
Redis常用命令
KEYS:查看符合模板的所有key
DEL:删除一个指定的key
EXISTS:判断key是否存在
EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
TTL:查看一个KEY的剩余有效期
HELP:查看key的帮助
String类型
SET:添加或者修改已经存在的一个String类型的键值对
GET:根据key获取String类型的value
MSET:批量添加多个String类型的键值对
MGET:根据多个key获取多个String类型的value
INCR:让一个整型的key自增1
INCRBY:让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
SETEX:添加一个String类型的键值对,并且指定有效期
String类型,也就是字符串类型,是Redis中最简单的存储类型。
存储就是一个string 如下:
Hash类型
HSET key field value:添加或者修改hash类型key的field的值
HGET key field:获取一个hash类型key的field的值
HMSET:批量添加多个hash类型key的field的值
HMGET:批量获取多个hash类型key的field的值
HGETALL:获取一个hash类型的key中的所有的field和value
HKEYS:获取一个hash类型的key中的所有的field
HINCRBY:让一个hash类型key的字段值自增并指定步长
HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构 。
Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD 。
HSAH存储的就是一个对象一样 (key, value) 如下:
List类型
LPUSH key element ... :向列表左侧插入一个或多个元素
LPOP key:移除并返回列表左侧的第一个元素,没有则返回nil
RPUSH key element ... :向列表右侧插入一个或多个元素
RPOP key:移除并返回列表右侧的第一个元素
LRANGE key star end:返回一段角标范围内的所有元素
BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
是一个 有序 元素也可以重复 插入和删除快 查询速度一半 和java中的差不多
list存储的是一个有序的列表 如下:
Set类型
SADD key member ... :向set中添加一个或多个元素
SREM key member ... : 移除set中的指定元素
SCARD key: 返回set中元素的个数
SISMEMBER key member:判断一个元素是否存在于set中
SMEMBERS:获取set中的所有元素
SINTER key1 key2 ... :求key1与key2的交集
Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征
无序
元素不可重复
查找快
支持交集、并集、差集等功能
存储的是无序的列表:
SortedSet类型
ZADD key score member:添加一个或多个元素到sorted set ,如果已经存在则更新其score值
ZREM key member:删除sorted set中的一个指定元素
ZSCORE key member : 获取sorted set中的指定元素的score值
ZRANK key member:获取sorted set 中的指定元素的排名
ZCARD key:获取sorted set中的元素个数
ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
ZINCRBY key increment member:让sorted set中的指定元素自增,步长为指定的increment值
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
ZDIFF、ZINTER、ZUNION:求差集、交集、并集
注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可,例如:
升序获取sorted set 中的指定元素的排名:ZRANK key member
降序获取sorted set 中的指定元素的排名:ZREVRANK key memeber
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。
可排序
元素不重复
查询速度快
存储结构如下:
JAVA操作Redis
Jedis
先导入依赖:
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
创建连接
private Jedis jedis;
@BeforeEach
void test() {
// 1.建立连接
// jedis = new Jedis("192.168.156.234", 6379);
jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("000000");
// 3.选择库
jedis.select(0);
}
正式编写:
@Test
void testString() {
// 存入数据
String result = jedis.set("name", "小零子");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}
@Test
void testHash() {
// 插入hash数据
jedis.hset("user:1", "name", "one");
jedis.hset("user:1", "age", "22");
// 获取
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}
RedisTemplate
package com.heima;
import com.heima.redis.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class RedisDemoApplicationTests {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "小零子");
// 获取string数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
@Test
void testSaveUser() {
// 写入数据
redisTemplate.opsForValue().set("user:100", new User("小零子", 21));
// 获取数据
User o = (User) redisTemplate.opsForValue().get("user:100");
System.out.println("o = " + o);
}
}
运行结构:
以上这些就是Redis基础部分。
Redis锁
以下是我自己写的一个工具类,有需要的可以拿走。这里自己去写一个接口然后就可以了,里面部分内容使用了lua脚本保证原子性,不需要的可以删除,有普通的锁,这里基本就是一个获取锁一个释放锁的操作:
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class RedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
// 静态代码块 对这个提前加载
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public RedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeout) {
// 获取线程id
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 设置锁
Boolean success = stringRedisTemplate
.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeout, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
// 既能解决在判断之前不出错也能保证判断之后还不会出错 lua方法
@Override
public void unlock() {
// 获取线程id
String threadId = ID_PREFIX + Thread.currentThread().getId();
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
// 只能解决在判断直接的id是否相同
// @Override
// public void unlock() {
// // 获取线程id
// String threadId = ID_PREFIX + Thread.currentThread().getId();
// // 拿到redis中的数据 判断数据存储的id是否和当前线程id一致 防止误删
// String redisCacheId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// if (threadId.equals(redisCacheId)) {
// // 删除
// stringRedisTemplate.delete(KEY_PREFIX + name);
// }
// }
}
锁
可重入锁
- 底层用hash存储当获取锁的时候如果已经有锁了那么判断是否是自己的锁(判断线程id){如果是自己的就对redis的hash里面次数+1然后执行自己的业务,并且在释放的时候先减一然后判断次数是否为0是0的话直接删除,不是的话不做处理},如果不是自己的线程则false锁重试
锁重试
- 底层使用等待和重试机制,等待就是等待释放锁的时候那个发的消息,重试是判断时间是否已经超时了,超时了直接返回false,但是不是一直等待,是先等待一会,然后如果收到了消息那就在判断看看自己有没有超时没有超时才做重试机制
看门口机制
- 当时间为-1的时候才会有看门口狗,底层是递归,默认超时时间是30秒(30*1000),当时执行看门口狗的时候会一直不超时(续时) 机制是30/3,一直递归循环,在释放锁的时候才会取消看门狗
锁释放
- 会取消订阅并取消看门口机制
缓存穿透
原因产生:缓存穿透就是redis中没有数据库中也没有然后大量访问打到数据库导致崩溃
解决方案:
我们可以先查询redis中有没有没有的话再去查询数据库,如果数据库中也没有那么我们把这个设置为空值放入到redis以防止继续访问,如果他在访问的话就直接到redis缓存了直接返回空,那么我们也需要把这个key设置一个过期时间,不然如果不设置那就是永久不过期一直存在reids中时间一久那么redis也会很浪费内存,所以这里采用的方法是 空值+时间过期
部分业务代码:
// 缓存穿透
public <R, ID> R SelectWithCT(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFunction) {
String key = keyPrefix + id;
// 1. 从redis查询缓存商铺
String json = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3. 存在,直接返回
R r = JSONUtil.toBean(json, type);
return r;
}
// 判断命中的是否是空值
if (json != null) {
return null;
}
// 4. 不存在,则查询数据库
R r = dbFunction.apply(id);
if (r == null) {
// 不存在,放入空值放入redis放在缓存穿透
this.set(key, "", time, unit);
// 5. 不存在,返回错误
return null;
}
// 6. 存在,存入redis
this.set(key, r, time, unit);
// 7. 返回
return r;
}
缓存击穿
原因产生:热点key失效,大量访问打到数据库导致数据库崩溃
解决方案:
- 逻辑过期
- 这里就是我们把热点key设置为永不过期但是并不是永久过期是添加一个逻辑过期,也就是在存redis的时候添加一个过期时间进去,当我们查reids的时候就把这个时间拿出来看看,看看有没有过期,如果过期的话就去尝试获取锁,如果获取到了呢那么我们就进行查询数据库再把新的数据放入到redis,如果没有拿到的话就(因为可能别的已经获取到锁了)那么就返回旧数据,这里最大的因素就是可能数据不一致性,如果不要求数据不一致性要求性能这个肯定ok的。
- 互斥锁
- 就是发现失效了然后直接去查数据库的同时加锁,然后其他过来访问发现失效了了然后去尝试获取锁结果发现有人在做修改了,那么这个就会一直等待,直到那个拿锁的完成了业务这个才去重新查询reids然后再返回。那么这个最大的因素呢就是如果拿锁的那位操作了很长时间那么其他就需要等待很久,就会导致性能不好,但是保证一致性。
部分业务代码:
// 缓存击穿 逻辑过期方法(较难)
public <R, ID> R SelectWithJC(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit unit, Function<ID, R> dbFunction) {
String key = keyPrefix + id;
// 1. 从redis查询缓存商铺
String json = stringRedisTemplate.opsForValue().get(key);
// 2. 判断是否存在
if (StrUtil.isBlank(json)) {
// 3. 不存在,直接返回
return null;
}
// 4. 命中,需要先把json反序列化成对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime exprireTime = redisData.getExprireTime();
// 5. 判断是否过期
if (exprireTime.isAfter(LocalDateTime.now())) {
// 5.1 未过期,直接返回店铺信息
return r;
}
// 5.2 过期 需要缓存重建
// 6. 缓存重建
String lockKey = LOCK_SHOP_KEY + id;
// 6.1 获取互斥锁
boolean lockBoolean = tryLock(lockKey);
// 6.2 判断是否获取锁成功
if (lockBoolean) {
// 6.3 成功 ,开启独立线程,完成缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R rr = dbFunction.apply(id);
// 放入缓存
this.setWithExpire(key, rr, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unLock(lockKey);
}
});
}
// 6.4 返回店铺过期数据
return r;
}
缓存雪崩
原因产生:大量key同时失效,导致大量访问打到数据库,然后崩溃
解决方案:
在保存key的过期时间加个随机值,保证不会同时失效
Redis实现签到
签到是用bigmap来做,因为他底层是用二进制数,就是1 0 1 0 1 1 1这样子来表示的,那么我们就可以约定签到就是1 未签到就是0。
思路是:我们把当前时间(年月)+当前用户作为key,然后获取到今天是多少号,然后去签到,比如今天是12号 那么我们今天去签到就是 0 0 0 0 0 0 0 0 0 0 0 1 这样子表示,底层是1-31而我们这里则需要减一到0-30就可以实现啦,如果需要做统计的话使用redis的count就可以得到一个月的签到数量如果是7那么就是这个月签到了七天
部分业务代码:
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.获取日期
LocalDateTime now = LocalDateTime.now();
// 3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.写入Redis SETBIT key offset 1
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return Result.ok();
那么我们经常还会看到已经连续签到多少天了,是怎么实现的呢?
我们的思路就是:比如现在0 0 0 0 1 1 1那么就是连续签到了3天,我们肉眼的话一下子就能看出吧,但是用代码实现就需要好好想想了,那么我们就去从最后面查第一次出现0的位置就好啦,那么我们需要使用field来操作,因为这个功能有增删查功能集合于一身,这里我们用位运算来做,用与位运算来做,如果判断了为0那就是没签到,循环+位运算
部分业务代码:
// 5.获取本月截止今天为止的所有的签到记录,返回的是一个十进制的数字 BITFIELD sign:5:202203 GET u14 0
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
);
if (result == null || result.isEmpty()) {
// 没有任何签到结果
return Result.ok(0);
}
Long num = result.get(0);
if (num == null || num == 0) {
return Result.ok(0);
}
// 6.循环遍历
int count = 0;
while (true) {
// 6.1.让这个数字与1做与运算,得到数字的最后一个bit位 // 判断这个bit位是否为0
if ((num & 1) == 0) {
// 如果为0,说明未签到,结束
break;
} else {
// 如果不为0,说明已签到,计数器+1
count++;
}
// 把数字右移一位,抛弃最后一个bit位,继续下一个bit位
num >>>= 1;
}
Redis实现秒杀
那么我们知道秒杀是一个高并发的功能,一瞬间大量的访问打过来,而且我们还需要保证不超卖的实现还有一人一单的实现,那么这里我们需要用到lua保证原子性,还有分布式锁redission来操作,这里最难得就是超卖的问题了。
先去查看还有没有库存了有的话就走下一步,然后查看用户有没有购买过,没有的话就扣库存+订单 ,为防止用户开多线程抢所以这里还实现了加锁的操作。大家要真正实现的话最好就是加异步调用和lua保证原子性还要加消息队列通知这样子就可以做到真正的不超卖问题,异步呢就是我们先看看用户有没有购买资格有的话直接返回在通知异步去做订单扣减之类的,在加个消息队列确定,确定了才算是真正的成功,这个功能其实我写了的但是由于太复杂了就不放出来了,放个简单版的部分业务代码
简单实现版:
使用java代码写的查询
@Override
public Result createOrder(Long voucherId) {
// 1. 根据id查询秒杀卷信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
// 2. 判断时间是否开始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("抢购时间未开始!");
}
// 3. 判断时间是否结束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("抢购时间已经结束!");
}
// 4. 判断库存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("被抢完啦!下次早点来哟");
}
// 使用redisson锁
// 用户id
Long userId = UserHolder.getUser().getId();
// 获取锁
RLock lock = redissonClient.getLock("lock:order:" + userId);
// 无参表示不等待
boolean tryLock = lock.tryLock();
if (!tryLock) {
// 获取锁失败
return Result.fail("限购一件哟!");
}
// 获取锁成功
try {
IVoucherOrderService service = (IVoucherOrderService) AopContext.currentProxy();
return service.inspectOrder(voucherId);
} finally {
lock.unlock();
}
// 使用自己写的锁
// 用户id
Long userId = UserHolder.getUser().getId();
// 获取锁
RedisLock redisLock = new RedisLock("order" + userId, stringRedisTemplate);
boolean tryLock = redisLock.tryLock(15);
if (!tryLock) {
// 获取锁失败
return Result.fail("限购一件哟!");
}
// 获取锁成功
try {
IVoucherOrderService service = (IVoucherOrderService) AopContext.currentProxy();
return service.inspectOrder(voucherId);
} finally {
redisLock.unlock();
}
// 使用悲观锁
synchronized (userId.toString().intern()){
IVoucherOrderService service = (IVoucherOrderService) AopContext.currentProxy();
return service.inspectOrder(voucherId);
}
}
判断用户有没有购买过:
// 5. 判断用户是否已经购买过
// 用户id
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count >= 1) {
return Result.fail("已经购买过了,不能贪心哟!");
}
// 6. 扣减库存
boolean haveStock = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!haveStock) {
return Result.fail("被抢完啦!下次早点来哟");
}
// 7. 创建秒杀订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1 订单id
long nextId = redisIDWork.nextId("order");
voucherOrder.setId(nextId);
// 7.2 用户名id
voucherOrder.setUserId(userId);
// 7.3 代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 8. 返回订单id
return Result.ok(nextId);
以上就是redis基础部分和部分经常出现的问题总结,下集见。