基础篇
127.0.0.1:6379> LPUSH user 1 2 3 4 5 6
127.0.0.1:6379> smembers si
1) "b"
2) "d"
3) "c"
4) "a"
127.0.0.1:6379> sadd s1 a e l c
(integer) 4
127.0.0.1:6379> sinter si s1
1) "c"
2) "a"
可以用于好友列表找共同好友
排行榜作用
Jedis连接池
避免了jedis中类对象作为值要先转为json的麻烦
set的name和不同的不一样,存进去的被当作了对象做序列化
掌握此方法
package com.example;
import com.example.pojo.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
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.StringRedisTemplate;
@SpringBootTest
class redisStringTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void testString() {//存字符串
stringRedisTemplate.opsForValue().set("new_name","虎哥");
Object name=stringRedisTemplate.opsForValue().get("new_name");
System.out.println(name);
}
private static final ObjectMapper mapper=new ObjectMapper();//将对象转为json的工具
@SneakyThrows
@Test
void testUser() {//存字符串
User uu=new User("克洛克达尔",25);
//手动序列化
String json=mapper.writeValueAsString(uu);
stringRedisTemplate.opsForValue().set("海贼",json);
String jsonUser=stringRedisTemplate.opsForValue().get("海贼");
//手动反序列化
User u1=mapper.readValue(jsonUser,User.class);
System.out.println(u1);
}
}
对于存hash ,一个key有多个字段
实战篇
短信登录
session的缺点![](https://i-blog.csdnimg.cn/direct/2ef9515c765543b0ab733242a225f31d.png)
用redis替代session
redis是共享的
//保存用户信息到redis
//7.1 随机生成token,作为登录令牌
String token=UUID.randomUUID().toString();
//将User转为HashMap存储
UserDTO userDTO=BeanUtil.copyProperties(user,UserDTO.class);
Map<String,Object> usermap=BeanUtil.beanToMap(userDTO);//此时user中id是long类型
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey+token,usermap);
//设置日期,用户在30分钟内进行了操作就不会清除
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
return Result.ok();
拦截器优化
不需要做登录校验的页面,也需要刷新
商户查询缓存
缓存更新策略
线程不安全
企业使用缓存的难点问题
布隆过滤器是数据库的哈希,但不准确
String key=CACHE_SHOP_KEY + id;
//先从Redis中查,这里的常量值是固定的前缀 + 店铺id
String shopJson = stringRedisTemplate.opsForValue().get(key);
//如果不为空(查询到了),则转为Shop类型直接返回
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
if(shopJson!=null){
return Result.fail("店铺不存在!!");
}
//否则去数据库中查
Shop shop = getById(id);
//查不到返回一个错误信息或者返回空都可以,根据自己的需求来
if (shop == null){
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在!!");
}
//查到了则转为json字符串
String jsonStr = JSONUtil.toJsonStr(shop);
//并存入redis
stringRedisTemplate.opsForValue().set(key, jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
//最终把查询到的商户信息返回给前端
return Result.ok(shop);
互斥锁是自定义的
原理是:set一个键值对时,如果已经存在,就不会设置成功,否则就是成功,在这个基础上添加键值对生存时间,这样相当于就创立了一个锁,其他线程进行时,设置返回为false,代表没抢到锁
优惠券秒杀
全局唯一ID
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
public class RedisIdWorker {
private StringRedisTemplate stringRedisTemplate;
//设置起始时间,我这里设定的是2022.01.01 00:00:00
public static final Long BEGIN_TIMESTAMP = 1640995200L;
//序列号长度
public static final Long COUNT_BIT = 32L;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
//1. 生成时间戳
LocalDateTime now = LocalDateTime.now();
long currentSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStamp = currentSecond - BEGIN_TIMESTAMP;
//2. 生成序列号
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);
//3. 拼接并返回,简单位运算
return timeStamp << COUNT_BIT | count;
}
}
秒杀
秒杀券其实是普通券的附加信息, 普通券中被标注了的,就会在秒杀券表中寻找其信息
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
//5,扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣减库存
return Result.fail("库存不足!");
}
//6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 6.2.用户id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
// 6.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
}
因为要操作两个库,所以设置为事务,进行滚动
高并发的超卖问题
可以将库存本身当作版本号
但是弊端就是抢失败人数提升,100个人,只要有一个人修改了,其他人全部失败
改进:
stock>0
//5,扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!success) {
//扣减库存
return Result.fail("库存不足!");
}
一人一单
也是有并发的风险
加悲观锁
事物提交之后加锁
且锁只争对相同用户,用户ID为锁对象
将悲观锁加到抽象出的函数中,事物提交了才释放锁,但是此时该函数被调用是没有事务性的,需要拿到代理对象来调用该函数
只适合在单体,不适合集群
分布式
不同JVM锁监视器对象不同
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
public class SimpleRedisLock implements ILock{
private String name;//锁的名字,因为不同业务锁不同
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX="lock:";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
long threadId = Thread.currentThread().getId();
//获取锁,使用SETNX方法进行加锁,同时设置过期时间,防止死锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
//自动拆箱可能会出现null,这样写更稳妥
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//通过DEL来删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
Long userId = UserHolder.getUser().getId();
// 创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 获取锁对象
boolean isLock = redisLock.tryLock(120);
// 加锁失败,说明当前用户开了多个线程抢优惠券,但是由于key是SETNX的,所以不能创建key,得等key的TTL到期或释放锁(删除key)
if (!isLock) {
return Result.fail("不允许抢多张优惠券");
}
try {
// 获取代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
// 释放锁
redisLock.unlock();
}
存在的问题
释放了别人的锁
解决办法,释放锁的适合看看线程的标识是否和自己的一样
在集群模式下,线程ID是有相同的,所以使用UUID +ID
@Override
public boolean tryLock(long timeoutSec) {
//获取线程标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
//获取锁,使用SETNX方法进行加锁,同时设置过期时间,防止死锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
//自动拆箱可能会出现null,这样写更稳妥
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取当前线程的标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标识
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标识是否一致
if (threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
还存在问题,判断锁标识和释放锁是两个动作,必须要保证原子性
分布式优化
阻塞式拿锁
按照之前的方法,是不能实现重复获取锁的
哈希结构来存储,记录重复拿的次数,当到了最外层,value=0,就删除锁
为了保证原子性,用lua脚本
解决办法:要在每一个主节点上拿到锁才算真正获取
创建连锁
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach
void setUp() {
RLock lock1 = redissonClient.getLock("lock");
RLock lock2 = redissonClient2.getLock("lock");
RLock lock3 = redissonClient3.getLock("lock");
lock = redissonClient.getMultiLock(lock1, lock2, lock3);
}
异步秒杀
开辟独立线程 执行数据库mysql的读写,用阻塞队列装订单id等,写mysql,就不用那么急,不需要高时效性
添加阻塞队列(当里面有元素才能被执行取出操作)后,利用线程池,每个线程独立将信息存入数据库
消息队列实现异步秒杀
消息队列比阻塞队列的优点:1、不受jvm内存限制2、消息持久化,安全
steam做消息队列
达人探店
@TableField(exist = false)
private String icon;
/**
* 用户姓名
*/
@TableField(exist = false)
private String name;
/**
* 是否点赞过了
*/
@TableField(exist = false)
private Boolean isLike;
注解表示这些字段不属于该表,后期手动维护就行
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Override
public Result queryHotById(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(this::queryBlogUser);
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
Blog blog = getById(id);
if (blog == null) {
return Result.fail("评价不存在或已被删除");
}
queryBlogUser(blog);
return Result.ok(blog);
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}
点赞操作
* 是否点赞过了
*/
@TableField(exist = false)
private Boolean isLike;
点赞排行
共同好友
附近商铺
用户签到
节省内存 ,将每个用户的当前月作为key
签到统计
UV统计