redis操作全是在本地虚拟机下进行,虚拟机安装,安装遇见问题可以参考以下链接内容
虚拟机安装解决linux虚拟机上网问题和xshell连接虚拟机VMware虚拟机中linux CentOS7上网联网,简单粗暴亲测有效redis安装
一:认识redis
1.NoSql
2.redis(远程词典服务器,键值型NoSql数据库)
二:redis客户端(用于操作redis)
- 命令行客户端
- 图形化桌面客户端
redis图形化软件 - 编程客户端
三:redis命令
- redis数据结构
- 通用命令
- String类型(三种格式:字符串,int,float)
- Key的层级格式
- Hash类型
- List类型
- Set类型
- SortedSet类型(特点:可排序,元素不可重复,查询速度快)
- eg:
四:redis的Java客户端(Jedis)
1.JedisPool连接池
为什么要使用JedisPool连接词呢?-----Jedis本身是线性不安全的,频繁的创建和销毁连接会有性能损耗。
连接池相关设置
2.SpringDataRedis(以下采用springboot框架)
A:快速入门
- 引入依赖
- 配置Redis相关信息(spring默认连接池是lettuce,也可修改为redis,需要手动配置才可以生效)
- 注入RedisTemplate,编写代码
B:SpringDataRedis的序列化与反序列化
a:方案一(缺点:占用内存,存对象时会存入对象的类标识,以实现自动反序列化)
redisTemplate的set方法采用的是jdk的序列化器----导致相同的key存入reidis时被序列化成不同字节
解决办法:重写RedisTemplate方法
b:方案二(缺点:比较麻烦,需要手动处理)
五:Redis实战 (黑马点评项目 )
1.基于Redis实现短信功能
- 流程图
- 代码实现
package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//2.不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码(hutool-all依赖中的方法)
String code = RandomUtil.randomString(6);
//4.保存到session/redis
//session.setAttribute("code",code);
redisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);
//5.发送验证码
log.debug("发送验证码成功,验证码:{}",code);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//2.失败,返回错误信息
return Result.fail("手机号码格式错误");
}
//3.校验验证码(从session或redis中获取)
//String cacheCode =(String) session.getAttribute("code");
String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if(cacheCode==null||!cacheCode.equals(loginForm.getCode())){
//4.不一致,报错
return Result.fail("验证码错误");
}
//5.查询用户根据手机号
User user = query().eq("phone", phone).one();
//6.判断用户是否存在
if(user==null){
//7.不存在,创建用户
user = createUserByPhone(phone);
}
//8.保存用户到session/redis
//session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
//8.1随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
//8.2将user对象转为Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
//8.3存储
String tokenKey=LOGIN_USER_KEY+token;
redisTemplate.opsForHash().putAll(tokenKey,map);
//8.4设置token有效期
redisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
//9.返回token
return Result.ok(token);
}
private User createUserByPhone(String phone){
//1.创建用户
User user=new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +RandomUtil.randomString(10));
//2.保存用户(mybatis-plus提供)
save(user);
return user;
}
}
六.缓存(存于Redis中)
1. 缓存定义
2. 缓存实例
@Override
public Result queryById(Long id) {
String key=CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.数据库不存在,返回错误
if (shop==null){
return Result.fail("店铺不存在!");
}
//7.返回
return Result.ok(shop);
}
3. 缓存更新策略
线程发生安全情况问题
//6.存在,存入redis,设置时间(CACHE_SHOP_TTL是时间30分钟)
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if(id==null){
return Result.ok("店铺不能为空");
}
//1.更新数据库
updateById(shop);
//2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY +id);
return Result.ok();
}
4. 缓存穿透
指客户端请求的数据在缓存中和 数据库中都不存在,这样缓存永远不会有效,这些请求会达到数据库。
常见解决方案
- 缓存空对象
优点:实现简单,维护方便
缺点:额外的内存消耗;
可能造成短期的不一致(刚请求的id数据库不存在,此时数据库向改id插入数据,这样会导致查不到) - 布隆过滤
优点:内存占用少,没有多余key
缺点:实现复杂,存在可能误判(不存在一定不存在) - 实例:
@Override
public Result queryById(Long id) {
String key=CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if (shopJson!=null){
//返回错误信息
return Result.fail("店铺信息不存在!");
}
//4.不存在,根据id查询数据库
Shop shop = getById(id);
//5.数据库不存在,返回错误
if (shop==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//发送错误信息
return Result.fail("店铺不存在!");
}
//7.返回
return Result.ok(shop);
}
总结
5. 缓存雪崩
指同一时段大量的缓存key同时失效或者Redis服务宕机(redis停掉),导致大量请求到达数据库,带来巨大压力。
6. 缓存击穿
缓存击穿也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无法请求访问会在瞬间给数据库带来巨大冲击。
- 常见解决方案:
- 互斥锁
案列
---------------------
- 采用String数据结构中的setnx命令(已存在的key不可以更新值,只有删除才可)
private boolean getLock(String key){
//开启锁
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
//直接返回会拆箱
return BooleanUtil.isTrue(flag);
}
//定义解锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
//缓存击穿互斥锁
public Shop queryWithMutex(Long id){
String key=CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//判断命中的是否是空值(解决穿透)
if (shopJson!=null){
//返回错误信息
return null;
}
//4.未命中,实现缓存重建
//4.1.获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
Shop shop = null;
try {
boolean islock = getLock(lockKey);
//4.2.判断是否获取成功
if (!islock){
//4.3.失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//4.4.成功,根据id查询数据库
shop = getById(id);
//模拟重建延时,因为我们是本地查询数据库,速度快
Thread.sleep(200);
//5.数据库不存在,写空值(解决缓存穿透)
if (shop==null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//发送错误信息
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//7.释放互斥锁
unlock(lockKey);
}
//8.返回
return shop;
}
- 逻辑过期
案例
-------------
在service层实现逻辑过期处理换成击穿
//定义线程池完成
private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
//逻辑过期解决缓存击穿
public Shop queryWithLogicalExpire(Long id){
String key=CACHE_SHOP_KEY + id;
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isBlank(shopJson)){
//3.不存在,返回
return null;
}
//4.存在,判断过期时间,json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1.未过期,直接返回店铺信息
return shop;
}
//5.2.过期,需要缓存重建
//6.缓存重建
//6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean islock = getLock(lockKey);
//6.2.判断是否获取成功
if (islock){
// TODO 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unlock(lockKey);
}
});
}
//6.4.返回过期的商铺信息
return shop;
}
两种解决方法对比
## 7.封装redis工具类
案例
```java
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.hmdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
//将数据存入Reis
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
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 expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
}
七.基于Redis实现优惠卷秒杀功能
- 优惠卷的id不可重复,必须唯一,采取Redis中String数据结构incr来构成id
全局id必须具有特性
高可用:必须一直可用,不可能我需要的时候挂掉了
高性能:生成id必须够快
Redis实现全局id
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
//1.1.得到当前时间
LocalDateTime now = LocalDateTime.now();
//1.2获得当前时间的秒数,括号内是时区
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回(时间戳在右侧,需要位移32位,拼接序列号)
return timestamp << COUNT_BITS | count;
}
}
2. 实现秒杀功能
实现流程图
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1.根据id查询优惠卷
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 isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId).update();
if(!isSuccess){
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);
}
上述秒杀会出现超卖问题,超卖产生原因如下,多个线程同时进入查询数据库,线程1查到了数据还没执行,线程2也去查询数据,因此线程1,2查到的数据一样,然后又同时减去库存,导致超卖
3.处理上述秒杀超卖问题(加锁处理)
乐观锁
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1.根据id查询优惠卷
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 isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock",0).update();
if(!isSuccess){
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);
}
4.秒杀实现一人一单
先晒流程
public Result seckillVoucher(Long voucherId) {
//1.根据id查询优惠卷
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一人一单
Long userId = UserHolder.getUser().getId();
//5.1查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count>0){
//用户购买过
return Result.fail("用户已经购买了一次!");
}
//6.扣减库存(处理前语句)
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId).update();
//6.扣减库存(处理后语句)
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock",0).update();
if(!isSuccess){
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2.用户id
voucherOrder.setUserId(userId);
//7.3.代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
上述代码块会出现并发线程不安全情况
解决上述办法就是加锁(悲观锁)
public Result seckillVoucher(Long voucherId) {
//1.根据id查询优惠卷
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("库存不足!");
}
Long userId = UserHolder.getUser().getId();
//悲观锁(放在这的原因:要等执行完事务并提交才释放锁,下一个线程才可以进行,
// 放在其他地方会导致事务没提交,下一个线程就进来了)
synchronized (userId.toString().intern()) {
//spring执行事务,要获取代理对象,这里是获取代理对象
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
return proxy.CreatVoucherOrder(voucherId);
}
}
@Transactional
public Result CreatVoucherOrder(Long voucherId){
//5一人一单
Long userId = UserHolder.getUser().getId();
//5.1查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count > 0) {
//用户购买过
return Result.fail("用户已经购买了一次!");
}
//6.扣减库存(处理前语句)
/*boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId).update();*/
//6.扣减库存(处理后语句)
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
if (!isSuccess) {
return Result.fail("库存不足");
}
//7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//7.2.用户id
voucherOrder.setUserId(userId);
//7.3.代金卷id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}
5.上述处理方式适合单体系统,集群下行不通
>原因: 每一个端口都会是一个全新的JVM,导致锁监视器是全新的,所以锁不起效
解决上述问题采用如下方法(分布式锁)
分布式锁基本特点及实现的多种方式
6.基于Redis实现分布锁
JDK提供的锁有两种方式:1:阻塞模式(没找到锁,等待直到有人释放锁);2.非阻塞模式(没找到锁,就返回信息)
实现代码
- 创建接口
package com.hmdp.utils;
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超市时间,过期自动释放
* @return
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
- 实现接口
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
/**
* name 锁的key
*/
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) {
//获取线程id
long id = Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
- 调用方法(基于优惠卷秒杀一人一单)
public Result seckillVoucher(Long voucherId) {
//1.根据id查询优惠卷
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("库存不足!");
}
Long userId = UserHolder.getUser().getId();
//获取锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁,到时间释放
boolean isLock = lock.tryLock(1200);
//判断是否成功
if (!isLock){
//失败
return Result.fail("不能重复购买!");
}
try {
//spring执行事务,要获取代理对象,这里是获取代理对象
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
return proxy.CreatVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
`
上述代码存在误删锁的问题(改进如下)
流程如下
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
/**
* name 锁的key
*/
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程id
String id = ID_PREFIX+Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//获取线程id
String threadId = ID_PREFIX+Thread.currentThread().getId();
//获取锁中的标时
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//判断标识是否一致
if (threadId.equals(id)){
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
}
上述问题解决了之后还可能存在延迟删锁问题
采用lua脚本语言解决
–
lua脚本
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0
修改锁接口实现类的释放锁方法
public class SimpleRedisLock implements ILock{
/**
* name 锁的key
*/
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 SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程id
String id = ID_PREFIX+Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//调用lua脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name),
ID_PREFIX+Thread.currentThread().getId());
}
}
分布式锁总结
**
七:六中实现Redis分布式锁的优化
1.上述分布式锁存在问题
2.采用Redisson框架解决
3.Redisson入门
- 导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
- 编写配置
package com.hmdp.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.242.128:6379").setPassword("123321");
//创建Redisson对象
return Redisson.create(config);
}
}
- 使用
@Test
void redissonClient() throws InterruptedException {
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("order");
//尝试获取锁,参数是:获取锁的最大等待时间(期间会重复),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
//判断是否获取成功
if(isLock){
//防止执行事务发送异常
try {
System.out.println("succeed!");
} finally {
//释放锁
lock.unlock();
}
}
}
4.Redisson可重入原理(采用hash数据结构,每次获取锁判断是否是同一个用户,是的话value加1,释放锁value减1)
-执行的lua脚本
5.Redis的锁重试和watchDog(看门狗:(不设置锁释放时间启用)分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间)原理
6.Redisson解决主从一致)问题(多个redis节点,一台为主节点,主节点处理写操作,从节点处理读操作,主从节点要同步,有可能宕机,同步失败),MultiLock解决原理
7.总结
**
八.阻塞队列实现秒杀业务优化,提升性能
1.原秒杀架构
2.为了提高性能现改进如下
3.秒杀功能流程(开启新的线程池操作数据库,实现异步下单)
- lua脚本
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]
-- 2.数据key
-- 2.1.库存key ..是拼接
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
- 业务代码
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
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 lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redisson;
//加载lua脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT=new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
private IVoucherOrderService proxy;
//阻塞队列,()是大小
private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);
//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
}
private class VoucherOrderHandle implements Runnable{
@Override
public void run() {
while(true){
try {
//1.获取队列中订单信息
VoucherOrder take = orderTasks.take();
//2.创建订单
handleVoucherOrder(take);
} catch (Exception e) {
log.error("处理订单异常",e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//1.获取用户id
Long userId = voucherOrder.getUserId();
//创建锁对象
RLock lock = redisson.getLock("order:" + userId);
//获取锁,到时间释放
boolean isLock = lock.tryLock();
//判断是否成功
if (!isLock){
//失败
log.error("不允许重复下单");
return ;
}
try {
proxy.CreatVoucherOrder(voucherOrder);
} finally {
//释放锁
lock.unlock();
}
}
/**
* 优化秒杀功能后
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
//1.执行lua脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(), voucherId.toString(), userId.toString());
//2.判断结果是否为0
if (result.intValue()!=0){
//2.1不为0,代表没资格购买
return Result.fail(result.intValue()==1?"库存不足":"不能重复下单");
}
//TODO 保存阻塞队列
//2.2.为0,有购买资格,把下单信息保存到阻塞队列
VoucherOrder voucherOrder = new VoucherOrder();
//2.3.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//2.4.用户id
voucherOrder.setUserId(userId);
//2.5.代金卷id
voucherOrder.setVoucherId(voucherId);
//2.6.放入阻塞队列
orderTasks.add(voucherOrder);
//3.获取代理对象
proxy =(IVoucherOrderService) AopContext.currentProxy();
//4.返回订单id
return Result.ok(orderId);
}
@Transactional
public void CreatVoucherOrder(VoucherOrder voucherOrder){
//5一人一单
Long userId = UserHolder.getUser().getId();
Long voucherId = voucherOrder.getVoucherId();
//5.1查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count > 0) {
//用户购买过
log.error("不能重复购买");
}
//6.扣减库存(处理后语句)
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
if (!isSuccess) {
log.error("库存不足");
}
save(voucherOrder);
}
}
4.总结(队列用的是jvm的内存,不限制会导致内存溢出)
九.消息队列实现异步秒杀,提升性能
1. 消息队列是JVM以为的独立服务,不受JVM内存限制
2. 消息队列确保消息投递以后,消费者确认后才可以销毁数据
Redis实现消息队列的方式
- List结构:基于list结构模拟消息队列
- PubSub:基于点对点消息模型
- Stream:比较完善的消息队列模型
> 消息队列
- 基于List的消息队列
- 基于PubSub的消息队列
- 不存储数据,没有持久化,消息没人接收直接丢失,消费者接收到信息会缓存下来,超出空间会丢失。
- 基于Stream的消息队列–单消费者模式(消息一直存在,可供多消费者获取)
- 基于Stream的消息队列–消费组模式
基于Stream数据结构实现消息队列-----消费组
- lua语句
-- 1.参数列表
-- 1.1.优惠券id
local voucherId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 1.3.订单id
local orderId = ARGV[3]
-- 2.数据key
-- 2.1.库存key ..是拼接
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.订单key
local orderKey = 'seckill:order:' .. voucherId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
- 实现代码
package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.Result;
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 lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redisson;
//加载lua脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT=new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
private IVoucherOrderService proxy;
//线程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();
@PostConstruct
private void init(){
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
}
String queueName="stream.orders";
private class VoucherOrderHandle implements Runnable{
@Override
public void run() {
while(true){
try {
//1.获取消息队列中订单信息 XREADGROUP GROUOP g1 c1 count 1 block 2000 STREAMS stream.orders >
// (g1组中c1从stream.orders读取1个数据,阻塞2s)
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
//2.判断消息是否获取成功
if(list==null||list.isEmpty()){
//2.1.失败,没消息,继续下一次循环
continue;
}
//3.解析list中信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//4.成功,下单
handleVoucherOrder(voucherOrder);
//5.ack确认(从pending-list中除掉)
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
} catch (Exception e) {
log.error("处理订单异常",e);
handlePendingList();
}
}
}
}
private void handlePendingList() {
while(true){
try {
//1.获取pending-list中订单信息 XREADGROUP GROUOP g1 c1 count 1 STREAMS stream.orders 0
// ()
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create(queueName,ReadOffset.from("0"))
);
//2.判断消息是否获取成功
if(list==null||list.isEmpty()){
//2.1.失败,pending-list没消息,结束循环
break;
}
//3.解析list中信息
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
//4.成功,下单
handleVoucherOrder(voucherOrder);
//5.ack确认(从pending-list中除掉)
stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
} catch (Exception e) {
log.error("处理pending-list订单异常",e);
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
//1.获取用户id
Long userId = voucherOrder.getUserId();
//创建锁对象
RLock lock = redisson.getLock("order:" + userId);
//获取锁,到时间释放
boolean isLock = lock.tryLock();
//判断是否成功
if (!isLock){
//失败
log.error("不允许重复下单");
return ;
}
try {
proxy.CreatVoucherOrder(voucherOrder);
} finally {
//释放锁
lock.unlock();
}
}
/**
* 优化秒杀功能后(Stream消息队列--群消费)
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
//获取用户
Long userId = UserHolder.getUser().getId();
//获取订单id
long orderId = redisIdWorker.nextId("order");
//1.执行lua脚本
Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
Collections.emptyList(), voucherId.toString(), userId.toString(),String.valueOf(orderId));
//2.判断结果是否为0
int r = result.intValue();
if (r!=0){
//2.1不为0,代表没资格购买
return Result.fail(r ==1?"库存不足":"不能重复下单");
}
//3.获取代理对象
proxy =(IVoucherOrderService) AopContext.currentProxy();
//4.返回订单id
return Result.ok(orderId);
}
@Transactional
public void CreatVoucherOrder(VoucherOrder voucherOrder){
//5一人一单
Long userId = UserHolder.getUser().getId();
Long voucherId = voucherOrder.getVoucherId();
//5.1查询订单
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2判断是否存在
if (count > 0) {
//用户购买过
log.error("不能重复购买");
}
//6.扣减库存(处理后语句)
boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
.eq("voucher_id", voucherId)
.gt("stock", 0).update();
if (!isSuccess) {
log.error("库存不足");
}
save(voucherOrder);
}
}
**
十:实现达人探店功能
1.实现点赞功能(采用sortedset结构实现–点赞需要唯一,并且需要显示出前5名点赞的)
public Result likeBlog(Long id) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断用户是否点赞
String key=BLOG_LIKED_KEY+id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
if(score==null){
//3.未点赞,可以点赞
//3.1.数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
//3.2.保存用户到redis
if (isSuccess){
stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
}
}else {
//4.已点赞,取消点赞
//4.1.数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//4.2.删除用户从redis
if (isSuccess){
stringRedisTemplate.opsForZSet().remove(key,userId.toString());
}
}
//5.返回
return Result.ok();
}
2.实现关注,取关(采用Redis中Set结构–关注需要唯一)
public Result follow(Long followUserId, Boolean isFollow) {
//获取登录用户id
Long userId = UserHolder.getUser().getId();
//redis的key
String key="follow"+userId;
//1.判断是关注or取关
if (isFollow){
//2.关注
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if(isSuccess){
//将关注用户id放入redis中set集合 sadd userId followerUserId
stringRedisTemplate.opsForSet().add(key,followUserId.toString());
}
}else {
//2.取关
boolean isSuccess = remove(new QueryWrapper<Follow>().
eq("user_id", userId).
eq("follow_user_id", followUserId));
if (isFollow){
//把关注用户id移除从redis
stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
return Result.ok();
}
3.显示互相关注好友(求两个用户交集)
@Override
public Result followCommonS(Long id) {
//1.获取当前登录用户id
Long userId = UserHolder.getUser().getId();
String key="follow"+userId;
//2.获取博主id
String key2="follow"+id;
//3.查询交集
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
if(intersect.isEmpty()||intersect==null){
return Result.ok(Collections.emptyList());
}
//4.解析id
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//5.查询用户
List<UserDTO> userDTOS = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOS);
}
4.关注推送(采取feed流)
- feed流模式
- 拉模式
- 推模式
- 推拉结合
- 总结
5.推模式实现推送,以及查询推送博文(博文查询采用feed流滚动分页查询),因此采用sortedset结构
- 普通分页查询(通过下标查询,在新增数据情况下会出错)
- 滚动分页查询采取的措施是对数据查询
public Result saveBlog(Blog blog) {
//1.获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
//2.保存探店博文
boolean isSuccess = save(blog);
//2.1.保存失败
if(!isSuccess){
return Result.fail("新增博文失败");
}
//2.2.成功,查询作者所有粉丝 select * from tb_follow where follow_user_id=?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//3.推送笔记id给粉丝
for (Follow follow:follows){
//3.1.获取粉丝id
Long userId = follow.getUserId();
//4.2.推送
String key=FEED_KEY+userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
return Result.ok();
}
public Result ofFollow(Long max,Integer offset) {
String key=FEED_KEY+UserHolder.getUser().getId();
//1.查询该用户的收件箱
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().
reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//2.非空判断
if (typedTuples.isEmpty()||typedTuples==null){
return Result.ok();
}
//3.解析数据:blogId,minTime(该组中最小时间戳),offset(最小值相同个数)
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime=0;
int count=1;
for (ZSetOperations.TypedTuple<String> tuple:typedTuples){
//3.1.获取blogId
ids.add(Long.valueOf(tuple.getValue()));
//3.2.获取分数(时间戳)
Long time = tuple.getScore().longValue();
if(time==minTime){
count++;
}else {
minTime=time;
count=1;
}
}
//4.根据id查询blog
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Blog blog : blogs) {
//4.1. 查询blog有关用户
queryBlogUser(blog);
//4.2.查询blog是否被点赞
isBlogLiked(blog);
}
//5.封装数据,返回
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setOffset(count);
scrollResult.setMinTime(minTime);
return Result.ok(scrollResult);
}
**
**
十一:基于redis实现附近商户功能
1.GEO数据结构实现查找附近商铺
- geoHash数据结构
- 先导入数据坐标
@Test
void importShopDate(){
//1.查询店铺信息
List<Shop> list = shopService.list();
//第一种方法
for (Shop shop : list) {
String key = SHOP_GEO_KEY + shop.getTypeId();
stringRedisTemplate.opsForGeo()
.add(key,new Point(shop.getX(), shop.getY()),shop.getId().toString());
}
//第二种
/*//2.店铺分组,typeId一致的放一起
Map<Long,List<Shop>> map=list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
//3.分批导入
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
//3.1.获取类型id
Long typeId = entry.getKey();
String key = SHOP_GEO_KEY + typeId;
//3,2.获取同类型店铺
List<Shop> value = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations=new ArrayList<>();
//3.3.写入redis GEOADD key 经度 纬度 member
for (Shop shop : value) {
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(), shop.getY())));
}
stringRedisTemplate.opsForGeo()
.add(key,locations);*/
}
- 实现代码
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//1.判断是否需要根据坐标查询
if(x==null||y==null){
// 不需要坐标查询,按数据库查询
Page<Shop> page = query().eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
// 返回数据
return Result.ok(page);
}
String key=SHOP_GEO_KEY+typeId;
//2.计算分页参数
int form = (current-1)*SystemConstants.DEFAULT_PAGE_SIZE;
int end=current*SystemConstants.DEFAULT_PAGE_SIZE;
//3.查询redis,按照距离排序
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
.search(key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
//4.解析数据
if(results==null){
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = results.getContent();
if(content.size()<=form){
//没有数据了
return Result.ok(Collections.emptyList());
}
//4.1.截取从from~end部分
List<Long> ids=new ArrayList<>(content.size());
Map<String,Distance> distanceMap=new HashMap<>(content.size());
//采用stream流转换,skip是跳转到from,forEach开始循环
content.stream().skip(form).forEach(result ->{
//4.2.获取店铺id
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
//4.3.获取距离
Distance distance = result.getDistance();
distanceMap.put(shopIdStr,distance);
});
//5.根据id查询shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
//6返回
return Result.ok(shops);
}
> 2.BitMap实现用户签到,签到统计功能
-BitMap介绍
- 签到统计分析
- 实现代码(签到)
public Result sign() {
//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 ,返回true就是1
redisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
- 实现代码(签到统计)
public Result signCount() {
//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.获取本月截止今天为止的所有签到记录,返回的是十进制数字
List<Long> result = redisTemplate.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.处理数据,循环遍历数字与1进行与运算,判断是否签到(1为签到)
int count=0;
while(true){
//6.1 与1做与运算,得到最后一位bit位 判断是否为0
if ((num&1)==0){
//为0,未签到,结束
break;
}else {
//计数器+1
count++;
}
//数字右移一位,抛弃最后一个bit位,获取下一个
num >>>=1;
}
return Result.ok(count);
}
3.HyperLogLog用法
- 实例代码
void testHyperLogLog(){
//定义数组
String[] values=new String[1000];
//插入1000000条数据
for(int i=0 ;i<1000000;i++) {
//数据处理,防止越界
int j = i % 1000;
values[j] = "user_" + i;
//每1000条插入一次
if (j == 999) {
stringRedisTemplate.opsForHyperLogLog().add("hl2", values);
}
}
//统计数量
Long size = stringRedisTemplate.opsForHyperLogLog().size("hl2");
System.out.println(size);
}
**