全局ID生成器
代码实现:
@Component
public class RedisIDGenerator {
//获取2022-01-01 00:00:00的时间戳为1640995200L
private static final long BEGIN_TIMESTAMP = 1640995200L;
private static final int COUNT_BITS = 32;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 符号位 时间戳(31 bit) 序列号(32bit)
* 0 - 00000000 00000000 00000000 0000000 - 00000000 00000000 00000000 00000000
*/
public long nextId(String keyPrefix) {
//1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
//2.生产序列号
//2.1获取当天的日期,请求到天
// 一个key是有数量上限的,所以添加上当天的日期,就减少了超过上限的概率
//yyyy:MM:dd 因为Redis可以根据:有层级关系,则可以统计每天,每月和每年的数量
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//2.2自增长
Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
//3.拼接返回
return timestamp << COUNT_BITS | count;
}
public static void main(String[] args) {
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
System.out.println(time.toEpochSecond(ZoneOffset.UTC));
}
}
超卖问题
使用乐观锁解决超卖问题:
假设sql: update order set num = num -1 where num = ?;
这个存在一个问题,多个人来同时购买时,会出现库存依旧还有,但是很多购买失败的问题,则修改sql为
update order set num = num -1 where num >0;
一人一单
单机
public interface SinglePay {
void buyOrder(Long userId);
String createOrder(Long userId);
}
@Service
public class SinglePayImpl implements SinglePay {
public void buyOrder(Long userId) {
//.....其他操作
//synchronized放在这里保护整个事务在同步块内
synchronized (userId.toString().intern()) {
//因为createOrder方法存在事务,如果直接this.createOrder的调用则用的spring中的SinglePayUtil对象
//不是代理对象则事务将不会生效,则不能直接调用createOrder方法,
// createOrder(userId);
// 需要拿到代理对象,调用代理对象createOrder方法,事务才会生效
SinglePay proxy = (SinglePay) AopContext.currentProxy();
proxy.createOrder(userId);
}
}
//每人只能下一单
//synchronized不放在方法上所有的用户公用这个类的同一个锁,影响性能
//则用在userId上
@Transactional
public String createOrder(Long userId) {
/**
* Long是一个对象,但相同值得Long每一次对象都是不同的,
* 所以要对值加锁,但是toString的底层是new String所以还是对象加锁
* 所以要调用String的intern方法,再常量池中找值相同的引用,则所有都一样
* 但是synchronized放在这里也有问题, Transaction的事务是在方法结束时才会提交,
* 在并发的情况下也会存在事务还没提交,锁已经释放,存在可能同一个ID进入同步块的情况,所以要放在整个方法的外面
* 保证事务提交后,才能再次进入此方法
*/
// synchronized (userId.toString().intern()){
//查询数据库order中userId是否存在记录,存在则不让购买
int count = queryOrderCount(userId);
if (count > 0) {
//已买过购买失败
return "fail";
}
boolean success = order_count(userId);
if (!success) {
return "库存不足";
}
saveOrder(userId);
return "成功";
// }
}
private void saveOrder(Long userId) {
//模拟提交订单插入数据库
//insert into order values(userId.....);
}
private boolean order_count(Long userId) {
//修改库存
//update order_count set count = count -1 where userId =? and count >0;
return true;
}
private int queryOrderCount(Long userId) {
//模拟查询数据库
//select count(*) from order where userId = ?
return 0;
}
}
Note:使用代理对象需要配置
- 引入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
-
启动类上启动Aspectj
//exposeProxy默认是false,不暴露代理对象,需要设置为true,code中才能获取代理对象
@EnableAspectJAutoProxy(exposeProxy = true)
分布式
单机的情况下虽然利用锁解决了一人一单的问题, 但是在分布式的情况下,锁只能保证在自己jvm中不会同一个userId进入代码块,多个jvm中就不能保证
分布式锁
问题1: 业务时间过长导致锁超时自动释放,其他线程获取到锁,进行操作时,前一个线程完成业务删除了锁,导致多个线程同时操作需要同步的业务。
解决方案:Redis加锁时,value为当前线程ID,则删除时判断锁的value是否是自己的线程id,如果不是则不删除。但是多个jvm生成的线程ID也可能相同,所以可以在线程ID和UUID进行拼接,减少重复的概率。
@Component
public class SimpleRedisLock implements ILock {
private static final String KEY_PREFIX = "lock:";
//不带横线的uuid
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean tryLock(String key, long timeoutSec) {
//获取当前线程的ID作为和 UUID 拼接作为value
String threadId = ID_PREFIX + Thread.currentThread().getId();
//获取锁
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(
KEY_PREFIX + key, threadId, timeoutSec, TimeUnit.SECONDS);
//可能存在拆箱问题
return Boolean.TRUE.equals(success);
}
@Override
public void unlock(String key) {
String threadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + key);
if (StrUtil.isEmpty(threadId)){
return;
}
String currentThreadId = ID_PREFIX + Thread.currentThread().getId();
if (currentThreadId.equals(threadId)){
stringRedisTemplate.delete(KEY_PREFIX + key);
}
}
}
/**
* 实现分布式锁
*/
@Service
public class DistributePayImpl implements SinglePay {
@Autowired
SimpleRedisLock simpleRedisLock;
public void buyOrder(Long userId) {
String lockKey = "order:" + userId;
boolean isLock = simpleRedisLock.tryLock(lockKey, 10);
if (!isLock) {
//获取锁失败,返回错误或重试
//"一人只能下一单"
return;
}
try {
SinglePay proxy = (SinglePay) AopContext.currentProxy();
proxy.createOrder(userId);
} finally {
simpleRedisLock.unlock(lockKey);
}
}
@Transactional
public String createOrder(Long userId) {
//查询数据库order中userId是否存在记录,存在则不让购买
int count = queryOrderCount(userId);
if (count > 0) {
//已买过购买失败
return "fail";
}
boolean success = order_count(userId);
if (!success) {
return "库存不足";
}
saveOrder(userId);
return "成功";
// }
}
private void saveOrder(Long userId) {
//模拟提交订单插入数据库
//insert into order values(userId.....);
}
private boolean order_count(Long userId) {
//修改库存
//update order_count set count = count -1 where userId =? and count >0;
return true;
}
private int queryOrderCount(Long userId) {
//模拟查询数据库
//select count(*) from order where userId = ?
return 0;
}
}
问题2:当第一个线程,业务操作完成后,并且判断了Redis锁的value是自己的线程ID,准备进行删除时,遇到了阻塞(eg:垃圾回收)导致超时自动释放,并且有另一个线程已经获取到锁,并且开始执行自己的业务时,第一个线程阻塞完成了继续删除锁的操作,则删除了另一线程的锁
解决方案:需要获取锁,判断是否为自己的锁 和 删除锁具有原子性。可以使用Lua脚本
--获取锁中的线程标示 get key
local id = redis.call('get',KEYS[1])
--比较线程标示和锁中的标示是否一致
if(id == ARGV[1]) then
--释放锁
return redis.call('del',KEYS[1])
end
return 0
代码实现:
代码中创建Lua文件,idea中需要安装EmmyLua插件
//接受lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
/**
* 基于Lua脚本解锁
*/
@Override
public void unlock(String key) {
//使用execute执行脚本指令
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + key),
ID_PREFIX + Thread.currentThread().getId());
}
分布式锁依然存在的问题:
使用Redisson替换自己的实现的分布式锁