level1 单机服务
最开始先假设为单机服务,该服务的功能是售卖某件商品。那么controller层的大概业务可以这么写:
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() {
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
}
}
这个是最初的版本,会出现单机超卖的情况,考虑加锁来进行改进
level2 单机加锁版
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public synchronized String buy_Goods() {
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
}
level3 分布式微服务版1.0
假设现在服务由原先的1台变成多台,那么就需要进行分布式部署。在这种情况下单机锁只能够锁住本机,但是无法锁住其他的服务,因此在分布式的情况下,单机锁失效了,此时应该将锁加在redis上,即使用redis分布式锁。
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
private String REDIS_LOCK = "REDIS_LOCK";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = template.setIfAbsent(REDIS_LOCK, value);
if (Boolean.FALSE.equals(flag)){
//没有抢到锁
return "此次没有抢到锁~";
}
try{
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
}
finally{
template.delete(REDIS_LOCK);
}
}
level4 分布式微服务版2.0
在1.0版本中,可以发现一个问题:如果在运行该服务时,服务主机宕机了,那么Redis锁就无法删除,其他主机的相同服务会一直等待,导致该业务出现异常。因此可以替锁加上一个过期时间,来保证在主机出现问题时,也能够正常运行。这里假设Redis锁保存10秒就自动删除。
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
private String REDIS_LOCK = "REDIS_LOCK";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = template.setIfAbsent(REDIS_LOCK, value, 10L, TimteUnit.SECONDS);
if (Boolean.FALSE.equals(flag)){
//没有抢到锁
return "此次没有抢到锁~";
}
try{
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
}
finally{
template.delete(REDIS_LOCK);
}
}
level5 分布式微服务版3.0
2.0版中解决了主机宕机,redis锁无法删除的情况,引入了过期时间,但这又会产生一个新的问题:假设服务A在执行逻辑时超过了锁自动删除的时间,此时其他服务就可以申请锁了,这里假设服务B申请了锁,而服务A最终会删除锁,此时删除的是服务B的锁,这就乱套了,即服务只能删除自己的锁,不能删除其他服务的锁。
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
private String REDIS_LOCK = "REDIS_LOCK";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = template.setIfAbsent(REDIS_LOCK, value, 10L, TimteUnit.SECONDS);
if (Boolean.FALSE.equals(flag)){
//没有抢到锁
return "此次没有抢到锁~";
}
try{
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
}
finally{
if (template.opsForValue().get(REDIS_LOCK).equals(value)){
template.delete(REDIS_LOCK);
}
}
}
level5 分布式微服务版4.0
3.0版本保证了服务只删除自己加的锁,但是这一过程不是原子性的,此时有两种方法来解决:
- 使用LUA脚本来实现删除锁这一过程
- 使用事务来实现删除锁这一过程。
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
private String REDIS_LOCK = "REDIS_LOCK";
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = template.setIfAbsent(REDIS_LOCK, value, 10L, TimteUnit.SECONDS);
if (Boolean.FALSE.equals(flag)){
//没有抢到锁
return "此次没有抢到锁~";
}
try{
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
}
finally{
//使用事务来保证原子性
while (true) {
template.watch(REDIS_LOCK);
if (template.opsForValue().get(REDIS_LOCK).equals(value)) {
template.setEnableTransactionSupport(true);
template.multi();
template.delete(REDIS_LOCK);
List<Object> list = template.exec();
if (list == null) continue;
}
template.unwatch();
break;
}
//使用lua脚本来保证原子性
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1]" +
"then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
try{
Object o = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
if ("1".equals(o)){
System.out.println("删锁成功");
}
else {
System.out.println("删锁失败");
}
}
finally{
if (jedis != null)
jedis.close();
}
}
}
level5 分布式微服务版4.0
以上分布式版本都存在一个问题:如何保证业务的执行时间小于锁的过期时间?
并且,假设redis现在也是集群环境,会出现一种情况:在master进行了数据的修改,在还未异步同步到slave节点时,master宕机了,slave节点升级为master节点,此时会出现数据不一致的问题。
为了解决以上问题,使用redisson来解决分布式锁问题。
@RestController
public class GoodController {
private static final String REDIS_LOCK = "LOCK";
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
try {
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
} finally {
lock.unlock();
}
}
}
level6 分布式微服务final版
使用redisson基本上解决了分布式下的并发需求,但是有可能会出现异常:IllegalMonitorStateException:attempt to unlock lock, not locked by current thread by node id: xxxx。即想要解锁,但发现不是由当前线程加的锁。可以在unlock处进行进一步的改进。
@RestController
public class GoodController {
private static final String REDIS_LOCK = "LOCK";
@Autowired
private StringRedisTemplate template;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
try {
String result = template.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
template.opsForValue().set("goods:001", String.valueOf(realNumber));
System.out.println("成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort);
return "成功买到商品,库存还剩下: " + realNumber + "件, 服务端口: " + serverPort;
}
System.out.println("商品已经售完~ 服务端口: " + serverPort);
return "商品已经售完~ 服务端口: " + serverPort;
} finally {
if (lock.isLocked()){
if (lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
}