解决思路
从读到写这段时间的数据不一致问题,根源在于用户并行(个人认为并发是时间概念,并行是空间概念),
要解决这个问题,需要让用户串行,单个用户原子性。锁 说它可以做到。
锁只有一个目的,就是把并行变为串行,但是上锁的方式 五花八门。
1. Java应用内存锁
Java中自带很多内存锁,synchronize,各种Lock,但是优惠券服务多机部署,内存锁无法满足需求;
2. Mysql数据库锁
优惠券服务使用MySql(一个写节点),innodb存储引擎,innodb 支持 行锁。
利用innodb的行锁机制,可以使用两种方式实现用户领券的原子性:
第一种,读取之前上锁, 更新之后解锁
select ... from table where ... for update;
update table set ....
优点: 简单明了; 缺点: select 和 update 之间处理 出异常或应用异常终止 会产生死锁。
第二中,利用update 锁行机制,加上where 条件 判断数据,也是读取前上锁,更新后解锁。
update table set .... where ....
优点:简单明了; 缺点: 效率不高
另外更新操作直接命中数据库会对数据库产生很大的压力,所以数据库锁无法满足抢券业务;
3. Redis分布式内存锁
优惠券服务使用单节点Redis,Redis 支持setnx命令。
利用setnx命令,可以在应用中自建锁及维护锁的生命周期。
基本思路是领券前将优惠券的key通过 setnx 命令写进 redis,成功则之后便执行后续的三次读取 比较 和更新,
最后 del 命令删除优惠券的key。
优点:逻辑简单,实现简单,total_got,user_got,user_today_got 三个值 存哪里不受任何限制。
缺点:不太可靠,setnx 成功后,应用出现异常,没有执行最后的del , 会产生死锁;也可以在 setnx 后再
设置一个过期时间,是的,这是一个办法,只需要保证过期时间大于 接口的最大执行时间。
另外,也可以使用 官方推荐的 分布式Redis锁 开源实现 Redisson。
以redis为例:
@RequestMapping("testRedisson")
@ResponseBody
public String testRedisson(){
Jedis jedis = redisUtil.getJedis();
RLock lock = redissonClient.getLock("lock");// 声明锁
lock.lock();//上锁
try {
String v = jedis.get("k");
if (StringUtils.isBlank(v)) {
v = "1";
}
System.out.println("->" + v);
jedis.set("k", (Integer.parseInt(v) + 1) + "");
}finally {
jedis.close();
lock.unlock();// 解锁
}
return "success";
}