

代码实现:
- @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.aspectjgroupId>
- <artifactId>aspectjweaverartifactId>
- 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
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替换自己的实现的分布式锁