文章目录
一、基础知识
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();
}
}
}
}