Redis



一、基础知识

1、定义:Redis是一个基于内存的 key-value结构的NoSQL数据库。
2、SQL数据库和NoSQL数据库的区别
在这里插入图片描述
2、特点:
①Redis基于内存存储,读写性能高(mysql基于磁盘存储)
②单线程,每个命令具备原子性;低延迟,速度快(基于内存、I0多路复用、良好的编码)
③适合存储热点数据(热点商品、资讯、新闻)
④企业应用广泛
3、Redis的连接
(1)利用Redis-x64-3.2.100/redis-server.exe来启动Redis

redis-server.exe redis.windows.conf

(2)使用Redis的图像界面化工具Another Redis Desktop Manager进行连接

二、Redis数据类型

1、常用数据类型:Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型。
①字符串string
②哈希hash:也叫散列,类似于Java中的HashMap结构
③列表list:按照插入顺序排序,可以有重复元素
④集合set:无序集合,没有重复元素,类似于Java中的Hashset
⑤有序集合sorted set/zset:集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
2、Redis的操作
(1)字符串操作

//设置指定key的值
SET key value
//获取指定key的值
GET key
//设置指定key的值,并将 key 的过期时间设为seconds秒
SETEX key seconds value
//只有在 key 不存在时设置 key 的值
SETNX key value

(2)哈希操作hash:hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象
在这里插入图片描述

//将哈希表key中的字段field的值设为value
HSET key field value
//获取存储在哈希表中指定字段的值
HGET key field
//删除存储在哈希表中的指定字段
HDEL key field
//获取哈希表中所有字段
HKEYS key
//获取哈希表中所有值
HVALS key

(3)列表操作list

//将一个或多个值插入到列表头部
LPUSH key value1 [value2]
//获取列表指定范围内的元素
LRANGE key start stop
//移除并获取列表尾部最后一个元素
RPOP key
//获取列表长度
LLEN key

(4)集合set操作

//向集合添加一个或多个成员
SADD key member1 [member2]
//返回集合中的所有成员
SMEMBERS key
//获取集合的成员数
SCARD key
//返回给定所有集合的交集
SINTER key1 [key2]
//返回给定所有集合的交集
SUNION key1 [key2]
//删除集合中一个或多个成员
SREM key member1 [member2]

(5)有序集合zset操作:Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。

//向有序集合添加一个或多个成员
ZADD key score1 member1 [score2 member2]
//通过索引区间返回有序集合中指定区间内的成员,括号代表要不要返回分数
ZRANGE key start stop [WITHSCORES]
//有序集合中对指定成员的分数加上增量increment
ZINCRBY key increment member
//移除有序集合中的一个或多个成员
ZREM key member [member ...]

(6)通用操作:通用命令不分数据类型,均可以使用

//查找所有符合给定模式( pattern)的 key
KEYS pattern
//检查给定 key 是否存在
EXISTS key
//返回 key 所储存的值的类型
TYPE key
//该命令用于在 key存在时删除key
DEL key

三、Redis在java中的实现操作

1、Redis的Java客户端
Spring Data Redis低层可以兼容Jedis和lettuce
在这里插入图片描述

2、Jedis

(1)Jedis的官网
Jedis的官网
(2)Jedis的使用方式
①导入Jedis的依赖

<!--pom.xml-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

②建立连接和测试

private Jedis jedis,
@BeforeEach
void setUp(){
	//建立连接,指定IP地址和端口号
	jedis = new Jedis("192.168.150.101"6379);
	/*从Jedis连接池获取jedis,再由close释放回连接池
	jedis = JedisConnectionFactory.getJedis();
	*/
	//设置密码
	jedis.auth("123321");
	//选择库
	jedis.select(0);
}
@Test
void teststring(){
	//插入数据,方法名称就是redis命令名称
	String result =jedis.set("name","张三");
	System.out.printl("result=" + result); //result=ok
	//获取数据
	String name = jedis.get("name");
	System.out.println("name=" + name); //name=张三
@AfterEach
void tearDown(){
	//释放资源
	if(jedis != null){
		jedis.close();
	}
}

(3)Jedis线程池:Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此使用ledis连接池代替Jedis的
直连方式。

public class JedisConnectionFactory{
	private static final JedisPool jedisPool:
	static{
		//配置连接池
		JedisPoolConfig jedisPoolConfig = new JedisPoolconfig();
		// 最大连接
		jedisPoolConfig.setMaxTotal(8); 
		//最大空闲连接
		jedisPoolConfig.setMaxIdle(8);
		//最小空闲连接
		jedisPoolconfig.setMinIdle(0);
		//设置最长等待时间,ms,超过释放连接
		jedisPoolConfig.setMaxWaitMillis(200);
		//创建连接池对象
		jedisPool = new JedisPool(jedisPoolconfig,"192.168.150.101"6379,1000"123321"
	);
	//获取Jedis对象
	public static Jedis getJedis(){
		return jedisPool.getResource();
	}
}

3、Spring Data Redis

(1)定义:SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis
(2)官网地址
Spring Data官网地址
(3)特点
① 提供了对不同Redis客户端的整合(Lettuce和Jedis),支持基于Lettuce的响应式编程
② 提供了RedisTemplate统一API来操作Redis
③ 支持Redis哨兵和Redis集群
④ 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
⑤ 支持基于Redis的JDKCollection实现
(4)RedisTemplate
① 工具类:封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:
在这里插入图片描述
② RedisTemplate可以接收任意Object作为值写入Redis,但写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果会乱码,解决方法如下
a. 自定义RedisTemplate,修改RedisTemplate的序列化器为Generic]ackson2JsonRedisSerializer,但缺点是会消耗额外的存储空间记录类的class类名。
b. 为了节省内存空间,使用StringRedisTemplate,只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。
(4)Spring Data Redis使用方式
①导入Spring Data Redis的依赖

<!--pom.xml-->
<!--Redis依赖-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--连接池依赖-->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

②配置Redis数据源

#application.yaml
spring:
	redis:
    	host: localhost
    	port: 6379
    	password: 1234
    	database: 0
    	lettuce:
			pool:
			max-active: 8  #最大连接
			max-idle: 8  #最大空闲连接
			min-idle: 0  #最小空闲连接
			max-wait: 100  #连接等待时间

③ a. 编写配置类,创建RedisTemplate对象,通过RedisTemplate对象操作Redis

//RedisConfiguration.java文件
@Slf4j
@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模版对象");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象,把注入的连接工厂对象传进来
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer= new GenericJackson2JsonRedisSerializer();
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
        //key和hashKey采用string序列化
     	redisTemplate.setKeySerializer(RedisSerializer.string());
     	redisTemplate.setHashKeySerializer(RedisSerializer.string());
     	//value和 hashValue采用JSON序列化
     	redisTemplate.setValueSerializer(jsonRedisSerializer);
     	redisTemplate.setHashValueSerializer(jsonRedisSerializer);
     	return redisTemplate;
    }
}
@SpringBootTest
public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        ValueOperations valueOperations = redisTemplate.opsForValue();
        HashOperations hashOperations = redisTemplate.opsForHash();
        ListOperations listOperations = redisTemplate.opsForList();
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }
    //操作字符串类型的数据
    @Test
    public void testString(){
        //set get setex setnx
        redisTemplate.opsForValue().set("city","北京");
        String city = (String)redisTemplate.opsForValue().get("city");
        System.out.println(city);
        //第3个参数是时间,第4个参数是时间单位
        redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);
        redisTemplate.opsForValue().setIfAbsent("lock","1");
        redisTemplate.opsForValue().setIfAbsent("lock","2");
    }
    //操作哈希类型的数据
    @Test
    public void testHash(){
        //hset hget hkeys hvals
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.put("100","name","tom"); //相当于hset
        hashOperations.put("100","age","20");
        String name = (String)hashOperations.get("100", "name"); //相当于hget
        System.out.println(name);
        Set keys = hashOperations.keys("100"); //相当于hkeys
        System.out.println(keys);
        List values = hashOperations.values("100"); //相当于hvals
        System.out.println(values);
        hashOperations.delete("100","age");//相当于hdel
    }
    //集合类数据
    @Test
    public void testList(){
        //Lpush lrange rpop llen
        ListOperations listOperations = redisTemplate.opsForList();
        listOperations.leftPushAll("mylist","a","b","c"); //lpush多个value
        listOperations.leftPush("mylist","d"); //lpush单个value
        List mylist = listOperations.range("mylist",0,-1); //lrange
        System.out.println(mylist);
        listOperations.rightPop("mylist"); //rpop
        Long size = listOperations.size("mylist"); //llen
        System.out.println(size);
    }
    //有序集合类数据
    @Test
    public void testZset(){
        //zadd zrange zincrby zrem
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        zSetOperations.add("zset1","a",10); //zadd
        zSetOperations.add("zset2","b",12);
        zSetOperations.add("zset1","c",9);
        Set zset1 = zSetOperations.range("zset1",0,-1); //zrange
        System.out.println(zset1);
        zSetOperations.incrementScore("zset1","c",10); //zincrby
        zSetOperations.remove("zset1","a","b"); //zrem
    }
    //通用类命令操作
    @Test
    public void testCommon(){
        //keys exists type del
        Set keys = redisTemplate.keys("*"); //keys
        System.out.println(keys);
        Boolean name = redisTemplate.hasKey("name"); //exists
        Boolean set1 = redisTemplate.hasKey("set1");
        for(Object key : keys){
            DataType type = redisTemplate.type(key); //type
            System.out.println(type.name());
        }
        redisTemplate.delete("mylist"); //del
    }
}

③ b. Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式,无需自定义RedisTemplate。

@Autowired
private StringRedisTemplate stringRedisTemplate;
//JSON工具
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testStringTemplate() throws JsonProcessingException{
	//准备对象
	User user = new User("虎哥"18);
	//手动序列化
	String json = mapper.writeValueAsString(user);
	//写入一条数据到redis
	stringRedisTemplate.opsForValue().set("user:200",json);
	//读取数据
	String val=stringRedisTemplate.opsForValue().get("user:200");
	//反序列化
	User userl=mapper.readValue(val,User.class);
	System.out.println("user1=u+ userl);

四、Redis实战模块

4.1 短信登陆

4.1.1 基于session实现短信登陆

1、逻辑结构
在这里插入图片描述
2、代码实现

//UserController.java
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
	return userService.sendCode(phone,session); //发送短信验证码并保存验证码到session
}
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
	return userService.login(loginForm,session);
}
//IUserService.class 
public interface IUserService extends IService<User> {
    Result sendCode(String phone, HttpSession session);
    Result login(LoginFormDTO loginForm, HttpSession session);
}
//UserServiceImpl.class 
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    /**
     * 发送手机验证码
     */
    public Result sendCode(String phone, HttpSession session) {
        //1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号错误");
        }
        //3、符合,随机生成六位验证码
        String code = RandomUtil.randomNumbers(6);
        //4、保存验证码到session
        session.setAttribute("code",code);
        //5. 发送验证码
        log.debug("发送短信验证码:{}",code);
        //6、返回ok
        return Result.ok();
    }
    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        //1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号错误");   //如果不符合,返回错误信息
        }
        //2、校验验证码
        Object cachecode = session.getAttribute("code"); //从session中获取code
        String code = loginForm.getCode();
        if (cachecode == null||cachecode.toString().equals(code) == false){
            // 3.不一致,报错
            return Result.fail("验证码错误");
        }
        // 4.一致、根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if (user == null){
            //6.不存在、创建新用户并保存
            user = createUserWithPhone(phone);
        }
        //7.保存用户信息到session中
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        session.setAttribute("user",userDTO);  //存储用户的一部分数据
        return Result.ok();
    }
    /**
     * 创建用户
     * @param phone
     * @return
     */
    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName("user_" + RandomUtil.randomString(10));
        save(user);
        return user;
    }
}
//MvcConfig.class 添加登陆校验拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override //添加拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(  //拦截路径排除
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}
//LoginInterceptor.class 登陆校验拦截器
public class LoginInterceptor implements HandlerInterceptor {
    @Override 
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session
        HttpSession session =request.getSession();
        //2.获取session中的用户
        Object user = session.getAttribute("user");
        //3、判潮用户是否存在
        if(user == null){
            //4、不存在、拦载,返回401状态码
            response.setStatus(401);
            return false;
        }
        //5、存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

3、存在问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

4.1.2 基于redis实现短信登陆

1、逻辑结构
在这里插入图片描述
在这里插入图片描述
2、代码实现

//UserServiceImpl.class 
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 发送手机验证码
     */
    public Result sendCode(String phone, HttpSession session) {
        //1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            //2、如果不符合,返回错误信息
            return Result.fail("手机号错误");
        }
        //3、符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4、保存验证码到redis phone为key(添加前缀加以区分),code为value,2为过期时间
        stringRedisTemplate.opsForValue().set("login:code:"+phone,code,2L, TimeUnit.MINUTES);
        //5. 发送验证码
        log.debug("发送短信验证码:{}",code);
        //6、返回ok
        return Result.ok();
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        //1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号错误");   //如果不符合,返回错误信息
        }
        //2、从redis获取验证码并校验
        String cachecode = stringRedisTemplate.opsForValue().get("login:code:"+);
        String code = loginForm.getCode();
        if (cachecode == null||!cachecode.equals(code)){
            // 3.不一致,报错
            return Result.fail("验证码错误");
        }
        // 4.一致、根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if (user == null){
            //6.不存在、创建新用户并保存
            user = createUserWithPhone(phone);
        }
        //7.保存用户信息到redis中
        //7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString();
        //7.2.将User对象转为UserDTO放入Hash存储,减少redis存储内容
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //TOD0 7.3.存储
        String tokenkey = RedisConstants.LOGIN_USER_KEY + token;
        //利用beanToMap的方法将value中的long型转换为string
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
//        利用反射将long型转换为string在送进map
//        Map<String, Object> userMap = new HashMap<>();
//        Field[] fields = UserDTO.class.getDeclaredFields();
//        for (Field field : fields){
//            field.setAccessible(true);
//            String fieldName = field.getName();
//            Object fieldValue;
//            try {
//                fieldValue = field.get(userDTO);
//                if (fieldValue instanceof Long) {
//                    fieldValue = String.valueOf(fieldValue);
//                }
//            } catch (IllegalAccessException e) {
//                fieldValue = null;
//            }
//            userMap.put(fieldName, fieldValue);
//        }
        stringRedisTemplate.opsForHash().putAll(tokenkey,userMap);
        //设置token过期时间三十分钟
        stringRedisTemplate.expire(tokenkey,30L,TimeUnit.MINUTES);
        //T0D0 8.返过token
        return Result.ok(token);
    }
}
//UserDTO.class 
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}
//LoginInterceptor.class 登陆校验拦截器
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            //判断token是否存在,不存在设置状态码为401
            response.setStatus(401);
            return false;
        }
        //2、基于token获取redis中的用户
        String tokenkey = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenkey);
        //3、判断用户是否存在
        if(userMap.isEmpty()){
            //4、不存在、拦载,返回401状态码
            response.setStatus(401);
            return false;
        }
        //5、将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        //7、刷新token有效期
        stringRedisTemplate.expire(tokenkey,30L, TimeUnit.MINUTES);
        //8、放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
//MvcConfig.class 添加登陆校验拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override //添加拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(  //拦截路径排除
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

4.2 商户查询缓存

1、缓存定义:缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
在这里插入图片描述

4.2.2 缓存更新

1、缓存更新
在这里插入图片描述
2、主动更新策略
在这里插入图片描述
3、缓存更新策略的最佳实践方案:
① 低一致性需求:使用Redis自带的内存淘汰机制
② 高一致性需求:主动更新,并以超时剔除作为兜底方案
③ 读操作:
缓存命中则直接返回
缓存未命中则查询数据库,并写入缓存,设定超时时间
④ 写操作:
先写数据库,然后再删除缓存
要确保数据库与缓存操作的原子性
4、代码实现
① 具体要求
根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
根据id修改店铺时,先修改数据库,再删除缓存
在这里插入图片描述
② 代码

@RestController
@RequestMapping("/shop")
public class ShopController {
    @Resource
    public IShopService shopService;
    /**
     * 根据id查询商铺信息
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryByid(id);
    }
	/**
     * 更新商铺信息
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
        return shopService.update(shop);
    }
}
public interface IShopService extends IService<Shop> {
    Result queryByid(Long id);
    Result update(Shop shop);
}
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryByid(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)){
            //3.存在,直接返回
            //将字符串转为shop类型
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4.不存在,根据id查询数据库
        Shop shop = getById(id);
        //5.数据库不存在,返回错误
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        //6.数据库存在,写入redis
        //将shop类型转为字符串
        stringRedisTemplate.opsForValue()
                .set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
        //7.返回
        return Result.ok(shop);
    }
    @Override
    @Transactional //添加事务,保证原子性
    public Result update(Shop shop) {
        if (shop.getId() == null){
            return Result.fail("店铺id不能为空");
        }
        //更新数据库
        updateById(shop);
        //删除缓存
        stringRedisTemplate.delete("cache:shop:"+shop.getId());
        return Result.ok();
    }
}
4.2.3 缓存穿透

1、定义:缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,不断发起这样的请求给数据库带来巨大压力。
2、解决方案
(1)缓存空对象
(2)布隆过滤
(3)增强id的复杂度,避免被猜测id规律
(4)做好数据的基础格式校验
(5)用户权限校验
在这里插入图片描述
3、代码实现
(1)具体要求:根据id查询商铺信息解决缓存穿透问题
在这里插入图片描述
(2)代码

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryByid(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //2.判断redis中商品缓存是否存在
        if (StrUtil.isNotBlank(shopJson)){
            //3.存在,直接返回
            //将字符串转为shop类型
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断是否命中的redis中商品缓存的空值(空字符串)
        if (shopJson != null){
            return Result.fail("店铺不存在");
        }
        //4.不存在,根据id查询数据库
        Shop shop = getById(id);
        //5.数据库不存在,返回错误
        if (shop == null) {
            //将空值写入redis,解决缓存穿透问题,并设置较短的有效期
            stringRedisTemplate.opsForValue()
                    .set("cache:shop:"+id,"",2L,TimeUnit.MINUTES);
            return Result.fail("店铺不存在");
        }
        //6.数据库存在,写入redis
        //将shop类型转为字符串
        stringRedisTemplate.opsForValue()
                .set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30L,TimeUnit.MINUTES);
        //7.返回
        return Result.ok(shop);
    }
}
4.2.4 缓存雪崩

1、定义:缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
在这里插入图片描述

2、解决方案
(1)给不同的Key的TTL添加随机值
(2)利用Redis集群提高服务的可用性
(3)给缓存业务添加降级限流策路
(4)给业务添加多级缓存

4.2.5 缓存击穿

1、定义:缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
在这里插入图片描述
2、解决方案
(1)互斥锁
① 优点:
没有额外的内存消耗
保证一致性
实现简单
② 缺点:
线程需要等待,性能受影响
可能有死锁风险
(2)逻辑过期
① 优点:
线程无需等待,性能较好
② 缺点:
不保证一致性
有额外内存消耗
实现复杂

在这里插入图片描述
3、代码实现
(1)修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
在这里插入图片描述
(2)修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
在这里插入图片描述
(3)代码

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //线程
    private static final ExecutorService CACHE_REBUILD_EXECUTOR=Executors.newFixedThreadPool(10);
    @Override
    public Result queryByid(Long id) {
        //互斥锁解决缓存击穿
        //Shop shop = queryWithMutex(id);
        //逻辑过期解决缓存击穿
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    //根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
    public Shop queryWithMutex(Long id){
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //2.判断redis中商品缓存是否存在
        if (StrUtil.isNotBlank(shopJson)){
            //3.存在,直接返回
            //将字符串转为shop类型
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的redis中商品缓存是否为空值(空字符串),解决缓存穿透问题
        if (shopJson != null){
            return null;
        }
        //4、没有命中,实现缓存重建
        //4.1.获取互斥锁
        String lockkey = "lock:shop:"+id;
        boolean islock = trylock(lockkey);
        Shop shop = null;
        try {
            //4.2.判断是否获取成功
            if (!islock){
                //4.3.失败,则体眠并重试
                Thread.sleep(50);
                queryWithMutex(id); //休眠结束后重试,使用递归来实现
            }
            //4.4.成功,为了防止重复新建缓存,直接查询redis
            String shopcache = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
            if (StrUtil.isNotBlank(shopcache)){
                Shop newshop = JSONUtil.toBean(shopcache, Shop.class);
                return newshop;
            }
            //redis没有命中,根据id查询数据库
            shop = getById(id);
            //模拟重建的延时
            Thread.sleep(200);
            //5.数据库不存在,返回错误
            if (shop == null) {
                //将空值写入redis,解决缓存穿透问题,并设置较短的有效期
                stringRedisTemplate.opsForValue()
                        .set("cache:shop:"+id,"",2L,TimeUnit.MINUTES);
                return null;
            }
            //6.数据库存在,写入redis
            //将shop类型转为字符串
            stringRedisTemplate.opsForValue()
                    .set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30L,TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //7.释放互斥锁
            unlock(lockkey);
        }
        //8、返回
        return shop;
    }
    
    //根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
    public Shop queryWithLogicalExpire(Long id){
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //2.判断redis中商品缓存是否存在
        if (StrUtil.isBlank(shopJson)){
            //3.redis中商品缓存不存在,直接返回空(不是热点数据)
            return null;
        }
        //4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, 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:"+id;
        boolean islock = trylock(lockkey);
        //6.2.判断是否获取锁成功
        if (!islock){
            // 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;
    }

    //重建缓存
    public void saveShop2Redis(Long id,Long expireSeconds){
        //1.查询店铺数据
        Shop shop = getById(id);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入Redis
        stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(redisData));
    }
    
    //添加互斥锁
    private boolean trylock(String key){
        //opsForValue().setIfAbsent()相当于setnx
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10L,TimeUnit.SECONDS);
        //setIfAbsent返回的是0和1,为了防止拆箱过程中返回空指针,使用BooleanUtil工具
        return BooleanUtil.isTrue(flag);
    }
    
    //删除互斥锁
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}

4.3 优惠券秒杀

4.3.1 全局唯一ID

1、定义:全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,满足唯一性、高可用、高性能、
递增性、安全性的特性。
2、全局唯一ID生成策略
(1)UUID
(2)Redis自增
(3)snowflake算法
(4)数据库自增
3、利用Redis自增和拼接一些其它信息生成全局唯一ID
(1)ID的组成部分:
① 符号位:1bit,永远为0
② 时间戳:31bit,以秒为单位,可以使用69年
③ 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
在这里插入图片描述
(2)代码实现

/*
utils/RedisIdWorker.class
基于Redis的id生成器,由符号位、时间戳和序列号组成
 */
@Component
public class RedisIdWorker {

    //序列号位数
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix){
        //记录开始时间戳,也可以设置为一个固定的值
        LocalDateTime time = LocalDateTime.of(2024, 5, 8, 0, 0, 0);
        long second= time.toEpochSecond(ZoneOffset.UTC);
        //1.生成时间戳
        LocalDateTime now = LocalDateTime.now(); //now为当前时间
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);  //将now转为秒数
        long timestamp = nowSecond - second;
        //2.生成序列号
        //2.1 获取当前日期,精确到天
        String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2 自增长 keyPrefix为自定义的前缀例如order
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + data);
        //3.拼接并返回,将时间戳向左移动COUNT_BITS位数,与count与得到最终的全局唯一id
        return timestamp << COUNT_BITS | count;
    }
}
4.3.2 超卖问题

1、定义:在并发的场景下,比如商城售卖商品中,一件商品的销售数量>库存数量的问题,称为超卖问题。主要原因是在并发场景下,请求几乎同时到达,对库存资源进行竞争,由于没有适当的并发控制策略导致的错误。
在这里插入图片描述
2、解决方案
(1)悲观锁
① 定义:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。
② 常见方法:同步(Synchronized)、锁(Lock)
(2)乐观锁**(常用)**
① 定义:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
② 常见方法:版本号法、CAS法
在这里插入图片描述
在这里插入图片描述

3、秒杀下单
(1)业务需求
修改秒杀业务,要求同一个优惠券,一个用户只能下一单
优惠券秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
优惠券库存是否充足,不足则无法下单
在这里插入图片描述

(2)代码实现

//VoucherOrderController.class
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    @Resource
    private IVoucherOrderService iVoucherOrderService;
    //优惠券秒杀下单
    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return iVoucherOrderService.seckillVoucher(voucherId);
    }
}
//IVoucherOrderService
public interface IVoucherOrderService extends IService<VoucherOrder> {

    Result seckillVoucher(Long voucherId);

    Result createVoucherOrder(Long voucherId);
}
//VoucherOrderServiceImpl.class
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    /**
     * 新增优惠券订单
     * @param voucherId
     * @return
     */
    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)
                .gt("stock",0) //乐观锁解决超卖问题
                .update();
        if (!success){
            return Result.fail("库存不足");
        }
        //6.创建订单
        Long userId = UserHolder.getUser().getId();
        //悲观锁:对userid加锁,确保每人一单
        synchronized (userId.toString().intern()){
            //拿到当前对象的代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }
    @Transactional //在同一个类方法直接调用会事务失效
    public Result createVoucherOrder(Long voucherId){
        // 6.创建订单——实现一人一单
        // 6.1 判断用户是否已经有该优惠券订单
        Long userId = UserHolder.getUser().getId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0){
            return Result.fail("不可重复购买");
        }
        // 6.2用全局id生成器生成订单id
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //6.3设置订单创建人id
        voucherOrder.setUserId(userId);
        //6.4设置优惠券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        // 7.返回订单id
        return Result.ok(orderId);
    }
}
<!--编写SpringAOP(面向切面编程)时,需要导入一个aspectjweaver.jar的包,它的主要作用是负责解析切入点表达式。-->
<dependency>
	<groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
//运行程序
@EnableAspectJAutoProxy(exposeProxy = true) //暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }
}

(3)缺点:一个JVM可以监视多个进程,但是分布式情况下仍然会出现并发问题。

4.3.3 分布式锁

1、定义:满足分布式系统或集群模式下多进程可见并且互斥的锁。
2、特点:多进程可见、互斥、高可用、高性能、安全性
3、实现方法
在这里插入图片描述

4.3.4 基于Redis的分布式锁

1、基本方法
① 获取锁 :
互斥:确保只能有一个线程获取锁
非阻塞:尝试一次,成功返回true,失败返回false

#SET key value [EX seconds] [PX milliseconds] [NX|XX]
#添加锁lock,EX设置超时时间,NX为互斥,保证唯一性
SET lock thread1 EX 10 NX
#查看lock时间
ttl lock

② 释放锁
手动释放
超时释放:获取锁时添加一个超时时间

DEL lock

2、实现方法

//utils.ILock
public interface ILock {
    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的时间,超时释放锁
     * @return true代表获取锁成功;false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);
    /**
     * 释放锁
     */
    void unlock();
}
//utils.SimpleRedisLock.class
public class SimpleRedisLock implements ILock{
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    @Override
    public boolean tryLock(long timeoutSec) {
//        //获取线程id作为值,这种获取方式多个JVM中的线程id可能会冲突,导致线程释放其他线程的锁
//        long threadId = Thread.currentThread().getId();
        //通过添加线程标识,判断线程标识的方法解决锁误删的问题
        //1、在获取锁时存入线程标示(可以用UUID表示)
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        //注意拆箱问题
        return BooleanUtil.isTrue(success);
    }
    @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示是否一致
        if (threadId.equals(id)){
            stringRedisTemplate.delete(KEY_PREFIX + name); //释放锁
        }
    }
}
//VoucherOrderServiceImpl.class
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisIdWorker redisIdWorker;
/**
* 新增优惠券订单
* @param voucherId
* @return
*/
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)
         .gt("stock",0) //乐观锁解决超卖问题
         .update();
    if (!success){
    	return Result.fail("库存不足");
    }
    //6.创建订单
    Long userId = UserHolder.getUser().getId();
    //对userid加锁,确保每人一单
    //利用分布式锁解决超卖问题,获取锁
    SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:"+userId,stringRedisTemplate);
    boolean islock = simpleRedisLock.tryLock(1200);
    //写代码过程中尽量避免嵌套
    if (!islock){
    	//获取锁失败,返回错误或重试(这里是返回错误)
        return Result.fail("不允许重复下单");
    }
    try {
    	//拿到当前对象的代理对象(事务)
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    }finally {
        //释放锁
        simpleRedisLock.unlock();
    }
// 	利用悲观锁解决超卖问题
// 	synchronized (userId.toString().intern()){
//  	//拿到当前对象的代理对象(事务)
//      IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//      return proxy.createVoucherOrder(voucherId);
//  }
}

3、redis的原子性:Lua脚本
(1)定义:Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言。
(2)Lua基本语法
Lua教程
(3)代码实现:基于Lua脚本实现分布式锁的释放锁逻辑

--resources/unlocl.lua
-- 比较线程标示与锁中的线程标示是否一致
if (redis.call('get',KEYS[1]) == ARGV[1]) then
    --释放锁
    return redis.call('del',KEYS[1])
end
return 0`````````								`
//utils.SimpleRedisLock.class
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);
}
@Override
public void unlock() {
	//调用lua脚本
	stringRedisTemplate.execute(
		UNLOCK_SCRIPT,
        Collections.singletonList(KEY_PREFIX + name),
        ID_PREFIX + Thread.currentThread().getId());
}
4.3.5 基于阻塞队列的异步秒杀优化

一、业务需求
(1)新增秒杀优惠券的同时,将优惠券信息保存到Redis中
(2)基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
(3)如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
(4)开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
二、存在问题
(1)内存限制问题
(2)数据安全问题
三、代码实现

--resources/seckill.lua
-- 1、参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]
--2、数据key
--2.1 库存key ..类似于 +
local stockKey = 'seckill:stock:' .. voucherId
--2.2 订单key
local orderkey = 'seckill:order:' .. voucherId

--3、脚本业务
--3.1 判断库存是否充足 tonumber将值转换为数字
if (tonumber(redis.call('get',stockKey)) <= 0) then
    -- 库存不足,返回1
    return 1
end
--3.2 判断用户是否下单
if (redis.call('sismember',orderkey,userId) == 1) then
    -- 用户已下过单
    return 2
end
--3.3 用户可以下单,减少库存
redis.call('incrby',stockKey,-1)
--3.4 下单,保存用户
redis.call('sadd',orderkey,userId)
return 0
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Resource
    private RedissonClient redissonClient;
    //定义lua脚本
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
    //阻塞队列
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
    //线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
    //一旦VoucherOrderServiceImpl初始化完毕,开始执行任务
    @PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }
    private IVoucherOrderService proxy;
    //线程任务
    private class VoucherOrderHandler implements Runnable{
        @Override
        public void run() {
            while (true){
                try {
                    //1、获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    //2、创建订单
                    handleVoucherOrder(voucherOrder);
                } catch ( Exception e) {
                    log.info("处理订单异常",e);
                }

            }
        }
    }

    //创建订单
    private void handleVoucherOrder(VoucherOrder voucherOrder){
        //获取用户id,这是新线程,不能从UserHolder里获取
        Long userId = voucherOrder.getId();
        //获取锁,不做没关系,为了兜底
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //释放不等待,传入无参
        boolean islock = lock.tryLock();
        //写代码过程中尽量避免嵌套
        if (!islock){
            //获取锁失败,兜底
            log.info("不允许重复下单");
        }
        try {
            proxy.createVoucherOrder(voucherOrder);
        }finally {
            //释放锁
//            simpleRedisLock.unlock();
            lock.unlock();
        }
    }
    /**
     * 新增优惠券订单
     * @param voucherId
     * @return
     */
    public Result seckillVoucher(Long voucherId) {
        //用户id
        Long userId = UserHolder.getUser().getId();
        //1、执行lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(), //传入的key为空集合
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue(); //将result从long型转为int
        //2、判断结果
        if (r != 0){
            return Result.fail(r == 1 ? "库存不足":"不能重复下单");
        }
        //3、用于购买资格,抢单成功,创建订单信息并保存到阻塞队列中
        //3.1 创建订单信息
        VoucherOrder voucherOrder = new VoucherOrder();
        //3.1.1设置订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //3.1.2设置订单创建人id
        voucherOrder.setUserId(userId);
        //3.1.3设置优惠券id
        voucherOrder.setVoucherId(voucherId);
        //3.2 放入阻塞队列
        orderTasks.add(voucherOrder);
        //3.3 存放代理对象 主线程可以通过AopContext.currentProxy()来获取代理对象,子线程不行
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        return Result.ok(orderId); //此时秒杀结束,实现异步下单
    }

    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder){
        // 6.创建订单——实现一人一单
        // 6.1 判断用户是否已经有该优惠券订单
        Long userId = voucherOrder.getUserId();
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        if (count > 0){
            log.info("用户已经购买过一次");
        }
        // 5.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock -1")
                .eq("voucher_id",voucherOrder.getVoucherId())
                .gt("stock",0) //乐观锁解决超卖问题
                .update();
        if (!success){
            log.info("库存不足");
        }
        //创建订单
        save(voucherOrder);
    }
}
4.3.6 基于消息队列的异步秒杀优化

1、定义:消息队列(Message Queue),字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色。
(1)消息队列:存储和管理消息,也被称为消息代理(Message Broker)
(2)生产者:发送消息到消息队列
(3)消费者:从消息队列获取消息并处理消息
在这里插入图片描述
2、特点:消息队列是JVM以外的独立服务,不受JVM内存的限制

五、Redission

1、定义:Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
2、官网
redission官网地址
github地址
3、特点
(1)可重入:利用hash结构记录线程id和重入次数
(2)可重试:利用信号量和PubSsub功能实现等待、唤醒,获取锁失败的重试机制
(3)超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间
(4)multiLock:多个独立的Redis节点,必须在所有节点都获取重入员,才算获取锁成功
4、快速入门
(1)引入依赖

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-spring-boot-starter</artifactId>
	<version>3.13.6</version>
</dependency>

(2)配置Redisson客户端

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;
/**
 * 配置redis客户端
 */
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://localhost:6379")
                //.setAddress("redis://192.168.150.101:6379")
                .setPassword("1234");
        return Redisson.create(config);
    }
}

(3)使用Redisson的分布式锁

package com.hmdp;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class HmDianPingApplicationTests {
    @Resource
    private RedissonClient redissonClient;
    @Test
    void testRedisson() throws InterruptedException{
        //获取锁(可重入),指定锁的名称
        RLock lock = redissonClient.getLock("testLock");
        // 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
        boolean islock = lock.tryLock(10, 100, TimeUnit.SECONDS);
        // 判断释放获取成功
        if (islock){
            try{
                System.out.println("执行业务");
            }finally {
                lock.unlock();
            }
        }
    }
}
  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值