1. redis的使用场景
redis使用场景的案例:[1]热点数据的缓存[2]分布式锁[3]短信业务(登录注册时)
2. redis实现注册登录功能
代码
在发送验证码时,先判断数据库是否有该手机号,有则发送验证码(此时redis缓存中有发送过该验证码,则返回已发送,防止多次发送验证码)并储存到redis中(手机号作为唯一的在加上过期时间)。
在登录验证手机号和验证码时,根据输入的手机号(唯一key在redis中查询)是否跟输入的验证码(不为空)时相同,登录成功,并删除该redis数据(验证码只能作为一次登录)。
@Autowiredprivate StringRedisTemplate redisTemplate;@GetMapping("send")public R send(String phone) throws Exception {//1. 校验手机号是否存在---连接数据库if(phone.equals("15137437506")||phone.equals("15959715454")){if(redisTemplate.hasKey("code::"+phone)){return new R(500,"验证码已发送",null);}//2. 发生验证码String code = SendMsgUtil.sendCode(phone);//3. 保存验证码到redis.redisTemplate.opsForValue().set("code::"+phone,code,5, TimeUnit.MICROSECONDS);return new R(200,"发送成功",null);}return new R(500,"手机号未注册",null);}@PostMapping("login")public R login(@RequestBody LoginVo loginVo){//1. 校验验证码String code = redisTemplate.opsForValue().get("code::" + loginVo.getPhone());String phone = loginVo.getPhone();if(StringUtils.hasText(loginVo.getCode()) && loginVo.getCode().equals(code)){if(phone.equals("18839986970")||phone.equals("15137437506")){redisTemplate.delete("code::"+phone);return new R(200,"登录成功",null);}else{return new R(500,"手机号错误",null);}}return new R(500,"验证码错误",null);}
3. 热点数据缓存
为了把一些经常访问的数据,放入缓存中以减少对数据库的访问频率。从而减少数据库的压力,提高程序的性能。【内存中存储】
3.1 缓存的原理
3.2 java使用redis如何实现缓存功能
在增删改查中模拟redis缓存
@Service
public class StockServiceImpl02 implements StockService {@Autowiredprivate StockDao stockDao;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic Stock getById(Integer id) {//1.查询redis缓存是否命中ValueOperations<String, Object> forValue = redisTemplate.opsForValue();Object o = forValue.get("stock::" + id);//System.out.println(o);//表示缓存命中if(o!=null){return (Stock) o;}//查询数据库Stock stock = stockDao.selectById(id);if(stock!=null){forValue.set("stock::" + id,stock);}return stock;}@Overridepublic Stock insert(Stock stock) {int insert = stockDao.insert(stock);return stock;}@Overridepublic Stock update(Stock stock) {//修改数据库int i = stockDao.updateById(stock);if(i>0){//修改缓存redisTemplate.opsForValue().set("stock::"+stock.getProductid(),stock);}return stock;}@Overridepublic int delete(Integer productid) {int i = stockDao.deleteById(productid);if(i>0){//删除缓存redisTemplate.delete("stock::"+productid);}return i;}
}
发现在查询时会访问redis缓存,如果命中则直接返回数据,未命中则查询数据库并放入redis缓存;在修改时保持数据一致,需要redis中数据一起改变;在删除时,数据库和redis缓存一起删除。
3.2 使用缓存注解完成缓存功能
使用AOP面向切面编程—spring缓存使用的组件
配置文件
@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置序列化(解决乱码的问题),过期时间600秒RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化.disableCachingNullValues();RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();return cacheManager;}
在主函数上需要启动:@EnableCaching
修改上述代码
@Service
public class StockServiceImpl implements StockService {@Autowiredprivate StockDao stockDao;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;//Cacheable:表示查询时使用的注解。 cacheNames:缓存的名称 key:缓存的唯一表示值// 1. 查询缓存中是否存在名称为cacheNames::key的值// 2. 如果存在则方法不会执行// 3. 如果不存在则执行方法体并把方法的返回结果放入缓存中cacheNames::key@Cacheable(cacheNames ={ "stock"}, key = "#id")@Overridepublic Stock getById(Integer id) {Stock stock = stockDao.selectById(id);return stock;}@Overridepublic Stock insert(Stock stock) {int insert = stockDao.insert(stock);return stock;}//CachePut:表示修改时使用的注解.// 1. 先执行方法体// 2. 把方法的返回结果放入缓存中@CachePut(cacheNames = "stock", key = "#stock.productid")@Overridepublic Stock update(Stock stock) {int i = stockDao.updateById(stock);return stock;}//CacheEvict:表示删除时使用的注解// 1. 先执行方法体// 2. 把缓存中名称为cacheNames::key的值删除@CacheEvict(cacheNames = "stock", key = "#productid")@Overridepublic int delete(Integer productid) {int i = stockDao.deleteById(productid);return i;}
}
4. 分布式锁
为了模拟高并发:---使用jmeter压测工具
模拟客户端请求环境
public String decrement(Integer productid) {//根据id查询商品的库存int num = stockDao.findById(productid);if (num > 0) {//修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}
}
运行结果:发现出现超卖重卖现象
解决办法:我们使用synchronized或者lock锁
public String decrement(Integer productid) {//根据id查询商品的库存synchronized (this) {int num = stockDao.findById(productid);if (num > 0) {//修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}}
}
再次测试:发现超卖重卖现象没有发生
上面使用syn和lock虽然解决了并发问题,但是我们未来项目部署时可能要部署集群模式。
在springboot模拟项目集群
接下来使用nginx代理集群
在nginx.conf配置文件中设置代理服务器
启动nginx,再次压测发现依旧会超卖
在多线程环境中上述的锁对象是本地锁,每个服务器都有本地锁,导致锁不是唯一的
通过压测发现本地锁 无效了。使用redis解决分布式锁文件
引入redis依赖和配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
spring.redis.host=172.16.7.110
spring.redis.port=6379
修改原有代码
Redis提供了一个命令
setnx
可以来实现分布式锁,该命令只在键key
不存在的情况下 将键key
的值设置为value
,若键key
已经存在, 则SETNX
命令不做任何动作。根据这一特性我们就可以制定Redis实现分布式锁的方案了。
@Autowired
private StringRedisTemplate redisTemplate;//
public String decrement(Integer productid) {ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();//1.获取共享锁资源Boolean flag = opsForValue.setIfAbsent("product::" + productid, "1111", 30, TimeUnit.SECONDS);//表示获取锁成功if(flag) {try {//根据id查询商品的库存int num = stockDao.findById(productid);if (num > 0) {//修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}}finally {//释放锁资源redisTemplate.delete("product::"+productid);}}else{//休眠100毫秒 在继续抢锁try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}return decrement(productid);}}
再次压测
上述发现并没有超卖重卖,,但是
锁超时问题
这里有一个问题,如果获取到锁的服务在执行方法体(释放锁的时候)宕机了,那锁不就释放不了么,别的服务也就没办法获取到锁,就造成了死锁。
使用redisson
在执行方法体的时候锁的时间到了或者宕机,watch dog会检测持有锁的进程给其增加锁时间(大约3次如果还没执行完或者宕机,该线程会结束)可以自动删除锁,别的服务就可以获取锁了,
引入依赖
<!--ression依赖--><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.24.3</version></dependency>
设置配置文件
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redisson(){Config config = new Config();
// //连接的为redis集群
// config.useClusterServers()
// // use "rediss://" for SSL connection
// .addNodeAddress("redis://127.0.0.1:7181","","","")
// ;//连接单机config.useSingleServer().setAddress("redis://192.168.111.188:6379");RedissonClient redisson = Redisson.create(config);return redisson;}
}
修改原有代码
@Autowiredprivate RedissonClient redisson;public String decrement(Integer productid) {RLock lock = redisson.getLock("product::" + productid);lock.lock();try {int num = stockDao.findById(productid);if (num > 0) {//修改库存stockDao.update(productid);System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";} else {System.out.println("商品编号为:" + productid + "的商品库存不足。");return "商品编号为:" + productid + "的商品库存不足。";}} finally {lock.unlock();}}
可以解决该问题。