1. redis的使用场景
redis使用场景的案例:
[1]热点数据的缓存
[2]分布式锁
[3]短信业务(登录注册时)
2. redis实现注册登录功能
代码
在发送验证码时,先判断数据库是否有该手机号,有则发送验证码(此时redis缓存中有发送过该验证码,则返回已发送,防止多次发送验证码)并储存到redis中(手机号作为唯一的在加上过期时间)。
在登录验证手机号和验证码时,根据输入的手机号(唯一key在redis中查询)是否跟输入的验证码(不为空)时相同,登录成功,并删除该redis数据(验证码只能作为一次登录)。
@Autowired
private 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 {
@Autowired
private StockDao stockDao;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public 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;
}
@Override
public Stock insert(Stock stock) {
int insert = stockDao.insert(stock);
return stock;
}
@Override
public Stock update(Stock stock) {
//修改数据库
int i = stockDao.updateById(stock);
if(i>0){
//修改缓存
redisTemplate.opsForValue().set("stock::"+stock.getProductid(),stock);
}
return stock;
}
@Override
public 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缓存使用的组件
配置文件
@Bean
public 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 {
@Autowired
private StockDao stockDao;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
//Cacheable:表示查询时使用的注解。 cacheNames:缓存的名称 key:缓存的唯一表示值
// 1. 查询缓存中是否存在名称为cacheNames::key的值
// 2. 如果存在则方法不会执行
// 3. 如果不存在则执行方法体并把方法的返回结果放入缓存中cacheNames::key
@Cacheable(cacheNames ={ "stock"}, key = "#id")
@Override
public Stock getById(Integer id) {
Stock stock = stockDao.selectById(id);
return stock;
}
@Override
public Stock insert(Stock stock) {
int insert = stockDao.insert(stock);
return stock;
}
//CachePut:表示修改时使用的注解.
// 1. 先执行方法体
// 2. 把方法的返回结果放入缓存中
@CachePut(cacheNames = "stock", key = "#stock.productid")
@Override
public Stock update(Stock stock) {
int i = stockDao.updateById(stock);
return stock;
}
//CacheEvict:表示删除时使用的注解
// 1. 先执行方法体
// 2. 把缓存中名称为cacheNames::key的值删除
@CacheEvict(cacheNames = "stock", key = "#productid")
@Override
public 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 {
@Bean
public 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;
}
}
修改原有代码
@Autowired
private 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();
}
}
可以解决该问题。