目录
实时同步数据:要求缓存中的数据必须与DB中的数据保持一致。如何保持?只要DB中的数据发生了变更,缓存中的数据立即消失。
阶段性同步数据:其没有必要与DB中的数据保持一致,只要大差不差就行。如何实现?为缓存数据添加生存时长属性。
缓存在使用的时候有一个预热的过程,就是提前加载一些常见的数据到缓存中。阶段性同步数据就可以在预热中进行缓存。
ACL,Access Control List,访问控制列表,是一种细粒度的权限管理策略,可以针对任意用户与组进行权限控制。目前大多数 Unix 系统与 Linux 2.6 版本已经支持 ACL 了。 Zookeeper 早已支持 ACL 了。
Unix 与 Linux 系统默认使用是 UGO(User、Group、Other)权限控制策略,其是一种粗粒度的权限管理策略。
|
安装指令yum -y install gcc gcc-c++
官网下载redis安装包
Linux上传安装包
rz指令进行上传
Linux解压安装包
tar -zxvf redis安装包 -C 解压路径
Linux安装Redis
进入redis文件输入Linux指令 make 进行安装
安装完后使用Linux指令 make install 进行安装
<1>直接启动
Linux指令 redis-server 这种方式有一个弊端,占用了命令行,当我们CTRL+C发现Redis也退出了
<2>命令式后台启动
Linux指令 nohup redis-server & 进行后台启动,启动成功后会在当前目录多一个文件
<3>配置后台启动
需要配置redis.conf文件,Linux指令 vim redis.conf
输入 / 使用命令模式搜索daemonize,
输入 i 使用输入模式,将no改为yes
输入 redis-server /opt/apps/redis/redis.conf
Linux指令 ps aux | grep redis
输入 redis-cli shutdown
在redis里面输入shutdown
需要配置redis.conf文件,Linux指令 vim redis.conf
输入 / 使用命令模式搜索bind,
输入 i 使用输入模式,修改目标为以下格式
需要配置redis.conf文件,Linux指令 vim redis.conf
输入 / 使用命令模式搜索protected,
输入 i 使用输入模式,将yes改为no
需要配置redis.conf文件,Linux指令 vim redis.conf
输入 / 使用命令模式搜索requirepass,
输入 i 使用输入模式,修改目标为以下格式,后面foobared修改为你想改的密码
设置以后进入redis输入无法完成得先输入 auth 密码
需要配置redis.conf文件,Linux指令 vim redis.conf
输入 / 使用命令模式搜索rename-command,
输入 i 使用输入模式,修改目标为以下格式,表示禁止flushall命令
命令行启动客户端
redis-cli -h IP地址 -p 端口号 -a 密码
-h输入客户端的IP地址
-p端口号,一般为6379
-a如果设置了密码,这里得输入密码
如果是本机,无需-h,如果端口号是6379无需-p,如果无密码无需-a
1.Redis Desktop Manager
官网为:https://resp.app/(原来是 http://redisdesktop.com)。
2.RedisPlus
RedisPlus 的官网地址为 https://gitee.com/MaxBill/RedisPlus。
所谓 Java 代码客户端就是一套操作 Redis 的 API,其作用就像 JDBC 一样,所以 Java 代码客户端其实就是一个或多个 Jar 包,提供了对 Redis 的操作接口。
对 Redis 操作的 API 很多,例如 jdbc-redis、jredis 等,但最常用也是最有名的是 Jedis。
输入ping命令,会看到pong响应,说明该客户端与Redis的连接时正常的,该命令成为心跳命令
通用指令是部分数据类型的,都可以使用的指令,常见的有
KEYS:查看符合模板的所有key
DEL :删除一个指定的key
EXISTS:判断key是否存在
EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
TTL:查看一个key的剩余有效期
key允许有多个单词形成层级结构,当额单词之间用“:”隔开,格式如:
项目名:业务名:类型:
这个格式并非固定,也可以根据自己的需求来删除或添加词条
例如:
user相关: admin:user:1
product相关:admin:product:1
string类型,也就是字符串类型,是Redis最简单的存储类型,其value是字符串,不过根据字符串的格式不同,又可以分为3类:
string:普通字符串
int:整数类型,可以做自增、自减操作
float:浮点类型,可以做自增、自减操作
不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512M
SET:添加或修改已经存在的一个String类型的键值对
GET:根据key获取String类型的value
MSET:批量添加多个String类型的键值对
MGET:根据多个key获取多个String类型的value
INCR:让一个整形的key自增1
INCRBY:让一个整形的key自增并指定步长
INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
SETNX:添加一个String类型的键值对,前提是这个key不存在,否则不执行
SETEX:添加一个String类型 的键值对,并且指定有效期
hash类型也叫散列,其value是一个无序字典,类似于Java中的HashMap结构
HSET key field value:添加或者修改hash类型key的field的值
HGET key field:获取一个hash类型key 的field的值
HMSET:批量添加多个hash类型key的field的值
HMGET:批量获取多个hash类型key的field的值
HGETALL:获取一个hash类型的key的所有field和value
HKEYS:获取一个hash类型的key中所有的field
HVALS:获取一个hash类型的key中所有的value
HINCRBY:让一个hash类型的key的字段值自增并指定步长
HSETNX:添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
list类型与Java中的LinkedList类似,可以看作一个双向链表结构。既可以支持正向检索,也可以支持反向检索。特征也与LinkedList类似:
常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。
LPUSH key element . . . :向列表左侧插入一个或多个元素
LPOP key:移除并返回列表左侧的第一个元素
RPUSH key element . . . :向列表右侧插入一个或多个元素
RPOP key:移除并返回列表右侧的第一个元素
LRANGE key star end:返回一段角标范围内的所有元素
BLPOP和BRPOP:与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
set类型与Java中的HashSet类似,可以看作一个value为null的HashMap。也可以是一个hash表,因此具备与HashSet类似的特征
SADD key member . . . :向set中添加一个或多个元素
SREM key member . . . :移除set中的指定元素
SCARD key:返回set中元素的个数
SISMEMBER key member:判断一个元素是否存在与set中
SMEMBERS:获取set中的所有元素
SINTER key1 key2 . . . :求key1与key2的交集
SDIFF key1 key2 . . . :求key1与key2的差集
SUNION key1 key2 . . . :求key1与key2的并集
是一个可排序的set集合,与Java中的TreeSet类似,但底层数据结构却差别很大。Zset中的每一个元素都带一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加hash表,具有以下特性
ZADD key score member:添加一个或多个元素到Zset,如果寂静存在则更新其score值
ZREM key member:删除Zset中的一个指定元素
ZSCORE key member:获取Zset中指定元素的score值
ZRANK key member:获取Zset中指定元素的排名
ZCARD key:获取Zset中的元素个数
ZCOUNT key min max:统计score值在给定范围内的所有元素的个数
ZINCRBY key increment member:让Zset中的指定元素自增,步长为指定的increment的值
ZRANGE key min max:按照score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max:按照score排序后,获取指定score范围内的元素
ZDIFF、ZINTER、ZUNION:求差集、交集、并集
注:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可
导入依赖GitHub - redis/jedis: Redis Java client designed for performance and ease of use.
- <dependency>
- <groupId>redis.clients</groupId>
- <artifactId>jedis</artifactId>
- <version>4.3.0</version>
- </dependency>
导入junit
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter</artifactId>
- <version>5.9.1</version>
- <scope>test</scope>
- </dependency>
1.进入test,引入Jedis,
2.调用Jedis,使用Redis命令
3.释放资源
- public class jedis {
- private Jedis jedis;
-
- @BeforeEach
- void setUp() {
- //1.建立连接
- jedis = new Jedis("192.168.80.135", 6379);
- //2.设置密码
- //jedis.auth("");
- //3.选择数据库
- jedis.select(0);
- }
- @Test
- void testString(){
- String setnames = jedis.set("name", "wangwu");
- System.out.println(setnames);
- String getname = jedis.get("name");
- System.out.println(getname);
- }
- @AfterEach
- void tearDown(){
- if (jedis!=null){
- jedis.close();
- }
- }
-
- }
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis的直连方式。
- public class JedisConnectionFactory {
- private static final JedisPool jedisPool;
-
- static {
- //配置连接池
- JedisPoolConfig poolConfig = new JedisPoolConfig();
- //最大连接数
- poolConfig.setMaxTotal(8);
- //最大空闲连接
- poolConfig.setMaxIdle(8);
- //最小空闲连接
- poolConfig.setMinIdle(0);
- //设置最长等待时间
- poolConfig.setMaxWaitMillis(1000);
- //创建连接池对象
- jedisPool = new JedisPool(poolConfig, "192.168.80.135", 6379, 1000);
-
- }
-
- public static Jedis getJedis() {
- return jedisPool.getResource();
- }
- }
SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
1.依赖,有StringDataRedis依赖以及连接池的依赖(idea版本为2.5.7)
- <!--redis依赖-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <!--redis连接池-->
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-pool2</artifactId>
- </dependency>
2.设置Reids配置(我是用的yml格式)
- spring:
- redis:
- port: 6379
- host: 192.168.80.135
- lettuce:
- pool:
- max-wait: 1000
- max-idle: 8
- max-active: 8
- min-idle: 0
3.自动注入redistemplate
- @Autowired
- private RedisTemplate redisTemplate;
- @Test
- void contextLoads() {
- redisTemplate.opsForValue().set("name","wangwu");
- Object name = redisTemplate.opsForValue().get("name");
- System.out.println(name);
- }
RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化。缺点是
采用自定义的序列化方式
-
- @Configuration
- public class RedisConfig {
- @Bean
- public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
- //创建RedisTempalte对象
- RedisTemplate<String, Object> template = new RedisTemplate<>();
- //设置连接工厂
- template.setConnectionFactory(connectionFactory);
- //创建JSON序列化工具
- GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
- //设置Key的序列化
- template.setKeySerializer(RedisSerializer.string());
- template.setHashKeySerializer(RedisSerializer.string());
- // 设置Value的序列化
- template.setValueSerializer(jsonRedisSerializer);
- template.setHashValueSerializer(jsonRedisSerializer);
- // 返回
- return template;
- }
- }
在使用序列化的时候要加入jackson依赖
- <!--Jackson依赖-->
- <dependency>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-databind</artifactId>
- </dependency>
使用自动序列化有一个问题,储存的Redis数据里面夹带了反序列化时需要的对象路径的数据,这个数据有时候比储存的数据还要大,Redis数据储存在内存中,这样会导致浪费空间。
为了节省内存空间,我们不会使用JSON序列化器来处理value,而是同意使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化
String默认提供了一个StringRedisTemplare类,它的key和value的序列化方式默认就是String方式。省区了我们自定义RedisTemplate的过程
-
- @SpringBootTest
- class StringRedisApplicationTests {
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
-
- @Test
- void contextLoads() {
- stringRedisTemplate.opsForValue().set("name", "wangwu");
- Object name = stringRedisTemplate.opsForValue().get("name");
- System.out.println(name);
- }
- private static final ObjectMapper mapper=new ObjectMapper();
- @Test
- void user() throws JsonProcessingException {
- // stringRedisTemplate.opsForValue().set("user:name",new User("lisi",21));
- // User user = (User) stringRedisTemplate.opsForValue().get("user:name");
- // System.out.println("user = " + user);
- //创建对象
- User user = new User("lisi",31);
- //手动序列化
- String userjson = mapper.writeValueAsString(user);
- //导入数据
- stringRedisTemplate.opsForValue().set("user:name:2",userjson);
- //获取数据
- String usergetjson = stringRedisTemplate.opsForValue().get("user:name:2");
- //手动反序列化
- User user1 = mapper.readValue(usergetjson,User.class);
- System.out.println("user1 = " + user1);
- }
-
- }
- public Result sendCode(String phone, HttpSession session) {
- // 1.验证手机号
- boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
- log.debug("手机号验证{}", phoneInvalid);
- if (phoneInvalid) {
- // 2.验证失败
- return Result.fail("手机号格式不对");
- }
- // 3.验证成功生成验证码
- String code = RandomUtil.randomNumbers(6);
- log.debug("生成的验证码{}", code);
- // 4.保存验证码到session
- session.setAttribute("code", code);
- // 5.发送验证码
- log.debug("验证码发送成功,验证码{}", code);
- // 6.返回
- return Result.ok();
- }
- /**
- * 登录验证
- *
- * @param loginForm
- * @param session
- * @return
- */
- @Override
- public Result login(LoginFormDTO loginForm, HttpSession session) {
- String phone = loginForm.getPhone();
- // 检验手机号是否是正确格式
- if (RegexUtils.isPhoneInvalid(phone)) {
- // 格式错误返回错误信息
- log.debug("登录验证手机号格式不正确");
- return Result.fail("您输入的手机号格式不正确");
- }
- String loginCode = loginForm.getCode();
- // 根据手机号获取Redis里面的code
- String RedisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
- // 比对验证吗是否一致
- if (loginCode == null || !RedisCode.equals(loginCode)) {
- // 比对失败返回错误信息
- log.debug("登录验证码不正确");
- return Result.fail("您未输入验证码或您输入的验证码不正确");
- }
- // 验证该手机号用户是否存在
- User user = query().eq("phone", phone).one();
- if (user == null) {
- // 不存在创建新用户
- user = createUserWithPhone(phone);
- }
-
- // 保存用户到Redis
- // 生成随机的token当作令牌
- String token = UUID.randomUUID().toString(true);
- // 将user复制给UserDto,目的是不要将用户的全部信息返回给前端
- UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
- // 将user对象转换为map集合
- Map<String, Object> UserMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions
- .create()
- .setIgnoreNullValue(true)
- .setFieldValueEditor((fieldName,fieldaValue)->fieldaValue.toString()));
- // 保存到redis中
- stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,UserMap);
- // 设置登录过期时间
- stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
- return Result.ok(token);
- }
- /**
- * 创建新用户
- * @param phone
- * @return
- */
- private User createUserWithPhone(String phone) {
- // 创建User
- User user = new User();
- user.setPhone(phone);
- user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(5));
- // 保存用户
- save(user);
- log.debug("用户保存成功");
- return user;
- }
登陆前要进行拦截,来判断用户的账号是否存在,以及一些需要登录才能访问的页面也需要拦截器进行拦截。
- public class LoginInterceptorXiang implements HandlerInterceptor {
- /**
- * 在control前进行
- * @param request
- * @param response
- * @param handler
- * @return
- * @throws Exception
- */
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- // 获取session
- HttpSession session = request.getSession();
- // 获取session里的用户
- Object user = session.getAttribute("user");
- // 判断用户是否存在
- if (user == null) {
- response.setStatus(401);
- return false;
- }
- // 不存在返回错误状态
- // 存在保存用户到ThreadLocald
- UserHolder.saveUser((UserDTO) user);
- return true;
- }
-
- /**
- * 在control方法后进行
- * @param request
- * @param response
- * @param handler
- * @param modelAndView
- * @throws Exception
- */
- // @Override
- // public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
- // HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
- // }
-
- /**
- * 在渲染后进行
- * @param request
- * @param response
- * @param handler
- * @param ex
- * @throws Exception
- */
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- UserHolderXiang.removeUser();
- }
- }
- @Configuration
- public class MvcConfigXiang implements WebMvcConfigurer {
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new LoginInterceptorXiang())
- .excludePathPatterns(
-
- "/user/code",
- "/user/login"
- );
- }
- }
- 校验手机号是否符合正确的手机号的格式,采用正则表达式
- 校验失败返回失败信息
- 生成随机验证码
- 将验证码作为String类型存入Redis,key是手机号,value是验证码
- 发送验证码
- /**
- * 验证手机号格式是否正确
- *
- * @param phone
- * @param session
- * @return
- */
- @Override
- public Result sendCode(String phone, HttpSession session) {
- // 验证手机号
-
- if (RegexUtils.isPhoneInvalid(phone)) {
- // 手机号不符合规范
- log.debug("手机号格式不对");
- return Result.fail("手机号格式不对");
- }
-
- // 手机号符合规范,生成验证码
- String code = RandomUtil.randomNumbers(6);
- log.debug("生成的验证码{}", code);
- // 保存验证码到redis
- stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TIME, TimeUnit.MINUTES);
-
- // 发送验证码d
- log.debug("发送验证码成功,验证码{}", code);
- return Result.ok();
- }
- 验证手机号格式是否正确
- 格式错误,返回错误信息
- 根据手机号取出Redis里面存储的验证码
- 比对验证码是否与输入的验证码一致
- 比对失败,返回错误信息
- 比对成功,验证该手机号用户是否存在
- 不存在,创建新用户,保存到数据库
- 存在,保存用户到Redis里面(这里用到一个时间,30分钟将会自动删除Redis用户,以此来模仿Session30分钟没操作自动退出登录)
- 生成随机的token当作令牌
- 将User复制给UserDto,目的是不要将用户的全部信息返回给前端
- 将User对象转换为Map集合
- 保存到Redis中(key为token,value为map集合)
- 设置登录有效期
- /**
- * 登录验证
- *
- * @param loginForm
- * @param session
- * @return
- */
- @Override
- public Result login(LoginFormDTO loginForm, HttpSession session) {
- String phone = loginForm.getPhone();
- // 检验手机号是否是正确格式
- if (RegexUtils.isPhoneInvalid(phone)) {
- // 格式错误返回错误信息
- log.debug("登录验证手机号格式不正确");
- return Result.fail("您输入的手机号格式不正确");
- }
- String loginCode = loginForm.getCode();
- // 根据手机号获取Redis里面的code
- String RedisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
- // 比对验证吗是否一致
- if (loginCode == null || !RedisCode.equals(loginCode)) {
- // 比对失败返回错误信息
- log.debug("登录验证码不正确");
- return Result.fail("您未输入验证码或您输入的验证码不正确");
- }
- // 验证该手机号用户是否存在
- User user = query().eq("phone", phone).one();
- if (user == null) {
- // 不存在创建新用户
- user = createUserWithPhone(phone);
- }
-
- // 保存用户到Redis
- // 生成随机的token当作令牌
- String token = UUID.randomUUID().toString(true);
- // 将user复制给UserDto,目的是不要将用户的全部信息返回给前端
- UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
- // 将user对象转换为map集合
- Map<String, Object> UserMap = BeanUtil.beanToMap(userDTO);
- // 保存到redis中
- stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,UserMap);
- // 设置登录过期时间
- stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
- return Result.ok(token);
- }
- /**
- * 创建新用户
- *
- * @param phone
- * @return
- */
- private User createUserWithPhone(String phone) {
- // 创建User
- User user = new User();
- user.setPhone(phone);
- user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(5));
- // 保存用户
- save(user);
- log.debug("用户保存成功");
- return user;
- }
- 编写拦截器
- 获取请求投中的token
- 如果token不存在,进行拦截,返回状态码401
- 根据Token获取Redis中的用户
- 判断用户是否存在
- 不存在,进行拦截,返回状态码401
- 将查询到的Hash数据转化为UserDto对象
- 将对象保存到ThreadLocal中
- 自动延长登录有效期
- public class LoginInterceptor implements HandlerInterceptor {
-
- private StringRedisTemplate stringRedisTemplate;
-
- public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
- this.stringRedisTemplate = stringRedisTemplate;
- }
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- //获取请求投中的token
- String token = request.getHeader("authorization");
-
- if (token == null) {
- //如果token不存在,进行拦截,返回状态码401
- response.setStatus(401);
- return false;
- }
- //根据Token获取Redis中的用户
- Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
-
- //判断用户是否存在
- if (userMap == null) {
- //不存在,进行拦截,返回状态码401
- response.setStatus(401);
- return false;
- }
-
- //将查询到的Hash数据转化为UserDto对象
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
- //将对象保存到ThreadLocal中
- UserHolder.saveUser(userDTO);
- //自动延长登录有效期
- stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL, TimeUnit.MINUTES);
-
- return true;
- }
- }
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
-
- @Resource
- 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"
- ).order(1);
- // token刷新的拦截器
- registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
- }
- }
在以上的拦截器前面再加一层拦截器,原因是,在原来的拦截器中,自动延长用户登录时间的时候,只有当用户访问了需要登录拦截的时候才会延长,再加一个拦截器,就可以做到只要用户有操作就可以延长登陆时间
- public class RefreshTokenInterceptor implements HandlerInterceptor {
-
- private StringRedisTemplate stringRedisTemplate;
-
- public RefreshTokenInterceptor(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)) {
- return true;
- }
- // 2.基于TOKEN获取redis中的用户
- String key = LOGIN_USER_KEY + token;
- Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
- // 3.判断用户是否存在
- if (userMap.isEmpty()) {
- return true;
- }
- // 5.将查询到的hash数据转为UserDTO
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
- // 6.存在,保存用户信息到 ThreadLocal
- UserHolder.saveUser(userDTO);
- // 7.刷新token有效期
- stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
- // 8.放行
- return true;
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- // 移除用户
- UserHolder.removeUser();
- }
- }
-
- public class LoginInterceptor implements HandlerInterceptor {
-
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- //1.判断是否需要拦截(ThreadLocal中是否有用户)
- if (UserHolder.getUser() == null) {
- // 没有,需要拦截,设置状态码
- response.setStatus(401);
- // 拦截
- return false;
- }
- // 有用户,则放行
- return true;
-
- }}
order属性是可以规定那个拦截器先执行(越小越先执行)
- @Configuration
- public class MvcConfig implements WebMvcConfigurer {
-
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- // 登录拦截器
- registry.addInterceptor(new LoginInterceptor())
- .excludePathPatterns(
- "/shop/**",
- "/voucher/**",
- "/shop-type/**",
- "/upload/**",
- "/blog/hot",
- "/user/code",
- "/user/login"
- ).order(1);
- // token刷新的拦截器
- registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
- }
- }
- 提交商铺id
- 从Redis里面查询商铺缓存
- 判断缓存是否存在
- 存在,返回缓存数据,得到商铺信息(返回的时候要记得反序列化,因为缓存查到的数据是JSON数据)
- 不存在,根据商铺id进入数据库查询
- 数据库不存在,返回错误信息
- 存在,将商铺信息存储在Redis中,建立该商铺缓存(将商铺信息进行序列化)
- 返回商铺信息
-
- @Override
- public Result queryById(Long id) {
-
-
- //从Redis里面查询商铺缓存
- String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
- //判断缓存是否存在
- if (StrUtil.isNotBlank(cacheShop)) {
- //存在,返回缓存数据,得到商铺信息
- Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
- return Result.ok(shop);
- }
- //不存在,根据商铺id进入数据库查询
- Shop shop = query().eq("id", id).one();
- //数据库不存在,返回错误信息
- if (shop==null) {
- return Result.fail("查询的商铺不存在");
- }
- //存在,将商铺信息存储在Redis中,建立该商铺缓存
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, JSONUtil.toJsonStr(shop));
- //返回商铺信息
- return Result.ok(shop);
- }
- 查询Redis的分类信息(按照分类排序)
- 存在,返回分类信息
- 不存在取数据库里查找
- 不存在,返回分类不存在
- 存在,将分类信息缓存到Redis
- 返回分类信息
- @Override
- public Result queryTypeList() {
- //查询Redis的分类信息(按照分类排序)
- String shopTypeJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOPTYPE);
- //存在,返回分类信息
- if (StrUtil.isNotBlank(shopTypeJSON)) {
- List<ShopType> shopType = JSONUtil.toList(shopTypeJSON, ShopType.class);
- return Result.ok(shopType);
- }
- //不存在取数据库里查找
- List<ShopType> shopTypesList = query().orderByAsc("sort").list();
- //不存在,返回分类不存在
- if (shopTypesList==null){
- return Result.fail("分类信息不存在");
- }
- //存在,将分类信息缓存到Redis
- shopTypeJSON = JSONUtil.toJsonStr(shopTypesList);
- stringRedisTemplate.opsForValue().set(CACHE_SHOPTYPE,shopTypeJSON);
- //返回分类信息
- return Result.ok(shopTypesList);
- }
操作缓存和数据库时由三个问题需要考虑
缓存更新策略的最佳实践方案:
- @Override
- public Result queryById(Long id) {
-
-
- //从Redis里面查询商铺缓存
- String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
- //判断缓存是否存在
- if (StrUtil.isNotBlank(cacheShop)) {
- //存在,返回缓存数据,得到商铺信息
- Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
- return Result.ok(shop);
- }
- //不存在,根据商铺id进入数据库查询
- Shop shop = query().eq("id", id).one();
- //数据库不存在,返回错误信息
- if (shop == null) {
- return Result.fail("查询的商铺不存在");
- }
- //存在,将商铺信息存储在Redis中,建立该商铺缓存,加入TTL
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
- //返回商铺信息
- return Result.ok(shop);
- }
- @Override
- @Transactional
- public Result update(Shop shop) {
- Long id = shop.getId();
- if (id == null) {
- return Result.fail("店铺不存在");
- }
- //先更新数据库
- updateById(shop);
- //再删除缓存
- stringRedisTemplate.delete(CACHE_SHOP_XIANG + shop.getId());
-
- return Result.ok();
- }
缓存穿透是指客户端请求的数据再缓存中和数据库中都不存在,这样缓存永远不会失效,这些请求都会打到数据库。
常见的解决方案有两种
根据id查询店铺信息有缓存穿透的风险
- 提交商铺id
- 从Redis里面查询商铺缓存
- 判断缓存是否存在
- 存在,返回缓存数据,得到商铺信息(返回的时候要记得反序列化,因为缓存查到的数据是JSON数据)
- 不存在但是不等于NULL
- 返回错误信息
- 不存在,根据商铺id进入数据库查询
- 不存在,将空值写入Redis(空值为 "" )
- 存在,将商铺信息存储在Redis中,建立该商铺缓存(将商铺信息进行序列化)
- 返回商铺信息
-
- @Override
- public Result queryById(Long id) {
- //从Redis里面查询商铺缓存
- String cacheShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
- //判断缓存是否存在
- if (StrUtil.isNotBlank(cacheShop)) {
- //存在,返回缓存数据,得到商铺信息
- Shop shop = JSONUtil.toBean(cacheShop, Shop.class);
- return Result.ok(shop);
- }
- //不存在,但缓存不为NULL
- if (cacheShop != null) {
- return Result.fail("您要查找的店铺不存在");
- }
- //不存在,根据商铺id进入数据库查询
- Shop shop = query().eq("id", id).one();
- //数据库不存在,返回错误信息
- if (shop == null) {
- //不存在,将空值写入Redis中
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
- return Result.ok(shop);
- }
- //存在,将商铺信息存储在Redis中,建立该商铺缓存,加入TTL
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
- //返回商铺信息
- return Result.ok(shop);
- }
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
常见的解决方案有两种
- 1.获取商铺id
- 2.从Redis中查询商铺缓存
- 2.1有缓存,直接返回数据
- 2.2没有缓存,也不会NULL
- 3尝试获取互斥锁
- 3.1没有获取到,休眠一段时间
- 3.2再次查询缓存是否存在
- 4获取到互斥锁,根据店铺id查询数据库
- 5判断数据库是否存在
- 5.1数据库不存在,返回空
- 5.2给Redis缓存一个 "" ,防止缓存穿透
- 6数据库存在,将数据缓存到Redis中
- 6.1释放互斥锁
- 7返回数据
- /**
- * 依靠互斥锁解决缓存击穿问题
- *
- * @param id
- * @return
- */
- public Shop queryWithShopMutual(Long id) {
- // 1.获取商铺id
- //2.从Redis中查询商铺缓存
- String shopJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
- //2.1有缓存,直接返回数据
- if (StrUtil.isNotBlank(shopJSON)) {
-
- Shop shop = JSONUtil.toBean(shopJSON, Shop.class);
- return shop;
- }
- //2.2没有缓存,也不会NULL
- if (shopJSON!=null){
- return null;
- }
- Shop shop=null;
- try {
- //3尝试获取互斥锁
- boolean tryLock = tryLock(LOCK_SHOP_KEY + id);
- //3.1没有获取到,休眠一段时间,再次查询缓存是否存在
- if (!tryLock) {
- Thread.sleep(30);
- return queryWithShopMutual(id);
- }
- //4获取到互斥锁,根据店铺id查询数据库
- shop = query().eq("id", id).one();
- //5判断数据库是否存在
- //5.1数据库不存在,返回空
- if (shop == null) {
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
- return null;
- }
- //6数据库存在,将数据缓存到Redis中
- String Shop2JSON = JSONUtil.toJsonStr(shop);
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, Shop2JSON, CACHE_SHOP_TTL, TimeUnit.MINUTES);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- } finally {
- //6.1释放互斥锁
- unlock(LOCK_SHOP_KEY + id);
- }
-
- //7返回数据
- return shop;
- }
- /**
- * 尝试获得互斥锁
- * @param key
- * @return
- */
- public boolean tryLock(String key) {
- Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 30, TimeUnit.MILLISECONDS);
- return BooleanUtil.isTrue(tryLock);
- }
- /**
- * 释放互斥锁
- * @param key
- */
- public void unlock(String key) {
- stringRedisTemplate.delete(key);
- }
- 1.获取商铺id
- 2.从Redis中查询商铺缓存
- 3.判断缓存是否存在
- 3.1缓存不存在,返回空
- 3.2缓存存在,需要判断缓存是否过期
- 4判断缓存是否过期
- 4.1缓存未过期,返回商铺信息
- 4.2缓存过期,尝试获取互斥锁
- 5获取互斥锁
- 6判断互斥锁是否成功
- 6.1获取锁失败,返回商铺信息
- 6.2获取锁成功,再次判断缓存是否过期(这里是避免在获取锁的时间内,缓存已经被重建)
- 6.3缓存未过期,返回商铺信息
- 6.4缓存过期,缓存重建
- 7.开启缓存重建
- 7.1开启独立线程,根据id查询数据库
- 7.2将商铺数据写入Redis建立缓存,并设置逻辑过期时间
- 8释放互斥锁
- 9返回过期的商铺信息
首先要进行数据预热,就是先将热点数据缓存到Redis缓存中。这里建立的缓存就已经是有逻辑时间的缓存
- /**
- * 提前将数据存入Redis(也叫缓存预热)
- * @param id
- */
- public void saveShop2Redis(Long id,Long expireSeconds) {
- Shop shop = query().eq("id", id).one();
- RedisData redisData = new RedisData();
- redisData.setData(shop);
- redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
- stringRedisTemplate.opsForValue().set(CACHE_SHOP_XIANG+id,JSONUtil.toJsonStr(redisData));
-
- }
- @Data
- public class RedisData {
- private LocalDateTime expireTime;
- private Object data;
- }
-
- /**
- * 逻辑过期解决缓存击穿问题
- *
- * @param id
- * @return
- */
- public Shop queryWithShopLogicalExpiration(Long id) {
- //1.获取商铺id
- //2.从Redis中查询商铺缓存
- String shopJSON = stringRedisTemplate.opsForValue().get(CACHE_SHOP_XIANG + id);
- //3.判断缓存是否存在
- if (StrUtil.isBlank(shopJSON)) {
- //3.1缓存不存在,返回空
- return null;
- }
-
- //3.2缓存存在,需要判断缓存是否过期
- RedisData redisData = JSONUtil.toBean(shopJSON, RedisData.class);
- LocalDateTime expireTime = redisData.getExpireTime();
- JSONObject shopJSONObject = (JSONObject) redisData.getData();
- Shop shop = JSONUtil.toBean(shopJSONObject, Shop.class);
- //4判断缓存是否过期
- if (expireTime.isAfter(LocalDateTime.now())) {
- //4.1缓存未过期,返回商铺信息
- return shop;
- }
-
- //4.2缓存过期,尝试获取互斥锁
- //5获取互斥锁
- boolean isLock = tryLock(LOCK_SHOP_KEY + id);
- //6判断互斥锁是否成功
- if (!isLock) {
- //6.1获取锁失败,返回商铺信息
-
- return shop;
- }
- //6.2获取锁成功,再次判断缓存是否过期(这里是避免在获取锁的时间内,缓存已经被重建)
- if (expireTime.isAfter(LocalDateTime.now())) {
- //6.3缓存未过期,返回商铺信息
- return shop;
- }
- //6.4缓存过期,缓存重建
- // 7.开启缓存重建
- //7.1开启独立线程,根据id查询数据库
- CACHE_REBUILD_EXECUTOR.submit(() -> {
- try {
- //7.2将商铺数据写入Redis建立缓存,并设置逻辑过期时间
-
- this.saveShop2Redis(id, 20L);
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- //8释放互斥锁
-
- unlock(LOCK_SHOP_KEY + id);
- }
- });
- //9返回商铺信息
- return shop;
- }
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性
ID组成部分:
符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,可以支持2^32个不同ID
-
- @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.生成时间戳
- LocalDateTime now = LocalDateTime.now();
- 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.拼接并返回
- return timestamp << COUNT_BITS | count;
- }
- }
- 1.获取优惠卷id
- 2.查询优惠卷信息
- 3.判断秒杀是否开始
- 4.判断秒杀是否结束
- 5.判断库存是否充足
- 6.扣减库存
- 7.创建订单
- 7.1获取订单id
- 7.2获取用户id
- 7.3获取代金卷id
- 8.返回订单
- @Override
- @Transactional
- public Result seckillVoucherXiang(Long voucherId) {
- //1.获取优惠卷id
- //2.查询优惠卷信息
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
-
- //3.判断秒杀是否开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
- return Result.fail("秒杀活动尚未开始");
- }
- //4.判断秒杀是否结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
- return Result.fail("秒杀活动已经结束");
- }
- //5.判断库存是否充足
- if (voucher.getStock() < 1) {
- return Result.fail("库存不足");
-
- }
- //6.扣减库存
- boolean istrue = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).update();
- if (!istrue){
- return Result.fail("库存不足");
- }
- //7.创建订单
- VoucherOrder voucherOrder = new VoucherOrder();
- //7.1获取订单id
- long orederID = redisIdWorker.nextId("voucherId");
- voucherOrder.setId(orederID);
- //7.2获取用户id
- Long userID = UserHolder.getUser().getId();
- voucherOrder.setUserId(userID);
- //7.3获取代金卷id
- voucherOrder.setVoucherId(voucherId);
- save(voucherOrder);
- //8.返回订单
- return Result.ok(orederID);
- }
超卖问题就是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
悲观锁:
认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行
乐观锁:
认为线程安全不一定会发生,因此不加锁,只是在更新数据时取判断有没有其他线程对数据做了修改
关于超卖我们可以加一个乐观锁,乐观锁的关键是判断之前查询得到的数据有被修改过,常见的方式有两种:
- //6.扣减库存
- boolean istrue = seckillVoucherService
- .update()
- .setSql("stock=stock-1")
- .eq("voucher_id", voucherId)
- .gt("stock",0)
- .update();
- if (!istrue){
- return Result.fail("库存不足");
- }
超卖这样的线程安全问题,解决方案有哪些
1.悲观锁:添加同步锁,让线程串行执行
2.乐观锁:不加锁,在更新时判断是否有其他线程在修改
秒杀业务,一人只能成功一单
- 1.获取优惠卷id
- 2.查询优惠卷信息
- 3.判断秒杀是否开始
- 3.1没有开始,返回异常结果
- 4.判断秒杀是否结束
- 4.1已经结束,返回异常结果
- 5.判断库存是否充足
- 5.1不足,返回异常结果
- 6.根据优惠卷和用户查询订单
- 7.判断订单是否存在
- 7.1存在,返回异常结果
- 8.不存在.扣减库存
- 9.创建订单
- 9.1获取订单id
- 9.2获取用户id
- 9.3获取代金卷id
- 10.将订单储存在数据库中
- 11.返回订单
-
- @Override
- @Transactional
- public Result seckillVoucherXiang(Long voucherId) {
- //1.获取优惠卷id
- //2.查询优惠卷信息
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- //3.判断秒杀是否开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())){
- //3.1没有开始,返回异常结果
- return Result.fail("活动尚未开始");
- }
- //4.判断秒杀是否结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())){
- //4.1已经结束,返回异常结果
- return Result.fail("活动已经结束");
- }
-
- //5.判断库存是否充足
- if (voucher.getStock()<1){
- //5.1不足,返回异常结果
- return Result.fail("库存不足");
- }
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- //6.根据优惠卷和用户查询订单
- int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();
- //7.判断订单是否存在
- if (count>0){
- //7.1存在,返回异常结果
- return Result.fail("订单已存在,请勿重复下单");
-
- }
- //8.不存在.扣减库存
- boolean update = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
- if (!update){
- return Result.fail("库存不足");
-
- }
- //9.创建订单
- VoucherOrder voucherOrder = new VoucherOrder();
- //9.1获取订单id
- long voucherId1 = redisIdWorker.nextId("voucherId");
- voucherOrder.setId(voucherId1) ;
- //9.2获取用户id
- voucherOrder.setUserId(userID);
-
- //9.3获取代金卷id
- voucherOrder.setVoucherId(voucherId);
- //10.将订单储存在数据库中
- save(voucherOrder);
- //11.返回订单
- return Result.ok(voucherId1);
- }
以上操作,存在线程安全问题,比如多线程下,会在判断订单是否存在的同时,多个线程同时进行,造成一个用户下了不止一单
这时候就要用悲观锁来完成。让获取订单的时候只有一个线程进行,就是在获取UserID的时候加上synchronized。
- @Override
- public Result seckillVoucherXiang(Long voucherId) {
- //1.获取优惠卷id
- //2.查询优惠卷信息
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- //3.判断秒杀是否开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
- //3.1没有开始,返回异常结果
- return Result.fail("活动尚未开始");
- }
- //4.判断秒杀是否结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
- //4.1已经结束,返回异常结果
- return Result.fail("活动已经结束");
- }
-
- //5.判断库存是否充足
- if (voucher.getStock() < 1) {
- //5.1不足,返回异常结果
- return Result.fail("库存不足");
- }
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- synchronized (userID.toString().intern()) {
- return createVoucherOrder(voucherId);
- }
- }
-
-
-
- @Transactional
- public Result createVoucherOrder(Long voucherId) {
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- //6.根据优惠卷和用户查询订单
- int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();
- //7.判断订单是否存在
- if (count > 0) {
- //7.1存在,返回异常结果
- return Result.fail("订单已存在,请勿重复下单");
-
- }
- //8.不存在.扣减库存
- boolean update = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
- if (!update) {
- return Result.fail("库存不足");
-
- }
- //9.创建订单
- VoucherOrder voucherOrder = new VoucherOrder();
- //9.1获取订单id
- long voucherId1 = redisIdWorker.nextId("voucherId");
- voucherOrder.setId(voucherId1);
- //9.2获取用户id
- voucherOrder.setUserId(userID);
-
- //9.3获取代金卷id
- voucherOrder.setVoucherId(voucherId);
- //10.将订单储存在数据库中
- save(voucherOrder);
- //10.返回订单
- return Result.ok(voucherId1);
- }
以上的做法,在逻辑上是没有问题的,但是忽略了@Transactional事务在Service里面方法内部调用会失效的问题Spring事务失效的场景。我这里采用的是通过AopContent类来解决问题。
采用AopContent类解决问题要先完成两步
- <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);
- }
-
- }
- @Override
- public Result seckillVoucherXiang(Long voucherId) {
-
- //1.获取优惠卷id
- //2.查询优惠卷信息
- SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
- //3.判断秒杀是否开始
- if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
- //3.1没有开始,返回异常结果
- return Result.fail("活动尚未开始");
- }
- //4.判断秒杀是否结束
- if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
- //4.1已经结束,返回异常结果
- return Result.fail("活动已经结束");
- }
-
- //5.判断库存是否充足
- if (voucher.getStock() < 1) {
- //5.1不足,返回异常结果
- return Result.fail("库存不足");
- }
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- synchronized (userID.toString().intern()) {
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- }
- }
-
- @Override
- @Transactional
- public Result createVoucherOrder(Long voucherId) {
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- //6.根据优惠卷和用户查询订单
- int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();
- //7.判断订单是否存在
- if (count > 0) {
- //7.1存在,返回异常结果
- return Result.fail("订单已存在,请勿重复下单");
-
- }
- //8.不存在.扣减库存
- boolean update = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).gt("stock", 0).update();
- if (!update) {
- return Result.fail("库存不足");
-
- }
- //9.创建订单
- VoucherOrder voucherOrder = new VoucherOrder();
- //9.1获取订单id
- long voucherId1 = redisIdWorker.nextId("voucherId");
- voucherOrder.setId(voucherId1);
- //9.2获取用户id
- voucherOrder.setUserId(userID);
-
- //9.3获取代金卷id
- voucherOrder.setVoucherId(voucherId);
- //10.将订单储存在数据库中
- save(voucherOrder);
- //10.返回订单
- return Result.ok(voucherId1);
- }
以上的一人一单是在单机模式下可以完成,但是在多机模式就会发生错误,原因是新的一个会有新的JVM,会有不同的锁监视器来监视锁。采用分布式锁可以解决这种问题。
分布式锁:满足分布式系统或集群模式下多进程可见并互斥的锁
有以下几个基本特点
分布式锁的实现
原来加锁是:
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- synchronized (userID.toString().intern()) {
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- }
现在加锁:
- public interface ILockXiang {
- boolean tryLock(Long timestamp);
-
- void unlock();
- }
- public class ILockXiangImpl implements ILockXiang{
- private StringRedisTemplate stringRedisTemplate;
- private Long name;
-
- public ILockXiangImpl(StringRedisTemplate stringRedisTemplate, Long name) {
- this.stringRedisTemplate = stringRedisTemplate;
- this.name = name;
- }
-
- @Override
- public boolean tryLock(Long timestamp) {
- String threadName = Thread.currentThread().getName();
- Boolean istrue = stringRedisTemplate.opsForValue().setIfAbsent("lock" + name, threadName, timestamp, TimeUnit.SECONDS);
-
- return BooleanUtil.isTrue(istrue);
- // return Boolean.TRUE.equals(istrue);
- }
-
- @Override
- public void unlock() {
- stringRedisTemplate.delete("lock" + name);
- }
- }
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- ILockXiangImpl iLockXiang = new ILockXiangImpl(stringRedisTemplate, userID);
- boolean tryLock = iLockXiang.tryLock(1200L);
- if (!tryLock) {
- return Result.fail("您已经下过单了,请到下单界面查看详情");
- }
-
- try {
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- } finally {
- iLockXiang.unlock();
- }
这样在两个客户端同时发请求,也会锁的住.但是存在一个问题,就是锁误删的情况。比如A处理业务,加锁但是业务时间超过了加锁时间,锁超时会自动释放,A业务并不知道仍旧在处理A业务,这时B业务过来,因为上一个锁已经被释放,所以B业务同样可以获得锁,如果A业务在B业务处理前释放锁的话,这里A业务释放的锁就是B业务的锁。
所以释放锁的时候要进行判断,这个锁是不是自己的锁。在尝试获取锁已经释放锁的地方加入判断满足:
- public class ILockXiangImpl implements ILockXiang {
- private StringRedisTemplate stringRedisTemplate;
- private Long name;
- private static final String LOCK_PREFIX = UUID.randomUUID().toString(true);
-
- public ILockXiangImpl(StringRedisTemplate stringRedisTemplate, Long name) {
- this.stringRedisTemplate = stringRedisTemplate;
- this.name = name;
- }
-
- @Override
- public boolean tryLock(Long timestamp) {
- String threadName = LOCK_PREFIX + Thread.currentThread().getName();
- Boolean istrue = stringRedisTemplate.opsForValue().setIfAbsent("lock" + name, threadName, timestamp, TimeUnit.SECONDS);
-
- return BooleanUtil.isTrue(istrue);
- // return Boolean.TRUE.equals(istrue);
- }
-
- @Override
- public void unlock() {
- String threadName = LOCK_PREFIX + Thread.currentThread().getName();
- String id = stringRedisTemplate.opsForValue().get("lock" + name);
- if (id.equals(threadName)){
- stringRedisTemplate.delete("lock" + name);
- }
-
- }
- }
基于setnx实现的分布式锁存在以下问题:
Redisson是一个在Redis的基础上实现的Java驻内存数据网络。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
官方地址https://redisson.org GitHub地址https://github.com/redisson/redisson
- <!--redisson-->
- <dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson</artifactId>
- <version>3.13.6</version>
- </dependency>
- @Configuration
- public class RedissonConfig {
-
- @Bean
- public RedissonClient redissonClient(){
- // 配置
- Config config = new Config();
- config.useSingleServer().setAddress("redis://192.168.80.135:6379");
- // 创建RedissonClient对象
- return Redisson.create(config);
- }
- }
- @Resource
- private RedissonClient redissonClient;
使用Redisson锁后如何加锁
- UserDTO user = UserHolder.getUser();
- Long userID = user.getId();
- RLock lock = redissonClient.getLock(LOCK_SHOP_KEY + userID);
- boolean tryLock = lock.tryLock();
- if (!tryLock) {
- return Result.fail("您已经下过单了,请到下单界面查看详情");
- }
-
- try {
- IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
- return proxy.createVoucherOrder(voucherId);
- } finally {
- lock.unlock();
- }
原本为以下构造,串行化一条龙进行,但是减库存以及创建订单的操作是针对于数据库,而且是写入操作,效率会比较低的,如果一条龙的进行,一次的时间过于漫长
这是优化后的样子,将其拆分为两个部分,一个复杂前面的,一个负责后面的数据库的操作,这样,前面的进行完了就可以直接返回
改进秒杀业务,提高并发性能
1.新增秒杀优惠卷的同时,将优惠卷信息保存到Redis中
- @Override
- @Transactional
- public void addSeckillVoucher(Voucher voucher) {
- // 保存优惠券
- save(voucher);
- // 保存秒杀信息
- SeckillVoucher seckillVoucher = new SeckillVoucher();
- seckillVoucher.setVoucherId(voucher.getId());
- seckillVoucher.setStock(voucher.getStock());
- seckillVoucher.setBeginTime(voucher.getBeginTime());
- seckillVoucher.setEndTime(voucher.getEndTime());
- seckillVoucherService.save(seckillVoucher);
- // 保存秒杀库存到Redis中
- stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
- }
- }
2.基于LUA脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
2.1执行lua脚本
- private static final DefaultRedisScript<Long> SECKILXAINGL_SCRIPT;
-
- static {
- SECKILXAINGL_SCRIPT = new DefaultRedisScript<>();
- SECKILXAINGL_SCRIPT.setLocation(new ClassPathResource("seckillxiang.lua"));
- SECKILXAINGL_SCRIPT.setResultType(Long.class);
- }
- @Override
- public Result seckillVoucherXiang(Long voucherId) {
- Long userId = UserHolder.getUser().getId();
-
- /**
- execute需要传三个参数
- 1.lua脚本
- 2.KEYS[] 如果为null不可以直接传null,要传一个空的List串Collections.emptyList(),
- 3.ARGV[]
- */
- Long lua = stringRedisTemplate.execute(
- SECKILXAINGL_SCRIPT,
- Collections.emptyList(),
- voucherId.toString(), userId.toString()
- );
- int i = lua.intValue();
- if (i!=0){
- if (i==1){
- return Result.fail("库存不足");
- }
- else {
- return Result.fail("请不要重复下单");
- }
- }
- return Result.ok(0);
-
- }
2.2lua脚本
- --1.判断库存是否充足
- --1.1获取优惠卷id
- local voucherId=ARGV[1]
- --1.2查询优惠卷id的库存是否充足
- --redis.call("get",voucherId)
- --1.3获取库存key
- local stockKey="seckillxiang:stock" ..voucherId
- --1.4库存充足,判断用户是否下单
- if(tonumber(redis.call("get",stockKey))<=0) then
- --1.5库存不足返回1
- return 1
- end
-
- --2.判断用户是否下单
- --2.1获取用户id
- local userID=ARGV[2]
- --2.2获取订单Key
- local orderKey="seckillxiang:order" .. userID
- --2.2根据用户id查询是否在set集合中存在
- --redis.call("sismember",orderKey,userID)
-
- --2.3用户已下单返回2
- if(redis.call("sismember",orderKey,userID)==1) then
- return 2
- end
- --3.扣减库存
- redis.call("incrby",stockKey,-1)
- --3.1将userID存入当前优惠卷的Set集合
- redis.call("sadd",orderKey,userID)
- --3.2返回0
- return 0
- -- 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
3.如果抢购成功,将优惠卷id和用户id封装后存入阻塞队列
4.开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能