所有的商品服务都去占同一把锁。
利用redis,的set nx , 当redis中不存在时才放入redis
set nx 是一个原子性的操作,只要set失败的线程就认为占锁失败。
进入虚拟机
输入
docker exec -it redis redis-cli
进入redis,模拟多个服务set nx
set lock haha NX
只有一个人会返回OK,其他都是nil
这样是否存在问题?
public Map> getCatalogJsonFromDbWithRedisLock() { //占分布式锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111"); if (lock) { //加锁成功,执行业务 return getStringListMap(); }else { //加锁失败 } }
synchronized `自旋`:同步锁会在这里一直监听,别人一释放他就会拿到。一直重试
public Map> getCatalogJsonFromDbWithRedisLock() { //占分布式锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111"); if (lock) { //加锁成功,执行业务 Map > listMap = getStringListMap(); redisTemplate.delete("lock");//删除锁 return listMap; }else { //加锁失败 //可以休眠100ms重试,调用自己 return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
如果出现异常,没有删锁,就会导致死锁。
我们可以给锁设置过期时间
if (lock) {
//设置过期时间
redisTemplate.expire("lock",30,TimeUnit.SECONDS);
//加锁成功,执行业务
Map> listMap = getStringListMap();
redisTemplate.delete("lock");//删除锁
return listMap;
}else {
那如果在if下面,设置过期时间上面断电怎么办?
我们必须保证加锁和删除锁是一个原子操作
所以要写成这样
public Map> getCatalogJsonFromDbWithRedisLock() { //占分布式锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS); if (lock) { //设置过期时间 //redisTemplate.expire("lock",30,TimeUnit.SECONDS); //加锁成功,执行业务 Map > listMap = getStringListMap(); redisTemplate.delete("lock");//删除锁 return listMap; }else { //加锁失败 //可以休眠100ms重试,调用自己 return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
再一次改进
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
if (lock) {
//设置过期时间
//redisTemplate.expire("lock",30,TimeUnit.SECONDS);
//加锁成功,执行业务
Map> listMap = getStringListMap();
//获取值-对比-对比成功再删除 == 原子操作
String lock1 = redisTemplate.opsForValue().get("lock");
if (uuid.equals(lock1)) {
redisTemplate.delete("lock");//删除锁
}
return listMap;
}else {
但是还是会出现删除别人的锁的情况。
需要用到lua脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
1.2 采用Redission框架
@Controller
public class IndexController {
@Autowired
RedissonClient redisson;
@GetMapping("/hello")
@ResponseBody
public String hello(){
//获取锁
RLock lock = redisson.getLock("my-lock");
//加锁
lock.lock(); //阻塞式等待,直到拿到锁才运行下面方法
try {
System.out.println("加锁成功,执行业务" + Thread.currentThread().getId());
//模拟业务
Thread.sleep(30000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
System.out.println("释放锁:"+Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
}
redission的强大之处:
1.锁的自动续期,如果业务超长,会在运行期间会自动给锁续上新的TTL,不用担心业务时间长,锁自动过期被删掉。
2.加锁的业务只要运行完成,不会给当前锁续期,即使不手动解锁,锁默认也会在30S后自动删除。
读写锁,保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁。
写锁没有释放,读就必须等待。
写+读:等待写锁释放
写+写:阻塞方式
读+写:
@GetMapping("/write")
@ResponseBody
public String writevalue(){
//读写锁
RReadWriteLock rwLock = redisson.getReadWriteLock("rw-lock");
String s = "";
//写锁
RLock writeLock = rwLock.writeLock();
try {
//改数据加写锁
writeLock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(3000);
stringRedisTemplate.opsForValue().set("writevalue",s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue(){
RReadWriteLock rwLock = redisson.getReadWriteLock("rw-lock");
//读锁
RLock readLock = rwLock.readLock();
String s = "";
readLock.lock();
try {
s = stringRedisTemplate.opsForValue().get("writevalue");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return s;
}
1.3 缓存里面的数据如何和数据库保存一致?
1.双写模式
即更新数据库的数据后,随即更新缓存里的数据
存在问题:
2.失效模式即更新数据库的数据后删除缓存中的数据
存在问题:
3.总结无论是双写模式还是失效模式,都会有缓存的不一致的问题,即并发情况下同时更新就会有不一致的问题
解决方式:
如果是用户维度数据(比如订单数据,用户数据),这种并发几率小的数据,可以不考虑这方面的问题。可以使用过期时间这一方式,让数据每一段时间触发数据的读的主动更新即可如果是菜单,商品介绍等基础数据,可以使用Canal(阿里开源中间件)订阅binlog的方式通常情况下,使用缓存数据 + 过期时间 就可以解决大部分业务的需求
通过加锁保证并发读写,写写的时候按顺序排队(读写锁)
总而言之:
缓存中的数据不应该是实时性,一致性要求较高的数据大部分业务使用缓存数据 + 过期时间,保证每天能拿到最新数据即可实时性,一致性较高的数据应该存储在数据库
一般缓存数据一致性解决方案:
- 缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新缓存
- 读写数据的时候,加上分布式读写锁



